mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-27 06:03:51 +03:00
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:
parent
be7cd73439
commit
7681d582cd
19 changed files with 118 additions and 106 deletions
|
@ -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) {
|
func TestEscapeControlString(t *testing.T) {
|
||||||
for _, tt := range escapeControlTests {
|
for _, tt := range escapeControlTests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
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) {
|
if !reflect.DeepEqual(*status, tt.status) {
|
||||||
t.Errorf("EscapeControlString() status = %v, wanted= %v", 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) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
input := strings.NewReader(tt.text)
|
input := strings.NewReader(tt.text)
|
||||||
output := &strings.Builder{}
|
output := &strings.Builder{}
|
||||||
status, err := EscapeControlReader(input, output, nullLocale{})
|
status, err := EscapeControlReader(input, output, &translation.MockLocale{})
|
||||||
result := output.String()
|
result := output.String()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Errorf("EscapeControlReader(): err = %v", err)
|
t.Errorf("EscapeControlReader(): err = %v", err)
|
||||||
|
@ -201,5 +193,5 @@ func TestEscapeControlReader_panic(t *testing.T) {
|
||||||
for i := 0; i < 6826; i++ {
|
for i := 0; i < 6826; i++ {
|
||||||
bs = append(bs, []byte("—")...)
|
bs = append(bs, []byte("—")...)
|
||||||
}
|
}
|
||||||
_, _ = EscapeControlString(string(bs), nullLocale{})
|
_, _ = EscapeControlString(string(bs), &translation.MockLocale{})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,6 +13,7 @@ import (
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/markup"
|
"code.gitea.io/gitea/modules/markup"
|
||||||
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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) {
|
func TestFormatError(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
err error
|
err error
|
||||||
|
@ -591,7 +578,7 @@ func TestFormatError(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for n, c := range cases {
|
for n, c := range cases {
|
||||||
message, err := FormatError(c.err, mockLocale{})
|
message, err := FormatError(c.err, &translation.MockLocale{})
|
||||||
if c.expectsError {
|
if c.expectsError {
|
||||||
assert.Error(t, err, "case %d: expected an error to be returned", n)
|
assert.Error(t, err, "case %d: expected an error to be returned", n)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -132,7 +132,6 @@ func NewFuncMap() []template.FuncMap {
|
||||||
// -----------------------------------------------------------------
|
// -----------------------------------------------------------------
|
||||||
// time / number / format
|
// time / number / format
|
||||||
"FileSize": base.FileSize,
|
"FileSize": base.FileSize,
|
||||||
"LocaleNumber": LocaleNumber,
|
|
||||||
"CountFmt": base.FormatNumberSI,
|
"CountFmt": base.FormatNumberSI,
|
||||||
"TimeSince": timeutil.TimeSince,
|
"TimeSince": timeutil.TimeSince,
|
||||||
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
"TimeSinceUnix": timeutil.TimeSinceUnix,
|
||||||
|
@ -782,12 +781,6 @@ func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteNa
|
||||||
return a
|
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.
|
// 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.
|
// To use this helper function in templates, pass each token as a separate parameter.
|
||||||
//
|
//
|
||||||
|
|
|
@ -18,6 +18,7 @@ import (
|
||||||
user_model "code.gitea.io/gitea/models/user"
|
user_model "code.gitea.io/gitea/models/user"
|
||||||
"code.gitea.io/gitea/modules/context"
|
"code.gitea.io/gitea/modules/context"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
|
"code.gitea.io/gitea/modules/translation"
|
||||||
"code.gitea.io/gitea/modules/web/middleware"
|
"code.gitea.io/gitea/modules/web/middleware"
|
||||||
|
|
||||||
chi "github.com/go-chi/chi/v5"
|
chi "github.com/go-chi/chi/v5"
|
||||||
|
@ -34,7 +35,7 @@ func MockContext(t *testing.T, path string) *context.Context {
|
||||||
Values: make(url.Values),
|
Values: make(url.Values),
|
||||||
},
|
},
|
||||||
Resp: context.NewResponse(resp),
|
Resp: context.NewResponse(resp),
|
||||||
Locale: &mockLocale{},
|
Locale: &translation.MockLocale{},
|
||||||
}
|
}
|
||||||
defer ctx.Close()
|
defer ctx.Close()
|
||||||
|
|
||||||
|
@ -91,20 +92,6 @@ func LoadGitRepo(t *testing.T, ctx *context.Context) {
|
||||||
assert.NoError(t, err)
|
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 {
|
type mockResponseWriter struct {
|
||||||
httptest.ResponseRecorder
|
httptest.ResponseRecorder
|
||||||
size int
|
size int
|
||||||
|
|
27
modules/translation/mock.go
Normal file
27
modules/translation/mock.go
Normal 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)
|
||||||
|
}
|
|
@ -15,17 +15,20 @@ import (
|
||||||
"code.gitea.io/gitea/modules/translation/i18n"
|
"code.gitea.io/gitea/modules/translation/i18n"
|
||||||
|
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
"golang.org/x/text/message"
|
||||||
|
"golang.org/x/text/number"
|
||||||
)
|
)
|
||||||
|
|
||||||
type contextKey struct{}
|
type contextKey struct{}
|
||||||
|
|
||||||
var ContextKey interface{} = &contextKey{}
|
var ContextKey any = &contextKey{}
|
||||||
|
|
||||||
// Locale represents an interface to translation
|
// Locale represents an interface to translation
|
||||||
type Locale interface {
|
type Locale interface {
|
||||||
Language() string
|
Language() string
|
||||||
Tr(string, ...interface{}) string
|
Tr(string, ...any) string
|
||||||
TrN(cnt interface{}, key1, keyN string, args ...interface{}) string
|
TrN(cnt any, key1, keyN string, args ...any) string
|
||||||
|
PrettyNumber(v any) string
|
||||||
}
|
}
|
||||||
|
|
||||||
// LangType represents a lang type
|
// LangType represents a lang type
|
||||||
|
@ -135,6 +138,7 @@ func Match(tags ...language.Tag) language.Tag {
|
||||||
type locale struct {
|
type locale struct {
|
||||||
i18n.Locale
|
i18n.Locale
|
||||||
Lang, LangName string // these fields are used directly in templates: .i18n.Lang
|
Lang, LangName string // these fields are used directly in templates: .i18n.Lang
|
||||||
|
msgPrinter *message.Printer
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLocale return a locale
|
// NewLocale return a locale
|
||||||
|
@ -147,13 +151,24 @@ func NewLocale(lang string) Locale {
|
||||||
langName := "unknown"
|
langName := "unknown"
|
||||||
if l, ok := allLangMap[lang]; ok {
|
if l, ok := allLangMap[lang]; ok {
|
||||||
langName = l.Name
|
langName = l.Name
|
||||||
|
} else if len(setting.Langs) > 0 {
|
||||||
|
lang = setting.Langs[0]
|
||||||
|
langName = setting.Names[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
i18nLocale, _ := i18n.GetLocale(lang)
|
i18nLocale, _ := i18n.GetLocale(lang)
|
||||||
return &locale{
|
l := &locale{
|
||||||
Locale: i18nLocale,
|
Locale: i18nLocale,
|
||||||
Lang: lang,
|
Lang: lang,
|
||||||
LangName: langName,
|
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 {
|
func (l *locale) Language() string {
|
||||||
|
@ -199,7 +214,7 @@ var trNLangRules = map[string]func(int64) int{
|
||||||
}
|
}
|
||||||
|
|
||||||
// TrN returns translated message for plural text translation
|
// 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
|
var c int64
|
||||||
if t, ok := cnt.(int); ok {
|
if t, ok := cnt.(int); ok {
|
||||||
c = int64(t)
|
c = int64(t)
|
||||||
|
@ -223,3 +238,8 @@ func (l *locale) TrN(cnt interface{}, key1, keyN string, args ...interface{}) st
|
||||||
}
|
}
|
||||||
return l.Tr(keyN, args...)
|
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))
|
||||||
|
}
|
||||||
|
|
27
modules/translation/translation_test.go
Normal file
27
modules/translation/translation_test.go
Normal 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))
|
||||||
|
}
|
|
@ -15,13 +15,13 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h1>LocaleNumber</h1>
|
<h1>LocaleNumber</h1>
|
||||||
<div>{{LocaleNumber 1}}</div>
|
<div>{{.locale.PrettyNumber 1}}</div>
|
||||||
<div>{{LocaleNumber 12}}</div>
|
<div>{{.locale.PrettyNumber 12}}</div>
|
||||||
<div>{{LocaleNumber 123}}</div>
|
<div>{{.locale.PrettyNumber 123}}</div>
|
||||||
<div>{{LocaleNumber 1234}}</div>
|
<div>{{.locale.PrettyNumber 1234}}</div>
|
||||||
<div>{{LocaleNumber 12345}}</div>
|
<div>{{.locale.PrettyNumber 12345}}</div>
|
||||||
<div>{{LocaleNumber 123456}}</div>
|
<div>{{.locale.PrettyNumber 123456}}</div>
|
||||||
<div>{{LocaleNumber 1234567}}</div>
|
<div>{{.locale.PrettyNumber 1234567}}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|
|
@ -13,11 +13,11 @@
|
||||||
<div class="ui compact tiny menu">
|
<div class="ui compact tiny menu">
|
||||||
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=open">
|
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=open">
|
||||||
{{svg "octicon-project-symlink" 16 "gt-mr-3"}}
|
{{svg "octicon-project-symlink" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
{{.locale.PrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
||||||
</a>
|
</a>
|
||||||
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=closed">
|
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=closed">
|
||||||
{{svg "octicon-check" 16 "gt-mr-3"}}
|
{{svg "octicon-check" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
{{.locale.PrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -46,9 +46,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="issue-stats">
|
<span class="issue-stats">
|
||||||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
{{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"}}
|
{{svg "octicon-check" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
|
{{$.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
|
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
|
||||||
|
|
|
@ -18,11 +18,11 @@
|
||||||
<div class="ui compact tiny menu">
|
<div class="ui compact tiny menu">
|
||||||
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/milestones?state=open&q={{$.Keyword}}">
|
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/milestones?state=open&q={{$.Keyword}}">
|
||||||
{{svg "octicon-milestone" 16 "gt-mr-3"}}
|
{{svg "octicon-milestone" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
{{.locale.PrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
||||||
</a>
|
</a>
|
||||||
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/milestones?state=closed&q={{$.Keyword}}">
|
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/milestones?state=closed&q={{$.Keyword}}">
|
||||||
{{svg "octicon-check" 16 "gt-mr-3"}}
|
{{svg "octicon-check" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
{{.locale.PrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,9 +84,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="issue-stats">
|
<span class="issue-stats">
|
||||||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
{{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"}}
|
{{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 .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}}
|
||||||
{{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix $.locale) | Safe}}{{end}}
|
{{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.update_ago" (TimeSinceUnix .UpdatedUnix $.locale) | Safe}}{{end}}
|
||||||
</span>
|
</span>
|
||||||
|
|
|
@ -5,10 +5,10 @@
|
||||||
{{else}}
|
{{else}}
|
||||||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
||||||
{{end}}
|
{{end}}
|
||||||
{{LocaleNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
{{.locale.PrettyNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
||||||
</a>
|
</a>
|
||||||
<a class="{{if .IsShowClosed}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&project={{.ProjectID}}&assignee={{.AssigneeID}}&poster={{.PosterID}}">
|
<a class="{{if .IsShowClosed}}active {{end}}item" href="{{$.Link}}?q={{$.Keyword}}&type={{.ViewType}}&sort={{$.SortType}}&state=closed&labels={{.SelectLabels}}&milestone={{.MilestoneID}}&project={{.ProjectID}}&assignee={{.AssigneeID}}&poster={{.PosterID}}">
|
||||||
{{svg "octicon-check" 16 "gt-mr-3"}}
|
{{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"}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -15,11 +15,11 @@
|
||||||
<div class="ui compact tiny menu">
|
<div class="ui compact tiny menu">
|
||||||
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/projects?state=open">
|
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/projects?state=open">
|
||||||
{{svg "octicon-project" 16 "gt-mr-3"}}
|
{{svg "octicon-project" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
{{.locale.PrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
||||||
</a>
|
</a>
|
||||||
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/projects?state=closed">
|
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/projects?state=closed">
|
||||||
{{svg "octicon-check" 16 "gt-mr-3"}}
|
{{svg "octicon-check" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
{{.locale.PrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -48,9 +48,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="issue-stats">
|
<span class="issue-stats">
|
||||||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
{{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"}}
|
{{svg "octicon-check" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
|
{{.locale.PrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
|
{{if and $.CanWriteProjects (not $.Repository.IsArchived)}}
|
||||||
|
|
|
@ -161,9 +161,9 @@
|
||||||
<li>
|
<li>
|
||||||
<span class="ui text middle aligned right">
|
<span class="ui text middle aligned right">
|
||||||
<span class="ui text grey">{{.Size | FileSize}}</span>
|
<span class="ui text grey">{{.Size | FileSize}}</span>
|
||||||
<gitea-locale-number data-number-in-tooltip="{{dict "message" ($.locale.Tr "repo.release.download_count") "number" .DownloadCount | Json}}">
|
<span data-tooltip-content="{{$.locale.Tr "repo.release.download_count" ($.locale.PrettyNumber .DownloadCount)}}">
|
||||||
{{svg "octicon-info"}}
|
{{svg "octicon-info"}}
|
||||||
</gitea-locale-number>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}">
|
<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}">
|
||||||
<strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong>
|
<strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong>
|
||||||
|
|
|
@ -71,9 +71,9 @@
|
||||||
<input name="attachment-edit-{{.UUID}}" class="gt-mr-3 attachment_edit" required value="{{.Name}}">
|
<input name="attachment-edit-{{.UUID}}" class="gt-mr-3 attachment_edit" required value="{{.Name}}">
|
||||||
<input name="attachment-del-{{.UUID}}" type="hidden" value="false">
|
<input name="attachment-del-{{.UUID}}" type="hidden" value="false">
|
||||||
<span class="ui text grey gt-mr-3">{{.Size | FileSize}}</span>
|
<span class="ui text grey gt-mr-3">{{.Size | FileSize}}</span>
|
||||||
<gitea-locale-number data-number-in-tooltip="{{dict "message" ($.locale.Tr "repo.release.download_count") "number" .DownloadCount | Json}}">
|
<span data-tooltip-content="{{$.locale.Tr "repo.release.download_count" ($.locale.PrettyNumber .DownloadCount)}}">
|
||||||
{{svg "octicon-info"}}
|
{{svg "octicon-info"}}
|
||||||
</gitea-locale-number>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<div class="ui two horizontal center list">
|
<div class="ui two horizontal center list">
|
||||||
{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}}
|
{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}}
|
||||||
<div class="item{{if .PageIsCommits}} active{{end}}">
|
<div class="item{{if .PageIsCommits}} active{{end}}">
|
||||||
<a href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}">{{svg "octicon-history"}} <b>{{LocaleNumber .CommitsCount}}</b> {{.locale.TrN .CommitsCount "repo.commit" "repo.commits"}}</a>
|
<a href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}">{{svg "octicon-history"}} <b>{{.locale.PrettyNumber .CommitsCount}}</b> {{.locale.TrN .CommitsCount "repo.commit" "repo.commits"}}</a>
|
||||||
</div>
|
</div>
|
||||||
<div class="item{{if .PageIsBranches}} active{{end}}">
|
<div class="item{{if .PageIsBranches}} active{{end}}">
|
||||||
<a href="{{.RepoLink}}/branches">{{svg "octicon-git-branch"}} <b>{{.BranchesCount}}</b> {{.locale.TrN .BranchesCount "repo.branch" "repo.branches"}}</a>
|
<a href="{{.RepoLink}}/branches">{{svg "octicon-git-branch"}} <b>{{.BranchesCount}}</b> {{.locale.TrN .BranchesCount "repo.branch" "repo.branches"}}</a>
|
||||||
|
|
|
@ -65,11 +65,11 @@
|
||||||
<div class="ui compact tiny menu">
|
<div class="ui compact tiny menu">
|
||||||
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
|
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
|
||||||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
{{.locale.PrettyNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
||||||
</a>
|
</a>
|
||||||
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
|
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?type={{$.ViewType}}&repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
|
||||||
{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
|
{{svg "octicon-issue-closed" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
{{.locale.PrettyNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -39,11 +39,11 @@
|
||||||
<div class="ui compact tiny menu">
|
<div class="ui compact tiny menu">
|
||||||
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
|
<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=open&q={{$.Keyword}}">
|
||||||
{{svg "octicon-milestone" 16 "gt-mr-3"}}
|
{{svg "octicon-milestone" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .MilestoneStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
{{.locale.PrettyNumber .MilestoneStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
|
||||||
</a>
|
</a>
|
||||||
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
|
<a class="item{{if .IsShowClosed}} active{{end}}" href="{{.Link}}?repos=[{{range $.RepoIDs}}{{.}}%2C{{end}}]&sort={{$.SortType}}&state=closed&q={{$.Keyword}}">
|
||||||
{{svg "octicon-check" 16 "gt-mr-3"}}
|
{{svg "octicon-check" 16 "gt-mr-3"}}
|
||||||
{{LocaleNumber .MilestoneStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
{{.locale.PrettyNumber .MilestoneStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -104,9 +104,9 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
<span class="issue-stats">
|
<span class="issue-stats">
|
||||||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}}
|
{{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"}}
|
{{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}}
|
{{if .TotalTrackedTime}}
|
||||||
{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}
|
{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -1,4 +1,3 @@
|
||||||
import '@webcomponents/custom-elements'; // polyfill for some browsers like Pale Moon
|
import '@webcomponents/custom-elements'; // polyfill for some browsers like Pale Moon
|
||||||
import '@github/relative-time-element';
|
import '@github/relative-time-element';
|
||||||
import './GiteaLocaleNumber.js';
|
|
||||||
import './GiteaOriginUrl.js';
|
import './GiteaOriginUrl.js';
|
||||||
|
|
Loading…
Reference in a new issue