diff --git a/modules/base/tool.go b/modules/base/tool.go
index 47ce125853fdf..a981fd6c57dc9 100644
--- a/modules/base/tool.go
+++ b/modules/base/tool.go
@@ -24,6 +24,7 @@ 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"
)
@@ -143,9 +144,9 @@ func FileSize(s int64) string {
}
// PrettyNumber produces a string form of the given number in base 10 with
-// commas after every three orders of magnitud
-func PrettyNumber(v int64) string {
- return humanize.Comma(v)
+// 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.
diff --git a/modules/base/tool_test.go b/modules/base/tool_test.go
index 5280827e8ae35..6685168bacd88 100644
--- a/modules/base/tool_test.go
+++ b/modules/base/tool_test.go
@@ -117,6 +117,7 @@ func TestFileSize(t *testing.T) {
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))
}
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 03e0e9899b44f..c0be5c1fa56ea 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -98,18 +98,19 @@ func NewFuncMap() []template.FuncMap {
"CustomEmojis": func() map[string]string {
return setting.UI.CustomEmojisMap
},
- "Safe": Safe,
- "SafeJS": SafeJS,
- "JSEscape": JSEscape,
- "Str2html": Str2html,
- "TimeSince": timeutil.TimeSince,
- "TimeSinceUnix": timeutil.TimeSinceUnix,
- "RawTimeSince": timeutil.RawTimeSince,
- "FileSize": base.FileSize,
- "PrettyNumber": base.PrettyNumber,
- "Subtract": base.Subtract,
- "EntryIcon": base.EntryIcon,
- "MigrationIcon": MigrationIcon,
+ "Safe": Safe,
+ "SafeJS": SafeJS,
+ "JSEscape": JSEscape,
+ "Str2html": Str2html,
+ "TimeSince": timeutil.TimeSince,
+ "TimeSinceUnix": timeutil.TimeSinceUnix,
+ "RawTimeSince": timeutil.RawTimeSince,
+ "FileSize": base.FileSize,
+ "PrettyNumber": base.PrettyNumber,
+ "JsPrettyNumber": JsPrettyNumber,
+ "Subtract": base.Subtract,
+ "EntryIcon": base.EntryIcon,
+ "MigrationIcon": MigrationIcon,
"Add": func(a ...int) int {
sum := 0
for _, val := range a {
@@ -1005,3 +1006,11 @@ 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(`` + base.PrettyNumber(num) + ``)
+}
diff --git a/modules/util/util.go b/modules/util/util.go
index 1017117874816..be60fe4b4bbdf 100644
--- a/modules/util/util.go
+++ b/modules/util/util.go
@@ -224,3 +224,21 @@ func Dedent(s string) string {
}
return strings.TrimSpace(s)
}
+
+// NumberIntoInt64 transform a given int into int64.
+func NumberIntoInt64(number interface{}) int64 {
+ var value int64
+ switch v := number.(type) {
+ case int:
+ value = int64(v)
+ case int8:
+ value = int64(v)
+ case int16:
+ value = int64(v)
+ case int32:
+ value = int64(v)
+ case int64:
+ value = v
+ }
+ return value
+}
diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini
index f1c164660d458..347022fbdb39b 100644
--- a/options/locale/locale_en-US.ini
+++ b/options/locale/locale_en-US.ini
@@ -1259,8 +1259,6 @@ issues.change_ref_at = `changed reference from %s to
issues.remove_ref_at = `removed reference %s %s`
issues.add_ref_at = `added reference %s %s`
issues.delete_branch_at = `deleted branch %s %s`
-issues.open_tab = %d Open
-issues.close_tab = %d Closed
issues.filter_label = Label
issues.filter_label_exclude = `Use alt
+ click/enter
to exclude labels`
issues.filter_label_no_select = All labels
@@ -1613,8 +1611,6 @@ pulls.auto_merge_newly_scheduled_comment = `scheduled this pull request to auto
pulls.auto_merge_canceled_schedule_comment = `canceled auto merging this pull request when all checks succeed %[1]s`
milestones.new = New Milestone
-milestones.open_tab = %d Open
-milestones.close_tab = %d Closed
milestones.closed = Closed %s
milestones.update_ago = Updated %s ago
milestones.no_due_date = No due date
diff --git a/templates/repo/issue/milestones.tmpl b/templates/repo/issue/milestones.tmpl
index b6b1c76d787a1..235044cb17bf2 100644
--- a/templates/repo/issue/milestones.tmpl
+++ b/templates/repo/issue/milestones.tmpl
@@ -18,11 +18,11 @@
@@ -83,8 +83,10 @@
{{end}}
{{end}}
- {{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}}
- {{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}}
+ {{svg "octicon-issue-opened" 16 "mr-3"}}
+ {{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}}
+ {{svg "octicon-check" 16 "mr-3"}}
+ {{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}}
{{if .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}}
{{if .UpdatedUnix}}{{svg "octicon-clock"}} {{$.i18n.Tr "repo.milestones.update_ago" (.TimeSinceUpdate|Sec2Time)}}{{end}}
diff --git a/templates/repo/issue/openclose.tmpl b/templates/repo/issue/openclose.tmpl
index 050660522a5b6..ae99f091b14ed 100644
--- a/templates/repo/issue/openclose.tmpl
+++ b/templates/repo/issue/openclose.tmpl
@@ -1,10 +1,14 @@
diff --git a/templates/repo/projects/list.tmpl b/templates/repo/projects/list.tmpl
index a8c51d668e247..7a2366da0b76e 100644
--- a/templates/repo/projects/list.tmpl
+++ b/templates/repo/projects/list.tmpl
@@ -14,12 +14,12 @@
{{template "base/alert" .}}
@@ -47,8 +47,10 @@
{{svg "octicon-clock"}} {{$.i18n.Tr "repo.milestones.closed" $closedDate|Str2html}}
{{end}}
- {{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.issues.open_tab" .NumOpenIssues}}
- {{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.issues.close_tab" .NumClosedIssues}}
+ {{svg "octicon-issue-opened" 16 "mr-3"}}
+ {{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}}
+ {{svg "octicon-check" 16 "mr-3"}}
+ {{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}}
{{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}}
diff --git a/templates/user/dashboard/issues.tmpl b/templates/user/dashboard/issues.tmpl
index ffaf5bb4ee01c..bd7d54b6705d9 100644
--- a/templates/user/dashboard/issues.tmpl
+++ b/templates/user/dashboard/issues.tmpl
@@ -61,11 +61,11 @@
diff --git a/templates/user/dashboard/milestones.tmpl b/templates/user/dashboard/milestones.tmpl
index 385f5c529b3a9..738438423f32c 100644
--- a/templates/user/dashboard/milestones.tmpl
+++ b/templates/user/dashboard/milestones.tmpl
@@ -38,12 +38,12 @@
@@ -103,9 +103,13 @@
{{end}}
{{end}}
- {{svg "octicon-issue-opened"}} {{$.i18n.Tr "repo.milestones.open_tab" .NumOpenIssues}}
- {{svg "octicon-issue-closed"}} {{$.i18n.Tr "repo.milestones.close_tab" .NumClosedIssues}}
- {{if .TotalTrackedTime}}{{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}{{end}}
+ {{svg "octicon-issue-opened" 16 "mr-3"}}
+ {{JsPrettyNumber .NumOpenIssues}} {{$.i18n.Tr "repo.issues.open_title"}}
+ {{svg "octicon-check" 16 "mr-3"}}
+ {{JsPrettyNumber .NumClosedIssues}} {{$.i18n.Tr "repo.issues.closed_title"}}
+ {{if .TotalTrackedTime}}
+ {{svg "octicon-clock"}} {{.TotalTrackedTime|Sec2Time}}
+ {{end}}
{{if and (or $.CanWriteIssues $.CanWritePulls) (not $.Repository.IsArchived)}}
diff --git a/web_src/js/features/formatting.js b/web_src/js/features/formatting.js
new file mode 100644
index 0000000000000..a7ee7ec3cf36f
--- /dev/null
+++ b/web_src/js/features/formatting.js
@@ -0,0 +1,14 @@
+import {prettyNumber} from '../utils.js';
+
+const {lang} = document.documentElement;
+
+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;
+ }
+ }
+}
diff --git a/web_src/js/index.js b/web_src/js/index.js
index b6a1aee7796f6..0568da64aec51 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -84,6 +84,11 @@ import {initRepoBranchButton} from './features/repo-branch.js';
import {initCommonOrganization} from './features/common-organization.js';
import {initRepoWikiForm} from './features/repo-wiki.js';
import {initRepoCommentForm, initRepository} from './features/repo-legacy.js';
+import {initFormattingReplacements} from './features/formatting.js';
+
+// Run time-critical code as soon as possible. This is safe to do because this
+// script appears at the end of and rendered HTML is accessible at that point.
+initFormattingReplacements();
// Silence fomantic's error logging when tabs are used without a target content element
$.fn.tab.settings.silent = true;
diff --git a/web_src/js/utils.js b/web_src/js/utils.js
index 67f8f1cc9865d..f01f2d3b22447 100644
--- a/web_src/js/utils.js
+++ b/web_src/js/utils.js
@@ -90,3 +90,10 @@ export function strSubMatch(full, sub) {
}
return res;
}
+
+// 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);
+}
diff --git a/web_src/js/utils.test.js b/web_src/js/utils.test.js
index acf3f1ece3e09..ba5335e3e43bf 100644
--- a/web_src/js/utils.test.js
+++ b/web_src/js/utils.test.js
@@ -1,5 +1,5 @@
import {
- basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, strSubMatch,
+ basename, extname, isObject, uniq, stripTags, joinPaths, parseIssueHref, strSubMatch, prettyNumber,
} from './utils.js';
test('basename', () => {
@@ -85,7 +85,6 @@ test('parseIssueHref', () => {
expect(parseIssueHref('')).toEqual({owner: undefined, repo: undefined, type: undefined, index: undefined});
});
-
test('strSubMatch', () => {
expect(strSubMatch('abc', '')).toEqual(['abc']);
expect(strSubMatch('abc', 'a')).toEqual(['', 'a', 'bc']);
@@ -98,3 +97,14 @@ test('strSubMatch', () => {
expect(strSubMatch('aabbcc', 'abc')).toEqual(['', 'a', 'a', 'b', 'b', 'c', 'c']);
expect(strSubMatch('the/directory', 'hedir')).toEqual(['t', 'he', '/', 'dir', 'ectory']);
});
+
+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');
+});