mirror of
				https://gitee.com/gitea/gitea
				synced 2025-11-04 08:30:25 +08:00 
			
		
		
		
	Introduce GiteaLocaleNumber custom element to handle number localization on pages. (#23861)
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:  
This commit is contained in:
		@@ -22,7 +22,6 @@ import (
 | 
				
			|||||||
	"code.gitea.io/gitea/modules/git"
 | 
						"code.gitea.io/gitea/modules/git"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/log"
 | 
						"code.gitea.io/gitea/modules/log"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/setting"
 | 
						"code.gitea.io/gitea/modules/setting"
 | 
				
			||||||
	"code.gitea.io/gitea/modules/util"
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	"github.com/dustin/go-humanize"
 | 
						"github.com/dustin/go-humanize"
 | 
				
			||||||
	"github.com/minio/sha256-simd"
 | 
						"github.com/minio/sha256-simd"
 | 
				
			||||||
@@ -142,12 +141,6 @@ func FileSize(s int64) string {
 | 
				
			|||||||
	return humanize.IBytes(uint64(s))
 | 
						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.
 | 
					// Subtract deals with subtraction of all types of number.
 | 
				
			||||||
func Subtract(left, right interface{}) interface{} {
 | 
					func Subtract(left, right interface{}) interface{} {
 | 
				
			||||||
	var rleft, rright int64
 | 
						var rleft, rright int64
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -114,13 +114,6 @@ func TestFileSize(t *testing.T) {
 | 
				
			|||||||
	assert.Equal(t, "2.0 EiB", FileSize(size))
 | 
						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) {
 | 
					func TestSubtract(t *testing.T) {
 | 
				
			||||||
	toFloat64 := func(n interface{}) float64 {
 | 
						toFloat64 := func(n interface{}) float64 {
 | 
				
			||||||
		switch v := n.(type) {
 | 
							switch v := n.(type) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,6 @@ import (
 | 
				
			|||||||
	"reflect"
 | 
						"reflect"
 | 
				
			||||||
	"regexp"
 | 
						"regexp"
 | 
				
			||||||
	"runtime"
 | 
						"runtime"
 | 
				
			||||||
	"strconv"
 | 
					 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
	texttmpl "text/template"
 | 
						texttmpl "text/template"
 | 
				
			||||||
	"time"
 | 
						"time"
 | 
				
			||||||
@@ -119,8 +118,7 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"TimeSince":     timeutil.TimeSince,
 | 
							"TimeSince":     timeutil.TimeSince,
 | 
				
			||||||
		"TimeSinceUnix": timeutil.TimeSinceUnix,
 | 
							"TimeSinceUnix": timeutil.TimeSinceUnix,
 | 
				
			||||||
		"FileSize":      base.FileSize,
 | 
							"FileSize":      base.FileSize,
 | 
				
			||||||
		"PrettyNumber":   base.PrettyNumber,
 | 
							"LocaleNumber":  LocaleNumber,
 | 
				
			||||||
		"JsPrettyNumber": JsPrettyNumber,
 | 
					 | 
				
			||||||
		"Subtract":      base.Subtract,
 | 
							"Subtract":      base.Subtract,
 | 
				
			||||||
		"EntryIcon":     base.EntryIcon,
 | 
							"EntryIcon":     base.EntryIcon,
 | 
				
			||||||
		"MigrationIcon": MigrationIcon,
 | 
							"MigrationIcon": MigrationIcon,
 | 
				
			||||||
@@ -410,62 +408,9 @@ func NewFuncMap() []template.FuncMap {
 | 
				
			|||||||
		"Join":        strings.Join,
 | 
							"Join":        strings.Join,
 | 
				
			||||||
		"QueryEscape": url.QueryEscape,
 | 
							"QueryEscape": url.QueryEscape,
 | 
				
			||||||
		"DotEscape":   DotEscape,
 | 
							"DotEscape":   DotEscape,
 | 
				
			||||||
		"Iterate": func(arg interface{}) (items []uint64) {
 | 
							"Iterate": func(arg interface{}) (items []int64) {
 | 
				
			||||||
			count := uint64(0)
 | 
								count := util.ToInt64(arg)
 | 
				
			||||||
			switch val := arg.(type) {
 | 
								for i := int64(0); i < count; i++ {
 | 
				
			||||||
			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++ {
 | 
					 | 
				
			||||||
				items = append(items, i)
 | 
									items = append(items, i)
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return items
 | 
								return items
 | 
				
			||||||
@@ -1067,10 +1012,8 @@ func mirrorRemoteAddress(ctx context.Context, m *repo_model.Repository, remoteNa
 | 
				
			|||||||
	return a
 | 
						return a
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// JsPrettyNumber renders a number using english decimal separators, e.g. 1,200 and subsequent
 | 
					// LocaleNumber renders a number with a Custom Element, browser will render it with a locale number
 | 
				
			||||||
// JS will replace the number with locale-specific separators, based on the user's selected language
 | 
					func LocaleNumber(v interface{}) template.HTML {
 | 
				
			||||||
func JsPrettyNumber(i interface{}) template.HTML {
 | 
						num := util.ToInt64(v)
 | 
				
			||||||
	num := util.NumberIntoInt64(i)
 | 
						return template.HTML(fmt.Sprintf(`<gitea-locale-number data-number="%d">%d</gitea-locale-number>`, num, num))
 | 
				
			||||||
 | 
					 | 
				
			||||||
	return template.HTML(`<span class="js-pretty-number" data-value="` + strconv.FormatInt(num, 10) + `">` + base.PrettyNumber(num) + `</span>`)
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,27 +35,3 @@ func SplitStringAtByteN(input string, n int) (left, right string) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	return input[:end] + utf8Ellipsis, utf8Ellipsis + input[end:]
 | 
						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", ""},
 | 
							{"\xef\x03", 1, "\xef\x03", ""},
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	test(tc, SplitStringAtByteN)
 | 
						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"
 | 
						"bytes"
 | 
				
			||||||
	"crypto/rand"
 | 
						"crypto/rand"
 | 
				
			||||||
	"errors"
 | 
						"errors"
 | 
				
			||||||
 | 
						"fmt"
 | 
				
			||||||
	"math/big"
 | 
						"math/big"
 | 
				
			||||||
	"regexp"
 | 
						"os"
 | 
				
			||||||
	"strconv"
 | 
						"strconv"
 | 
				
			||||||
	"strings"
 | 
						"strings"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -200,40 +201,14 @@ func ToTitleCaseNoLower(s string) string {
 | 
				
			|||||||
	return titleCaserNoLower.String(s)
 | 
						return titleCaserNoLower.String(s)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
var (
 | 
					func logError(msg string, args ...any) {
 | 
				
			||||||
	whitespaceOnly    = regexp.MustCompile("(?m)^[ \t]+$")
 | 
						// TODO: the "util" package can not import the "modules/log" package, so we use the "fmt" package here temporarily.
 | 
				
			||||||
	leadingWhitespace = regexp.MustCompile("(?m)(^[ \t]*)(?:[^ \t\n])")
 | 
						// In the future, we should decouple the dependency between them.
 | 
				
			||||||
)
 | 
						_, _ = fmt.Fprintf(os.Stderr, msg, args...)
 | 
				
			||||||
 | 
					 | 
				
			||||||
// 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 != "" {
 | 
					// ToInt64 transform a given int into int64.
 | 
				
			||||||
		s = regexp.MustCompile("(?m)^"+margin).ReplaceAllString(s, "")
 | 
					func ToInt64(number interface{}) int64 {
 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return strings.TrimSpace(s)
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// NumberIntoInt64 transform a given int into int64.
 | 
					 | 
				
			||||||
func NumberIntoInt64(number interface{}) int64 {
 | 
					 | 
				
			||||||
	var value int64
 | 
						var value int64
 | 
				
			||||||
	switch v := number.(type) {
 | 
						switch v := number.(type) {
 | 
				
			||||||
	case int:
 | 
						case int:
 | 
				
			||||||
@@ -246,6 +221,23 @@ func NumberIntoInt64(number interface{}) int64 {
 | 
				
			|||||||
		value = int64(v)
 | 
							value = int64(v)
 | 
				
			||||||
	case int64:
 | 
						case int64:
 | 
				
			||||||
		value = v
 | 
							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
 | 
						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`)
 | 
				
			||||||
	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">
 | 
							<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"}}
 | 
				
			||||||
				{{JsPrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
 | 
									{{LocaleNumber .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"}}
 | 
				
			||||||
				{{JsPrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
 | 
									{{LocaleNumber .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"}}
 | 
				
			||||||
							{{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
 | 
												{{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
 | 
				
			||||||
							{{svg "octicon-check" 16 "gt-mr-3"}}
 | 
												{{svg "octicon-check" 16 "gt-mr-3"}}
 | 
				
			||||||
							{{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
 | 
												{{LocaleNumber .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"}}
 | 
				
			||||||
						{{JsPrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
 | 
											{{LocaleNumber .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"}}
 | 
				
			||||||
						{{JsPrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
 | 
											{{LocaleNumber .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"}}
 | 
				
			||||||
							{{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
 | 
												{{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
 | 
				
			||||||
							{{svg "octicon-check" 16 "gt-mr-3"}}
 | 
												{{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 .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}}
 | 
				
			||||||
							{{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.update_ago" (.TimeSinceUpdate|Sec2Time)}}{{end}}
 | 
												{{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.locale.Tr "repo.milestones.update_ago" (.TimeSinceUpdate|Sec2Time)}}{{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}}
 | 
				
			||||||
		{{JsPrettyNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
 | 
							{{LocaleNumber .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"}}
 | 
				
			||||||
		{{JsPrettyNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
 | 
							{{LocaleNumber .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"}}
 | 
				
			||||||
				{{JsPrettyNumber .OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
 | 
									{{LocaleNumber .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"}}
 | 
				
			||||||
				{{JsPrettyNumber .ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
 | 
									{{LocaleNumber .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"}}
 | 
				
			||||||
							{{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
 | 
												{{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
 | 
				
			||||||
							{{svg "octicon-check" 16 "gt-mr-3"}}
 | 
												{{svg "octicon-check" 16 "gt-mr-3"}}
 | 
				
			||||||
							{{JsPrettyNumber .NumClosedIssues}} {{$.locale.Tr "repo.issues.closed_title"}}
 | 
												{{LocaleNumber .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>
 | 
				
			||||||
													<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"}}
 | 
																			{{svg "octicon-info"}}
 | 
				
			||||||
													</span>
 | 
																		</gitea-locale-number>
 | 
				
			||||||
												</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>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,9 +72,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>
 | 
				
			||||||
							<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"}}
 | 
													{{svg "octicon-info"}}
 | 
				
			||||||
							</span>
 | 
												</gitea-locale-number>
 | 
				
			||||||
						</div>
 | 
											</div>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
				{{end}}
 | 
									{{end}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
		<div class="ui two horizontal center link list">
 | 
							<div class="ui two horizontal center link 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>{{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>
 | 
				
			||||||
				<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"}}
 | 
				
			||||||
								{{JsPrettyNumber .IssueStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
 | 
													{{LocaleNumber .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"}}
 | 
				
			||||||
								{{JsPrettyNumber .IssueStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
 | 
													{{LocaleNumber .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"}}
 | 
				
			||||||
								{{JsPrettyNumber .MilestoneStats.OpenCount}} {{.locale.Tr "repo.issues.open_title"}}
 | 
													{{LocaleNumber .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"}}
 | 
				
			||||||
								{{JsPrettyNumber .MilestoneStats.ClosedCount}} {{.locale.Tr "repo.issues.closed_title"}}
 | 
													{{LocaleNumber .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"}}
 | 
				
			||||||
									{{JsPrettyNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
 | 
														{{LocaleNumber .NumOpenIssues}} {{$.locale.Tr "repo.issues.open_title"}}
 | 
				
			||||||
									{{svg "octicon-check" 16 "gt-mr-3"}}
 | 
														{{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}}
 | 
														{{if .TotalTrackedTime}}
 | 
				
			||||||
										{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}
 | 
															{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}
 | 
				
			||||||
									{{end}}
 | 
														{{end}}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,9 @@
 | 
				
			|||||||
import {prettyNumber} from '../utils.js';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const {lang} = document.documentElement;
 | 
					const {lang} = document.documentElement;
 | 
				
			||||||
const dateFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'long', day: 'numeric'});
 | 
					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 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'});
 | 
					const dateTimeFormatter = new Intl.DateTimeFormat(lang, {year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: 'numeric', second: 'numeric'});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function initFormattingReplacements() {
 | 
					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
 | 
					  // for each <time></time> tag, if it has the data-format attribute, format
 | 
				
			||||||
  // the text according to the user's chosen locale and formatter.
 | 
					  // the text according to the user's chosen locale and formatter.
 | 
				
			||||||
  formatAllTimeElements();
 | 
					  formatAllTimeElements();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -54,13 +54,6 @@ export function parseIssueHref(href) {
 | 
				
			|||||||
  return {owner, repo, type, index};
 | 
					  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'
 | 
					// parse a URL, either relative '/path' or absolute 'https://localhost/path'
 | 
				
			||||||
export function parseUrl(str) {
 | 
					export function parseUrl(str) {
 | 
				
			||||||
  return new URL(str, str.startsWith('http') ? undefined : window.location.origin);
 | 
					  return new URL(str, str.startsWith('http') ? undefined : window.location.origin);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
import {expect, test} from 'vitest';
 | 
					import {expect, test} from 'vitest';
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  basename, extname, isObject, stripTags, joinPaths, parseIssueHref,
 | 
					  basename, extname, isObject, stripTags, joinPaths, parseIssueHref,
 | 
				
			||||||
  prettyNumber, parseUrl, translateMonth, translateDay, blobToDataURI,
 | 
					  parseUrl, translateMonth, translateDay, blobToDataURI,
 | 
				
			||||||
  toAbsoluteUrl,
 | 
					  toAbsoluteUrl,
 | 
				
			||||||
} from './utils.js';
 | 
					} from './utils.js';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -84,17 +84,6 @@ test('parseIssueHref', () => {
 | 
				
			|||||||
  expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined});
 | 
					  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', () => {
 | 
					test('parseUrl', () => {
 | 
				
			||||||
  expect(parseUrl('').pathname).toEqual('/');
 | 
					  expect(parseUrl('').pathname).toEqual('/');
 | 
				
			||||||
  expect(parseUrl('/path').pathname).toEqual('/path');
 | 
					  expect(parseUrl('/path').pathname).toEqual('/path');
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										20
									
								
								web_src/js/webcomponents/GiteaLocaleNumber.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								web_src/js/webcomponents/GiteaLocaleNumber.js
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
					// Convert an absolute or relative URL to an absolute URL with the current origin
 | 
				
			||||||
 | 
					 | 
				
			||||||
// this is a Gitea's private HTML component, it converts an absolute or relative URL to an absolute URL with the current origin
 | 
					 | 
				
			||||||
window.customElements.define('gitea-origin-url', class extends HTMLElement {
 | 
					window.customElements.define('gitea-origin-url', class extends HTMLElement {
 | 
				
			||||||
  connectedCallback() {
 | 
					  connectedCallback() {
 | 
				
			||||||
    const urlStr = this.getAttribute('data-url');
 | 
					    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:
 | 
					There are still some components that are not migrated to web components yet:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
* `<span class="js-pretty-number">`
 | 
					 | 
				
			||||||
* `<time data-format>`
 | 
					* `<time data-format>`
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								web_src/js/webcomponents/webcomponents.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								web_src/js/webcomponents/webcomponents.js
									
									
									
									
									
										Normal file
									
								
							@@ -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)),
 | 
					      fileURLToPath(new URL('web_src/css/index.css', import.meta.url)),
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
    webcomponents: [
 | 
					    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: [
 | 
					    swagger: [
 | 
				
			||||||
      fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)),
 | 
					      fileURLToPath(new URL('web_src/js/standalone/swagger.js', import.meta.url)),
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user