Follow #21429 & #22861 Use `<gitea-locale-number>` instead of backend `PrettyNumber`. All old `PrettyNumber` related functions are removed. A lot of code could be simplified. And some functions haven't been used for long time (dead code), so they are also removed by the way (eg: `SplitStringAtRuneN`, `Dedent`) This PR only tries to improve the `PrettyNumber` rendering problem, it doesn't touch the "plural" problem. Screenshot: ![image](https://user-images.githubusercontent.com/2114189/229290804-1f63db65-1e34-4a54-84ba-e00b44331b17.png) ![image](https://user-images.githubusercontent.com/2114189/229290911-c88dea00-b11d-48dd-accb-9f52edd73ce4.png)tags/v1.20.0-rc0
@@ -22,7 +22,6 @@ import ( | |||
"code.gitea.io/gitea/modules/git" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/modules/util" | |||
"github.com/dustin/go-humanize" | |||
"github.com/minio/sha256-simd" | |||
@@ -142,12 +141,6 @@ func FileSize(s int64) string { | |||
return humanize.IBytes(uint64(s)) | |||
} | |||
// PrettyNumber produces a string form of the given number in base 10 with | |||
// commas after every three orders of magnitude | |||
func PrettyNumber(i interface{}) string { | |||
return humanize.Comma(util.NumberIntoInt64(i)) | |||
} | |||
// Subtract deals with subtraction of all types of number. | |||
func Subtract(left, right interface{}) interface{} { | |||
var rleft, rright int64 |
@@ -114,13 +114,6 @@ func TestFileSize(t *testing.T) { | |||
assert.Equal(t, "2.0 EiB", FileSize(size)) | |||
} | |||
func TestPrettyNumber(t *testing.T) { | |||
assert.Equal(t, "23,342,432", PrettyNumber(23342432)) | |||
assert.Equal(t, "23,342,432", PrettyNumber(int32(23342432))) | |||
assert.Equal(t, "0", PrettyNumber(0)) | |||
assert.Equal(t, "-100,000", PrettyNumber(-100000)) | |||
} | |||
func TestSubtract(t *testing.T) { | |||
toFloat64 := func(n interface{}) float64 { | |||
switch v := n.(type) { |
@@ -19,7 +19,6 @@ import ( | |||
"reflect" | |||
"regexp" | |||
"runtime" | |||
"strconv" | |||
"strings" | |||
texttmpl "text/template" | |||
"time" | |||
@@ -112,18 +111,17 @@ func NewFuncMap() []template.FuncMap { | |||
"IsShowFullName": func() bool { | |||
return setting.UI.DefaultShowFullName | |||
}, | |||
"Safe": Safe, | |||
"SafeJS": SafeJS, | |||
"JSEscape": JSEscape, | |||
"Str2html": Str2html, | |||
"TimeSince": timeutil.TimeSince, | |||
"TimeSinceUnix": timeutil.TimeSinceUnix, | |||
"FileSize": base.FileSize, | |||
"PrettyNumber": base.PrettyNumber, | |||
"JsPrettyNumber": JsPrettyNumber, | |||
"Subtract": base.Subtract, | |||
"EntryIcon": base.EntryIcon, | |||
"MigrationIcon": MigrationIcon, | |||
"Safe": Safe, | |||
"SafeJS": SafeJS, | |||
"JSEscape": JSEscape, | |||
"Str2html": Str2html, | |||
"TimeSince": timeutil.TimeSince, | |||
"TimeSinceUnix": timeutil.TimeSinceUnix, | |||
"FileSize": base.FileSize, | |||
"LocaleNumber": LocaleNumber, | |||
"Subtract": base.Subtract, | |||
"EntryIcon": base.EntryIcon, | |||
"MigrationIcon": MigrationIcon, | |||
"Add": func(a ...int) int { | |||
sum := 0 | |||
for _, val := range a { | |||
@@ -410,62 +408,9 @@ func NewFuncMap() []template.FuncMap { | |||
"Join": strings.Join, | |||
"QueryEscape": url.QueryEscape, | |||
"DotEscape": DotEscape, | |||
"Iterate": func(arg interface{}) (items []uint64) { | |||
count := uint64(0) | |||
switch val := arg.(type) { | |||
case uint64: | |||
count = val | |||
case *uint64: | |||
count = *val | |||
case int64: | |||
if val < 0 { | |||
val = 0 | |||
} | |||
count = uint64(val) | |||
case *int64: | |||
if *val < 0 { | |||
*val = 0 | |||
} | |||
count = uint64(*val) | |||
case int: | |||
if val < 0 { | |||
val = 0 | |||
} | |||
count = uint64(val) | |||
case *int: | |||
if *val < 0 { | |||
*val = 0 | |||
} | |||
count = uint64(*val) | |||
case uint: | |||
count = uint64(val) | |||
case *uint: | |||
count = uint64(*val) | |||
case int32: | |||
if val < 0 { | |||
val = 0 | |||
} | |||
count = uint64(val) | |||
case *int32: | |||
if *val < 0 { | |||
*val = 0 | |||
} | |||
count = uint64(*val) | |||
case uint32: | |||
count = uint64(val) | |||
case *uint32: | |||
count = uint64(*val) | |||
case string: | |||
cnt, _ := strconv.ParseInt(val, 10, 64) | |||
if cnt < 0 { | |||
cnt = 0 | |||
} | |||
count = uint64(cnt) | |||
} | |||
if count <= 0 { | |||
return items | |||
} | |||
for i := uint64(0); i < count; i++ { | |||
"Iterate": func(arg interface{}) (items []int64) { | |||
count := util.ToInt64(arg) | |||
for i := int64(0); i < count; i++ { | |||
items = append(items, i) | |||
} | |||
return items | |||
@@ -1067,10 +1012,8 @@ func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteNa | |||
return a | |||
} | |||
// JsPrettyNumber renders a number using english decimal separators, e.g. 1,200 and subsequent | |||
// JS will replace the number with locale-specific separators, based on the user's selected language | |||
func JsPrettyNumber(i interface{}) template.HTML { | |||
num := util.NumberIntoInt64(i) | |||
return template.HTML(`<span class="js-pretty-number" data-value="` + strconv.FormatInt(num, 10) + `">` + base.PrettyNumber(num) + `</span>`) | |||
// 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)) | |||
} |
@@ -35,27 +35,3 @@ func SplitStringAtByteN(input string, n int) (left, right string) { | |||
return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:] | |||
} | |||
// SplitStringAtRuneN splits a string at rune n accounting for rune boundaries. (Combining characters are not accounted for.) | |||
func SplitStringAtRuneN(input string, n int) (left, right string) { | |||
if !utf8.ValidString(input) { | |||
if len(input) <= n || n-3 < 0 { | |||
return input, "" | |||
} | |||
return input[:n-3] + asciiEllipsis, asciiEllipsis + input[n-3:] | |||
} | |||
if utf8.RuneCountInString(input) <= n { | |||
return input, "" | |||
} | |||
count := 0 | |||
end := 0 | |||
for count < n-1 { | |||
_, size := utf8.DecodeRuneInString(input[end:]) | |||
end += size | |||
count++ | |||
} | |||
return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:] | |||
} |
@@ -43,18 +43,4 @@ func TestSplitString(t *testing.T) { | |||
{"\xef\x03", 1, "\xef\x03", ""}, | |||
} | |||
test(tc, SplitStringAtByteN) | |||
tc = []*testCase{ | |||
{"abc123xyz", 0, "", utf8Ellipsis}, | |||
{"abc123xyz", 1, "", utf8Ellipsis}, | |||
{"abc123xyz", 4, "abc", utf8Ellipsis}, | |||
{"啊bc123xyz", 4, "啊bc", utf8Ellipsis}, | |||
{"啊bc123xyz", 6, "啊bc12", utf8Ellipsis}, | |||
{"啊bc", 3, "啊bc", ""}, | |||
{"啊bc", 4, "啊bc", ""}, | |||
{"abc\xef\x03\xfe", 3, "", asciiEllipsis}, | |||
{"abc\xef\x03\xfe", 4, "a", asciiEllipsis}, | |||
{"\xef\x03", 1, "\xef\x03", ""}, | |||
} | |||
test(tc, SplitStringAtRuneN) | |||
} |
@@ -7,8 +7,9 @@ import ( | |||
"bytes" | |||
"crypto/rand" | |||
"errors" | |||
"fmt" | |||
"math/big" | |||
"regexp" | |||
"os" | |||
"strconv" | |||
"strings" | |||
@@ -200,40 +201,14 @@ func ToTitleCaseNoLower(s string) string { | |||
return titleCaserNoLower.String(s) | |||
} | |||
var ( | |||
whitespaceOnly = regexp.MustCompile("(?m)^[ \t]+$") | |||
leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])") | |||
) | |||
// Dedent removes common indentation of a multi-line string along with whitespace around it | |||
// Based on https://github.com/lithammer/dedent | |||
func Dedent(s string) string { | |||
var margin string | |||
s = whitespaceOnly.ReplaceAllString(s, "") | |||
indents := leadingWhitespace.FindAllStringSubmatch(s, -1) | |||
for i, indent := range indents { | |||
if i == 0 { | |||
margin = indent[1] | |||
} else if strings.HasPrefix(indent[1], margin) { | |||
continue | |||
} else if strings.HasPrefix(margin, indent[1]) { | |||
margin = indent[1] | |||
} else { | |||
margin = "" | |||
break | |||
} | |||
} | |||
if margin != "" { | |||
s = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(s, "") | |||
} | |||
return strings.TrimSpace(s) | |||
func logError(msg string, args ...any) { | |||
// TODO: the "util" package can not import the "modules/log" package, so we use the "fmt" package here temporarily. | |||
// In the future, we should decouple the dependency between them. | |||
_, _ = fmt.Fprintf(os.Stderr, msg, args...) | |||
} | |||
// NumberIntoInt64 transform a given int into int64. | |||
func NumberIntoInt64(number interface{}) int64 { | |||
// ToInt64 transform a given int into int64. | |||
func ToInt64(number interface{}) int64 { | |||
var value int64 | |||
switch v := number.(type) { | |||
case int: | |||
@@ -246,6 +221,23 @@ func NumberIntoInt64(number interface{}) int64 { | |||
value = int64(v) | |||
case int64: | |||
value = v | |||
case uint: | |||
value = int64(v) | |||
case uint8: | |||
value = int64(v) | |||
case uint16: | |||
value = int64(v) | |||
case uint32: | |||
value = int64(v) | |||
case uint64: | |||
value = int64(v) | |||
case string: | |||
var err error | |||
if value, err = strconv.ParseInt(v, 10, 64); err != nil { | |||
logError("strconv.ParseInt failed for %q: %v", v, err) | |||
} | |||
default: | |||
logError("unable to convert %q to int64", v) | |||
} | |||
return value | |||
} |
@@ -224,10 +224,3 @@ func TestToTitleCase(t *testing.T) { | |||
assert.Equal(t, ToTitleCase(`foo bar baz`), `Foo Bar Baz`) | |||
assert.Equal(t, ToTitleCase(`FOO BAR BAZ`), `Foo Bar Baz`) | |||
} | |||
func TestDedent(t *testing.T) { | |||
assert.Equal(t, Dedent(` | |||
foo | |||
bar | |||
`), "foo\n\tbar") | |||
} |
@@ -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"}} | |||
{{JsPrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} | |||
{{svg "octicon-check" 16 "gt-mr-3"}} | |||
{{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} | |||
{{svg "octicon-check" 16 "gt-mr-3"}} | |||
{{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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" (.TimeSinceUpdate|Sec2Time)}}{{end}} | |||
</span> |
@@ -5,10 +5,10 @@ | |||
{{else}} | |||
{{svg "octicon-issue-opened" 16 "gt-mr-3"}} | |||
{{end}} | |||
{{JsPrettyNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} | |||
{{svg "octicon-check" 16 "gt-mr-3"}} | |||
{{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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> | |||
<span data-tooltip-content="{{$.locale.Tr "repo.release.download_count" (.DownloadCount | PrettyNumber)}}"> | |||
<gitea-locale-number data-number-in-tooltip="{{dict "message" ($.locale.Tr "repo.release.download_count") "number" .DownloadCount | Json}}"> | |||
{{svg "octicon-info"}} | |||
</span> | |||
</gitea-locale-number> | |||
</span> | |||
<a target="_blank" rel="noopener noreferrer" href="{{.DownloadURL}}"> | |||
<strong>{{svg "octicon-package" 16 "gt-mr-2"}}{{.Name}}</strong> |
@@ -72,9 +72,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> | |||
<span data-tooltip-content="{{$.locale.Tr "repo.release.download_count" (.DownloadCount | PrettyNumber)}}"> | |||
<gitea-locale-number data-number-in-tooltip="{{dict "message" ($.locale.Tr "repo.release.download_count") "number" .DownloadCount | Json}}"> | |||
{{svg "octicon-info"}} | |||
</span> | |||
</gitea-locale-number> | |||
</div> | |||
</div> | |||
{{end}} |
@@ -4,7 +4,7 @@ | |||
<div class="ui two horizontal center link list"> | |||
{{if and (.Permission.CanRead $.UnitTypeCode) (not .IsEmptyRepo)}} | |||
<div class="item{{if .PageIsCommits}} active{{end}}"> | |||
<a href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}">{{svg "octicon-history"}} <b>{{JsPrettyNumber .CommitsCount}}</b> {{.locale.TrN .CommitsCount "repo.commit" "repo.commits"}}</a> | |||
<a href="{{.RepoLink}}/commits/{{.BranchNameSubURL}}">{{svg "octicon-history"}} <b>{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .MilestoneStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .MilestoneStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .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"}} | |||
{{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} | |||
{{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}} | |||
{{svg "octicon-check" 16 "gt-mr-3"}} | |||
{{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} | |||
{{LocaleNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}} | |||
{{if .TotalTrackedTime}} | |||
{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}} | |||
{{end}} |
@@ -1,20 +1,9 @@ | |||
import {prettyNumber} from '../utils.js'; | |||
const {lang} = document.documentElement; | |||
const dateFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'long', day: 'numeric'}); | |||
const shortDateFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'short', day: 'numeric'}); | |||
const dateTimeFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'}); | |||
export function initFormattingReplacements() { | |||
// replace english formatted numbers with locale-specific separators | |||
for (const el of document.getElementsByClassName('js-pretty-number')) { | |||
const num = Number(el.getAttribute('data-value')); | |||
const formatted = prettyNumber(num, lang); | |||
if (formatted && formatted !== el.textContent) { | |||
el.textContent = formatted; | |||
} | |||
} | |||
// for each <time></time> tag, if it has the data-format attribute, format | |||
// the text according to the user's chosen locale and formatter. | |||
formatAllTimeElements(); |
@@ -54,13 +54,6 @@ export function parseIssueHref(href) { | |||
return {owner, repo, type, index}; | |||
} | |||
// pretty-print a number using locale-specific separators, e.g. 1200 -> 1,200 | |||
export function prettyNumber(num, locale = 'en-US') { | |||
if (typeof num !== 'number') return ''; | |||
const {format} = new Intl.NumberFormat(locale); | |||
return format(num); | |||
} | |||
// parse a URL, either relative '/path' or absolute 'https://localhost/path' | |||
export function parseUrl(str) { | |||
return new URL(str, str.startsWith('http') ? undefined : window.location.origin); |
@@ -1,7 +1,7 @@ | |||
import {expect, test} from 'vitest'; | |||
import { | |||
basename, extname, isObject, stripTags, joinPaths, parseIssueHref, | |||
prettyNumber, parseUrl, translateMonth, translateDay, blobToDataURI, | |||
parseUrl, translateMonth, translateDay, blobToDataURI, | |||
toAbsoluteUrl, | |||
} from './utils.js'; | |||
@@ -84,17 +84,6 @@ test('parseIssueHref', () => { | |||
expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined}); | |||
}); | |||
test('prettyNumber', () => { | |||
expect(prettyNumber()).toEqual(''); | |||
expect(prettyNumber(null)).toEqual(''); | |||
expect(prettyNumber(undefined)).toEqual(''); | |||
expect(prettyNumber('1200')).toEqual(''); | |||
expect(prettyNumber(12345678, 'en-US')).toEqual('12,345,678'); | |||
expect(prettyNumber(12345678, 'de-DE')).toEqual('12.345.678'); | |||
expect(prettyNumber(12345678, 'be-BE')).toEqual('12 345 678'); | |||
expect(prettyNumber(12345678, 'hi-IN')).toEqual('1,23,45,678'); | |||
}); | |||
test('parseUrl', () => { | |||
expect(parseUrl('').pathname).toEqual('/'); | |||
expect(parseUrl('/path').pathname).toEqual('/path'); |
@@ -0,0 +1,20 @@ | |||
// 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,6 +1,4 @@ | |||
import '@webcomponents/custom-elements'; // automatically adds custom elements for older browsers that don't support it | |||
// this is a Gitea's private HTML component, it converts an absolute or relative URL to an absolute URL with the current origin | |||
// Convert an absolute or relative URL to an absolute URL with the current origin | |||
window.customElements.define('gitea-origin-url', class extends HTMLElement { | |||
connectedCallback() { | |||
const urlStr = this.getAttribute('data-url'); |
@@ -15,5 +15,4 @@ https://developer.mozilla.org/en-US/docs/Web/Web_Components | |||
There are still some components that are not migrated to web components yet: | |||
* `<span class="js-pretty-number">` | |||
* `<time data-format>` |
@@ -0,0 +1,3 @@ | |||
import '@webcomponents/custom-elements'; // polyfill for some browsers like Pale Moon | |||
import './GiteaLocaleNumber.js'; | |||
import './GiteaOriginUrl.js'; |
@@ -60,7 +60,7 @@ export default { | |||
fileURLToPath(new URL('web_src/css/index.css', import.meta.url)), | |||
], | |||
webcomponents: [ | |||
fileURLToPath(new URL('web_src/js/webcomponents/GiteaOriginUrl.js', import.meta.url)), | |||
fileURLToPath(new URL('web_src/js/webcomponents/webcomponents.js', import.meta.url)), | |||
], | |||
swagger: [ | |||
fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)), |