diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go
index 26e82bf13acf..f63c5c5c52b3 100644
--- a/modules/charset/escape_test.go
+++ b/modules/charset/escape_test.go
@@ -132,18 +132,10 @@ then resh (ר), and finally heh (ה) (which should appear leftmost).`,
},
}
-type nullLocale struct{}
-
-func (nullLocale) Language() string { return "" }
-func (nullLocale) Tr(key string, _ ...interface{}) string { return key }
-func (nullLocale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string { return "" }
-
-var _ (translation.Locale) = nullLocale{}
-
func TestEscapeControlString(t *testing.T) {
for _, tt := range escapeControlTests {
t.Run(tt.name, func(t *testing.T) {
- status, result := EscapeControlString(tt.text, nullLocale{})
+ status, result := EscapeControlString(tt.text, &translation.MockLocale{})
if !reflect.DeepEqual(*status, tt.status) {
t.Errorf("EscapeControlString() status = %v, wanted= %v", status, tt.status)
}
@@ -179,7 +171,7 @@ func TestEscapeControlReader(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
input := strings.NewReader(tt.text)
output := &strings.Builder{}
- status, err := EscapeControlReader(input, output, nullLocale{})
+ status, err := EscapeControlReader(input, output, &translation.MockLocale{})
result := output.String()
if err != nil {
t.Errorf("EscapeControlReader(): err = %v", err)
@@ -201,5 +193,5 @@ func TestEscapeControlReader_panic(t *testing.T) {
for i := 0; i < 6826; i++ {
bs = append(bs, []byte("—")...)
}
- _, _ = EscapeControlString(string(bs), nullLocale{})
+ _, _ = EscapeControlString(string(bs), &translation.MockLocale{})
}
diff --git a/modules/csv/csv_test.go b/modules/csv/csv_test.go
index 6b91a81fc479..f6e782a5a460 100644
--- a/modules/csv/csv_test.go
+++ b/modules/csv/csv_test.go
@@ -13,6 +13,7 @@ import (
"code.gitea.io/gitea/modules/git"
"code.gitea.io/gitea/modules/markup"
+ "code.gitea.io/gitea/modules/translation"
"github.com/stretchr/testify/assert"
)
@@ -550,20 +551,6 @@ a|"he said, ""here I am"""`,
}
}
-type mockLocale struct{}
-
-func (l mockLocale) Language() string {
- return "en"
-}
-
-func (l mockLocale) Tr(s string, _ ...interface{}) string {
- return s
-}
-
-func (l mockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface{}) string {
- return key1
-}
-
func TestFormatError(t *testing.T) {
cases := []struct {
err error
@@ -591,7 +578,7 @@ func TestFormatError(t *testing.T) {
}
for n, c := range cases {
- message, err := FormatError(c.err, mockLocale{})
+ message, err := FormatError(c.err, &translation.MockLocale{})
if c.expectsError {
assert.Error(t, err, "case %d: expected an error to be returned", n)
} else {
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index f93419fe873e..27d6000daf24 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -132,7 +132,6 @@ func NewFuncMap() []template.FuncMap {
// -----------------------------------------------------------------
// time / number / format
"FileSize": base.FileSize,
- "LocaleNumber": LocaleNumber,
"CountFmt": base.FormatNumberSI,
"TimeSince": timeutil.TimeSince,
"TimeSinceUnix": timeutil.TimeSinceUnix,
@@ -782,12 +781,6 @@ func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteNa
return a
}
-// LocaleNumber renders a number with a Custom Element, browser will render it with a locale number
-func LocaleNumber(v interface{}) template.HTML {
- num, _ := util.ToInt64(v)
- return template.HTML(fmt.Sprintf(`%d`, num, num))
-}
-
// Eval the expression and return the result, see the comment of eval.Expr for details.
// To use this helper function in templates, pass each token as a separate parameter.
//
diff --git a/modules/test/context_tests.go b/modules/test/context_tests.go
index 94dbd2f290d9..e558bf1b9f17 100644
--- a/modules/test/context_tests.go
+++ b/modules/test/context_tests.go
@@ -18,6 +18,7 @@ import (
user_model "code.gitea.io/gitea/models/user"
"code.gitea.io/gitea/modules/context"
"code.gitea.io/gitea/modules/git"
+ "code.gitea.io/gitea/modules/translation"
"code.gitea.io/gitea/modules/web/middleware"
chi "github.com/go-chi/chi/v5"
@@ -34,7 +35,7 @@ func MockContext(t *testing.T, path string) *context.Context {
Values: make(url.Values),
},
Resp: context.NewResponse(resp),
- Locale: &mockLocale{},
+ Locale: &translation.MockLocale{},
}
defer ctx.Close()
@@ -91,20 +92,6 @@ func LoadGitRepo(t *testing.T, ctx *context.Context) {
assert.NoError(t, err)
}
-type mockLocale struct{}
-
-func (l mockLocale) Language() string {
- return "en"
-}
-
-func (l mockLocale) Tr(s string, _ ...interface{}) string {
- return s
-}
-
-func (l mockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface{}) string {
- return key1
-}
-
type mockResponseWriter struct {
httptest.ResponseRecorder
size int
diff --git a/modules/translation/mock.go b/modules/translation/mock.go
new file mode 100644
index 000000000000..6ce66166aa13
--- /dev/null
+++ b/modules/translation/mock.go
@@ -0,0 +1,27 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package translation
+
+import "fmt"
+
+// MockLocale provides a mocked locale without any translations
+type MockLocale struct{}
+
+var _ Locale = (*MockLocale)(nil)
+
+func (l MockLocale) Language() string {
+ return "en"
+}
+
+func (l MockLocale) Tr(s string, _ ...interface{}) string {
+ return s
+}
+
+func (l MockLocale) TrN(_cnt interface{}, key1, _keyN string, _args ...interface{}) string {
+ return key1
+}
+
+func (l MockLocale) PrettyNumber(v any) string {
+ return fmt.Sprint(v)
+}
diff --git a/modules/translation/translation.go b/modules/translation/translation.go
index 331da0f965c3..56cf1df2d438 100644
--- a/modules/translation/translation.go
+++ b/modules/translation/translation.go
@@ -15,17 +15,20 @@ import (
"code.gitea.io/gitea/modules/translation/i18n"
"golang.org/x/text/language"
+ "golang.org/x/text/message"
+ "golang.org/x/text/number"
)
type contextKey struct{}
-var ContextKey interface{} = &contextKey{}
+var ContextKey any = &contextKey{}
// Locale represents an interface to translation
type Locale interface {
Language() string
- Tr(string, ...interface{}) string
- TrN(cnt interface{}, key1, keyN string, args ...interface{}) string
+ Tr(string, ...any) string
+ TrN(cnt any, key1, keyN string, args ...any) string
+ PrettyNumber(v any) string
}
// LangType represents a lang type
@@ -135,6 +138,7 @@ func Match(tags ...language.Tag) language.Tag {
type locale struct {
i18n.Locale
Lang, LangName string // these fields are used directly in templates: .i18n.Lang
+ msgPrinter *message.Printer
}
// NewLocale return a locale
@@ -147,13 +151,24 @@ func NewLocale(lang string) Locale {
langName := "unknown"
if l, ok := allLangMap[lang]; ok {
langName = l.Name
+ } else if len(setting.Langs) > 0 {
+ lang = setting.Langs[0]
+ langName = setting.Names[0]
}
+
i18nLocale, _ := i18n.GetLocale(lang)
- return &locale{
+ l := &locale{
Locale: i18nLocale,
Lang: lang,
LangName: langName,
}
+ if langTag, err := language.Parse(lang); err != nil {
+ log.Error("Failed to parse language tag from name %q: %v", l.Lang, err)
+ l.msgPrinter = message.NewPrinter(language.English)
+ } else {
+ l.msgPrinter = message.NewPrinter(langTag)
+ }
+ return l
}
func (l *locale) Language() string {
@@ -199,7 +214,7 @@ var trNLangRules = map[string]func(int64) int{
}
// TrN returns translated message for plural text translation
-func (l *locale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) string {
+func (l *locale) TrN(cnt any, key1, keyN string, args ...any) string {
var c int64
if t, ok := cnt.(int); ok {
c = int64(t)
@@ -223,3 +238,8 @@ func (l *locale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) st
}
return l.Tr(keyN, args...)
}
+
+func (l *locale) PrettyNumber(v any) string {
+ // TODO: this mechanism is not good enough, the complete solution is to switch the translation system to ICU message format
+ return l.msgPrinter.Sprintf("%v", number.Decimal(v))
+}
diff --git a/modules/translation/translation_test.go b/modules/translation/translation_test.go
new file mode 100644
index 000000000000..83a40f145815
--- /dev/null
+++ b/modules/translation/translation_test.go
@@ -0,0 +1,27 @@
+// Copyright 2023 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package translation
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/modules/translation/i18n"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestPrettyNumber(t *testing.T) {
+ // TODO: make this package friendly to testing
+
+ i18n.ResetDefaultLocales()
+
+ allLangMap = make(map[string]*LangType)
+ allLangMap["id-ID"] = &LangType{Lang: "id-ID", Name: "Bahasa Indonesia"}
+
+ l := NewLocale("id-ID")
+ assert.EqualValues(t, "1.000.000", l.PrettyNumber(1000000))
+
+ l = NewLocale("nosuch")
+ assert.EqualValues(t, "1,000,000", l.PrettyNumber(1000000))
+}
diff --git a/templates/devtest/gitea-ui.tmpl b/templates/devtest/gitea-ui.tmpl
index 1ab9ae7b7c7a..2fe478a07d8a 100644
--- a/templates/devtest/gitea-ui.tmpl
+++ b/templates/devtest/gitea-ui.tmpl
@@ -15,13 +15,13 @@
LocaleNumber
-
{{LocaleNumber 1}}
-
{{LocaleNumber 12}}
-
{{LocaleNumber 123}}
-
{{LocaleNumber 1234}}
-
{{LocaleNumber 12345}}
-
{{LocaleNumber 123456}}
-
{{LocaleNumber 1234567}}
+
{{.locale.PrettyNumber 1}}
+
{{.locale.PrettyNumber 12}}
+
{{.locale.PrettyNumber 123}}
+
{{.locale.PrettyNumber 1234}}
+
{{.locale.PrettyNumber 12345}}
+
{{.locale.PrettyNumber 123456}}
+
{{.locale.PrettyNumber 1234567}}
diff --git a/templates/projects/list.tmpl b/templates/projects/list.tmpl
index 213bab70b66e..ae9e3a0d1187 100644
--- a/templates/projects/list.tmpl
+++ b/templates/projects/list.tmpl
@@ -13,11 +13,11 @@
@@ -46,9 +46,9 @@
{{end}}
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
- {{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
+ {{$.locale.PrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
{{svg "octicon-check" 16 "gt-mr-3"}}
- {{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
+ {{$.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index 602461400b92..e54a72714a5c 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -18,11 +18,11 @@
@@ -84,9 +84,9 @@
{{end}}
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
- {{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
+ {{$.locale.PrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
{{svg "octicon-check" 16 "gt-mr-3"}}
- {{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
+ {{$.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
{{if .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}}
{{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix $.locale) | Safe}}{{end}}
diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl
index 6eb26b36c5d6..045f51397467 100644
--- a/templates/repo/issue/openclose.tmpl
+++ b/templates/repo/issue/openclose.tmpl
@@ -5,10 +5,10 @@
{{else}}
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
{{end}}
- {{LocaleNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
+ {{.locale.PrettyNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
{{svg "octicon-check" 16 "gt-mr-3"}}
- {{LocaleNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
+ {{.locale.PrettyNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
diff --git a/templates/repo/projects/list.tmpl b/templates/repo/projects/list.tmpl
index fb5bc4f48d91..bbcc20dd7cbd 100644
--- a/templates/repo/projects/list.tmpl
+++ b/templates/repo/projects/list.tmpl
@@ -15,11 +15,11 @@
@@ -48,9 +48,9 @@
{{end}}
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
- {{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
+ {{.locale.PrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
{{svg "octicon-check" 16 "gt-mr-3"}}
- {{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
+ {{.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl
index 12aaa0bd7172..8e1793a5bab7 100644
--- a/templates/repo/release/list.tmpl
+++ b/templates/repo/release/list.tmpl
@@ -161,9 +161,9 @@
{{.Size | FileSize}}
-
+
{{svg "octicon-info"}}
-
+
{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}
diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl
index 7a4e28cffa1b..ddedfd608690 100644
--- a/templates/repo/release/new.tmpl
+++ b/templates/repo/release/new.tmpl
@@ -71,9 +71,9 @@
{{.Size | FileSize}}
-
+
{{svg "octicon-info"}}
-
+
{{end}}
diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl
index 97fbabda412d..9289295b1d73 100644
--- a/templates/repo/sub_menu.tmpl
+++ b/templates/repo/sub_menu.tmpl
@@ -4,7 +4,7 @@
{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}}
{{svg "octicon-git-branch"}} {{.BranchesCount}} {{.locale.TrN .BranchesCount "repo.branch" "repo.branches"}}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index 6b9bfe4ebad4..5a207aac60e3 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -65,11 +65,11 @@
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 99151597217a..39eea2fc7517 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -39,11 +39,11 @@
@@ -104,9 +104,9 @@
{{end}}
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
- {{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
+ {{.locale.PrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
{{svg "octicon-check" 16 "gt-mr-3"}}
- {{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
+ {{.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
{{if .TotalTrackedTime}}
{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}
{{end}}
diff --git a/web_src/js/webcomponents/GiteaLocaleNumber.js b/web_src/js/webcomponents/GiteaLocaleNumber.js
deleted file mode 100644
index 613aa6735912..000000000000
--- a/web_src/js/webcomponents/GiteaLocaleNumber.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// Convert a number to a locale string by data-number attribute.
-// Or add a tooltip by data-number-in-tooltip attribute. JSON: {message: "count: %s", number: 123}
-window.customElements.define('gitea-locale-number', class extends HTMLElement {
- connectedCallback() {
- // ideally, the number locale formatting and plural processing should be done by backend with translation strings.
- // if we have complete backend locale support (eg: Golang "x/text" package), we can drop this component.
- const number = this.getAttribute('data-number');
- if (number) {
- this.attachShadow({mode: 'open'});
- this.shadowRoot.textContent = new Intl.NumberFormat().format(Number(number));
- }
- const numberInTooltip = this.getAttribute('data-number-in-tooltip');
- if (numberInTooltip) {
- // TODO: only 2 usages of this, we can replace it with Golang's "x/text/number" package in the future
- const {message, number} = JSON.parse(numberInTooltip);
- const tooltipContent = message.replace(/%[ds]/, new Intl.NumberFormat().format(Number(number)));
- this.setAttribute('data-tooltip-content', tooltipContent);
- }
- }
-});
diff --git a/web_src/js/webcomponents/webcomponents.js b/web_src/js/webcomponents/webcomponents.js
index 7e8135aa00cc..123607282bc7 100644
--- a/web_src/js/webcomponents/webcomponents.js
+++ b/web_src/js/webcomponents/webcomponents.js
@@ -1,4 +1,3 @@
import '@webcomponents/custom-elements'; // polyfill for some browsers like Pale Moon
import '@github/relative-time-element';
-import './GiteaLocaleNumber.js';
import './GiteaOriginUrl.js';