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'); +});