mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 11:28:24 +00: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) { | ||||
| 	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{}) | ||||
| } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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. | ||||
| // | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
							
								
								
									
										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" | ||||
|  | ||||
| 	"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)) | ||||
| } | ||||
|   | ||||
							
								
								
									
										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> | ||||
| 		<h1>LocaleNumber</h1> | ||||
| 		<div>{{LocaleNumber 1}}</div> | ||||
| 		<div>{{LocaleNumber 12}}</div> | ||||
| 		<div>{{LocaleNumber 123}}</div> | ||||
| 		<div>{{LocaleNumber 1234}}</div> | ||||
| 		<div>{{LocaleNumber 12345}}</div> | ||||
| 		<div>{{LocaleNumber 123456}}</div> | ||||
| 		<div>{{LocaleNumber 1234567}}</div> | ||||
| 		<div>{{.locale.PrettyNumber 1}}</div> | ||||
| 		<div>{{.locale.PrettyNumber 12}}</div> | ||||
| 		<div>{{.locale.PrettyNumber 123}}</div> | ||||
| 		<div>{{.locale.PrettyNumber 1234}}</div> | ||||
| 		<div>{{.locale.PrettyNumber 12345}}</div> | ||||
| 		<div>{{.locale.PrettyNumber 123456}}</div> | ||||
| 		<div>{{.locale.PrettyNumber 1234567}}</div> | ||||
| 	</div> | ||||
|  | ||||
| 	<div> | ||||
|   | ||||
| @@ -13,11 +13,11 @@ | ||||
| 		<div class="ui compact tiny menu"> | ||||
| 			<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=open"> | ||||
| 				{{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 class="item{{if .IsShowClosed}} active{{end}}" href="{{$.Link}}?state=closed"> | ||||
| 				{{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> | ||||
| 		</div> | ||||
|  | ||||
| @@ -46,9 +46,9 @@ | ||||
| 						{{end}} | ||||
| 						<span class="issue-stats"> | ||||
| 							{{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"}} | ||||
| 						</span> | ||||
| 					</div> | ||||
| 					{{if and $.CanWriteProjects (not $.Repository.IsArchived)}} | ||||
|   | ||||
| @@ -18,11 +18,11 @@ | ||||
| 				<div class="ui compact tiny menu"> | ||||
| 					<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/milestones?state=open&q={{$.Keyword}}"> | ||||
| 						{{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 class="item{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/milestones?state=closed&q={{$.Keyword}}"> | ||||
| 						{{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> | ||||
| 				</div> | ||||
| 			</div> | ||||
| @@ -84,9 +84,9 @@ | ||||
| 						{{end}} | ||||
| 						<span class="issue-stats"> | ||||
| 							{{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}} | ||||
| 						</span> | ||||
|   | ||||
| @@ -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"}} | ||||
| 	</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}}"> | ||||
| 		{{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> | ||||
| </div> | ||||
|   | ||||
| @@ -15,11 +15,11 @@ | ||||
| 		<div class="ui compact tiny menu"> | ||||
| 			<a class="item{{if not .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/projects?state=open"> | ||||
| 				{{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 class="item{{if .IsShowClosed}} active{{end}}" href="{{.RepoLink}}/projects?state=closed"> | ||||
| 				{{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> | ||||
| 		</div> | ||||
|  | ||||
| @@ -48,9 +48,9 @@ | ||||
| 						{{end}} | ||||
| 						<span class="issue-stats"> | ||||
| 							{{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"}} | ||||
| 						</span> | ||||
| 					</div> | ||||
| 					{{if and $.CanWriteProjects (not $.Repository.IsArchived)}} | ||||
|   | ||||
| @@ -161,9 +161,9 @@ | ||||
| 											<li> | ||||
| 												<span class="ui text middle aligned right"> | ||||
| 													<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"}} | ||||
| 													</gitea-locale-number> | ||||
| 													</span> | ||||
| 												</span> | ||||
| 												<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}"> | ||||
| 													<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-del-{{.UUID}}" type="hidden" value="false"> | ||||
| 							<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"}} | ||||
| 							</gitea-locale-number> | ||||
| 							</span> | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				{{end}} | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| 		<div class="ui two horizontal center list"> | ||||
| 			{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}} | ||||
| 				<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 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> | ||||
|   | ||||
| @@ -65,11 +65,11 @@ | ||||
| 						<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}}"> | ||||
| 								{{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 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"}} | ||||
| 								{{LocaleNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | ||||
| 								{{.locale.PrettyNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | ||||
| 							</a> | ||||
| 						</div> | ||||
| 					</div> | ||||
|   | ||||
| @@ -39,11 +39,11 @@ | ||||
| 						<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}}"> | ||||
| 								{{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 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"}} | ||||
| 								{{LocaleNumber .MilestoneStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | ||||
| 								{{.locale.PrettyNumber .MilestoneStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | ||||
| 							</a> | ||||
| 						</div> | ||||
| 					</div> | ||||
| @@ -104,9 +104,9 @@ | ||||
| 								{{end}} | ||||
| 								<span class="issue-stats"> | ||||
| 									{{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}} | ||||
|   | ||||
| @@ -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 '@github/relative-time-element'; | ||||
| import './GiteaLocaleNumber.js'; | ||||
| import './GiteaOriginUrl.js'; | ||||
|   | ||||
		Reference in New Issue
	
	Block a user