mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09: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>   </details>
This commit is contained in:
		| @@ -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'; | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user