Refactor locale number (#24134)

Before, the `GiteaLocaleNumber.js` was just written as a a drop-in
replacement for old `js-pretty-number`.

Actually, we can use Golang's `text` package to format.

This PR partially completes the TODOs in `GiteaLocaleNumber.js`:

> if we have complete backend locale support (eg: Golang "x/text"
package), we can drop this component.
> tooltip: only 2 usages of this, we can replace it with Golang's
"x/text/number" package in the future.

This PR also helps #24131

Screenshots:

<details>

![image](https://user-images.githubusercontent.com/2114189/232179420-b1b9974b-9d96-4408-b209-b80182c8b359.png)


![image](https://user-images.githubusercontent.com/2114189/232179416-14f36aa0-3f3e-4ac9-b366-7bd3a4464a11.png)

</details>
This commit is contained in:
wxiaoguang 2023-04-17 11:37:23 +08:00 committed by GitHub
parent be7cd73439
commit 7681d582cd
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 118 additions and 106 deletions

View file

@ -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{})
}

View file

@ -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 {

View file

@ -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(`<gitea-locale-number data-number="%d">%d</gitea-locale-number>`, 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.
//

View file

@ -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

View file

@ -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)
}

View file

@ -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))
}

View file

@ -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))
}