diff --git a/modules/charset/escape_test.go b/modules/charset/escape_test.go index 26e82bf13a..f63c5c5c52 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 6b91a81fc4..f6e782a5a4 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 f93419fe87..27d6000daf 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 94dbd2f290..e558bf1b9f 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 0000000000..6ce66166aa --- /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 331da0f965..56cf1df2d4 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 0000000000..83a40f1458 --- /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 1ab9ae7b7c..2fe478a07d 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 213bab70b6..ae9e3a0d11 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 602461400b..e54a72714a 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 6eb26b36c5..045f513974 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 fb5bc4f48d..bbcc20dd7c 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 12aaa0bd71..8e1793a5ba 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 7a4e28cffa..ddedfd6086 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 97fbabda41..9289295b1d 100644 --- a/templates/repo/sub_menu.tmpl +++ b/templates/repo/sub_menu.tmpl @@ -4,7 +4,7 @@
    {{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}} diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl index 9915159721..39eea2fc75 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 613aa67359..0000000000 --- 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 7e8135aa00..123607282b 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';