diff --git a/.deadcode-out b/.deadcode-out index 1b65e3203d..940551da04 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -295,6 +295,7 @@ package "code.gitea.io/gitea/modules/translation" func (MockLocale).TrString func (MockLocale).Tr func (MockLocale).TrN + func (MockLocale).TrSize func (MockLocale).PrettyNumber package "code.gitea.io/gitea/modules/util/filebuffer" diff --git a/models/repo/repo.go b/models/repo/repo.go index 4e1b6fcbcf..350dc86d4b 100644 --- a/models/repo/repo.go +++ b/models/repo/repo.go @@ -16,7 +16,6 @@ import ( "code.gitea.io/gitea/models/db" "code.gitea.io/gitea/models/unit" user_model "code.gitea.io/gitea/models/user" - "code.gitea.io/gitea/modules/base" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/markup" @@ -24,6 +23,7 @@ import ( "code.gitea.io/gitea/modules/setting" api "code.gitea.io/gitea/modules/structs" "code.gitea.io/gitea/modules/timeutil" + "code.gitea.io/gitea/modules/translation" "code.gitea.io/gitea/modules/util" "xorm.io/builder" @@ -249,13 +249,17 @@ func (repo *Repository) SizeDetails() []SizeDetail { } // SizeDetailsString returns a concatenation of all repository size details as a string -func (repo *Repository) SizeDetailsString() string { +func (repo *Repository) SizeDetailsString(locale translation.Locale) string { var str strings.Builder sizeDetails := repo.SizeDetails() - for _, detail := range sizeDetails { - str.WriteString(fmt.Sprintf("%s: %s, ", detail.Name, base.FileSize(detail.Size))) + for i, detail := range sizeDetails { + if i > 0 { + // TODO: use semicolon if decimal point of user localization is a comma + str.WriteString(", ") + } + str.WriteString(fmt.Sprintf("%s: %s", detail.Name, locale.TrSize(detail.Size))) } - return strings.TrimSuffix(str.String(), ", ") + return str.String() } func (repo *Repository) LogString() string { diff --git a/modules/templates/helper.go b/modules/templates/helper.go index 18a2993fb8..c9799a38ec 100644 --- a/modules/templates/helper.go +++ b/modules/templates/helper.go @@ -63,7 +63,7 @@ func NewFuncMap() template.FuncMap { // ----------------------------------------------------------------- // time / number / format - "FileSize": base.FileSize, + "FileSize": FileSizePanic, "CountFmt": base.FormatNumberSI, "TimeSince": timeutil.TimeSince, "TimeSinceUnix": timeutil.TimeSinceUnix, @@ -249,3 +249,7 @@ func Eval(tokens ...any) (any, error) { n, err := eval.Expr(tokens...) return n.Value, err } + +func FileSizePanic(s int64) string { + panic("Usage of FileSize in templates is deprecated in Forgejo. Locale.TrSize should be used instead.") +} diff --git a/modules/translation/mock.go b/modules/translation/mock.go index 18fbc1044a..fe3a1502ea 100644 --- a/modules/translation/mock.go +++ b/modules/translation/mock.go @@ -31,6 +31,10 @@ func (l MockLocale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { return template.HTML(key1) } +func (l MockLocale) TrSize(s int64) ReadableSize { + return ReadableSize{fmt.Sprint(s), ""} +} + func (l MockLocale) PrettyNumber(v any) string { return fmt.Sprint(v) } diff --git a/modules/translation/translation.go b/modules/translation/translation.go index 36ae58a9f1..16eb55e28e 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -16,6 +16,7 @@ import ( "code.gitea.io/gitea/modules/translation/i18n" "code.gitea.io/gitea/modules/util" + "github.com/dustin/go-humanize" "golang.org/x/text/language" "golang.org/x/text/message" "golang.org/x/text/number" @@ -33,6 +34,8 @@ type Locale interface { Tr(key string, args ...any) template.HTML TrN(cnt any, key1, keyN string, args ...any) template.HTML + TrSize(size int64) ReadableSize + PrettyNumber(v any) string } @@ -252,6 +255,35 @@ func (l *locale) TrN(cnt any, key1, keyN string, args ...any) template.HTML { return l.Tr(keyN, args...) } +type ReadableSize struct { + PrettyNumber string + TranslatedUnit string +} + +func (bs ReadableSize) String() string { + return bs.PrettyNumber + " " + bs.TranslatedUnit +} + +// TrSize returns array containing pretty formatted size and localized output of FileSize +// output of humanize.IBytes has to be split in order to be localized +func (l *locale) TrSize(s int64) ReadableSize { + us := uint64(s) + if s < 0 { + us = uint64(-s) + } + untranslated := humanize.IBytes(us) + if s < 0 { + untranslated = "-" + untranslated + } + numberVal, unitVal, found := strings.Cut(untranslated, " ") + if !found { + log.Error("no space in go-humanized size of %d: %q", s, untranslated) + } + numberVal = l.PrettyNumber(numberVal) + unitVal = l.TrString("munits.data." + strings.ToLower(unitVal)) + return ReadableSize{numberVal, unitVal} +} + func (l *locale) PrettyNumber(v any) string { // TODO: this mechanism is not good enough, the complete solution is to switch the translation system to ICU message format if s, ok := v.(string); ok { diff --git a/modules/translation/translation_test.go b/modules/translation/translation_test.go index 464aa32661..bffbb155ca 100644 --- a/modules/translation/translation_test.go +++ b/modules/translation/translation_test.go @@ -3,6 +3,8 @@ package translation +// TODO: make this package friendly to testing + import ( "testing" @@ -11,9 +13,25 @@ import ( "github.com/stretchr/testify/assert" ) -func TestPrettyNumber(t *testing.T) { - // TODO: make this package friendly to testing +func TestTrSize(t *testing.T) { + l := NewLocale("") + size := int64(1) + assert.EqualValues(t, "1 munits.data.b", l.TrSize(size).String()) + size *= 2048 + assert.EqualValues(t, "2 munits.data.kib", l.TrSize(size).String()) + size *= 2048 + assert.EqualValues(t, "4 munits.data.mib", l.TrSize(size).String()) + size *= 2048 + assert.EqualValues(t, "8 munits.data.gib", l.TrSize(size).String()) + size *= 2048 + assert.EqualValues(t, "16 munits.data.tib", l.TrSize(size).String()) + size *= 2048 + assert.EqualValues(t, "32 munits.data.pib", l.TrSize(size).String()) + size *= 128 + assert.EqualValues(t, "4 munits.data.eib", l.TrSize(size).String()) +} +func TestPrettyNumber(t *testing.T) { i18n.ResetDefaultLocales() allLangMap = make(map[string]*LangType) diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index fbe67c28b8..078c82c681 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -3417,6 +3417,15 @@ years = %d years raw_seconds = seconds raw_minutes = minutes +[munits.data] +b = B +kib = KiB +mib = MiB +gib = GiB +tib = TiB +pib = PiB +eib = EiB + [dropzone] default_message = Drop files or click here to upload. invalid_input_type = You cannot upload files of this type. diff --git a/options/locale/locale_ru-RU.ini b/options/locale/locale_ru-RU.ini index e9daba082e..01aeaaffc4 100644 --- a/options/locale/locale_ru-RU.ini +++ b/options/locale/locale_ru-RU.ini @@ -3417,6 +3417,15 @@ years=%d лет raw_seconds=секунд raw_minutes=минут +[munits.data] +b = Б +kib = КиБ +mib = МиБ +gib = ГиБ +tib = ТиБ +pib = ПиБ +eib = ЕиБ + [dropzone] default_message=Перетащите файл или кликните сюда для загрузки. invalid_input_type=Вы не можете загружать файлы этого типа. diff --git a/templates/admin/packages/list.tmpl b/templates/admin/packages/list.tmpl index 863f11da25..d111c57378 100644 --- a/templates/admin/packages/list.tmpl +++ b/templates/admin/packages/list.tmpl @@ -2,8 +2,8 @@

{{ctx.Locale.Tr "admin.packages.package_manage_panel"}} ({{ctx.Locale.Tr "admin.total" .TotalCount}}, - {{ctx.Locale.Tr "admin.packages.total_size" (FileSize .TotalBlobSize)}}, - {{ctx.Locale.Tr "admin.packages.unreferenced_size" (FileSize .TotalUnreferencedBlobSize)}}) + {{ctx.Locale.Tr "admin.packages.total_size" (ctx.Locale.TrSize .TotalBlobSize)}}, + {{ctx.Locale.Tr "admin.packages.unreferenced_size" (ctx.Locale.TrSize .TotalUnreferencedBlobSize)}})
{{.CsrfTokenHtml}} @@ -70,7 +70,7 @@ {{.Repository.Name}} {{end}} - {{FileSize .CalculateBlobSize}} + {{ctx.Locale.TrSize .CalculateBlobSize}} {{DateTime "short" .Version.CreatedUnix}} {{svg "octicon-trash"}} diff --git a/templates/admin/repo/list.tmpl b/templates/admin/repo/list.tmpl index 4b27d87a45..2c6e1d67a9 100644 --- a/templates/admin/repo/list.tmpl +++ b/templates/admin/repo/list.tmpl @@ -80,8 +80,8 @@ {{.NumStars}} {{.NumForks}} {{.NumIssues}} - {{FileSize .GitSize}} - {{FileSize .LFSSize}} + {{ctx.Locale.TrSize .GitSize}} + {{ctx.Locale.TrSize .LFSSize}} {{DateTime "short" .UpdatedUnix}} {{DateTime "short" .CreatedUnix}} {{svg "octicon-trash"}} diff --git a/templates/mail/release.tmpl b/templates/mail/release.tmpl index 90a3caa4c5..92af3216bb 100644 --- a/templates/mail/release.tmpl +++ b/templates/mail/release.tmpl @@ -43,7 +43,7 @@ {{range .Release.Attachments}}
  • - {{.Name}} ({{.Size | FileSize}}) + {{.Name}} ({{.Size | $.locale.TrSize}})
  • {{end}} diff --git a/templates/package/content/container.tmpl b/templates/package/content/container.tmpl index 0d5f0f09d5..9e255b3d60 100644 --- a/templates/package/content/container.tmpl +++ b/templates/package/content/container.tmpl @@ -39,7 +39,7 @@ {{.Digest}} {{.Platform}} - {{FileSize .Size}} + {{ctx.Locale.TrSize .Size}} {{end}} diff --git a/templates/package/shared/cleanup_rules/preview.tmpl b/templates/package/shared/cleanup_rules/preview.tmpl index cff8e8249f..0d9c4b0d46 100644 --- a/templates/package/shared/cleanup_rules/preview.tmpl +++ b/templates/package/shared/cleanup_rules/preview.tmpl @@ -21,7 +21,7 @@ {{.Package.Name}} {{.Version.Version}} {{.Creator.Name}} - {{FileSize .CalculateBlobSize}} + {{ctx.Locale.TrSize .CalculateBlobSize}} {{DateTime "short" .Version.CreatedUnix}} {{else}} diff --git a/templates/package/view.tmpl b/templates/package/view.tmpl index 6beb249a7f..1d87f4d3af 100644 --- a/templates/package/view.tmpl +++ b/templates/package/view.tmpl @@ -70,7 +70,7 @@ {{template "package/metadata/swift" .}} {{template "package/metadata/vagrant" .}} {{if not (and (eq .PackageDescriptor.Package.Type "container") .PackageDescriptor.Metadata.Manifests)}} -
    {{svg "octicon-database" 16 "tw-mr-2"}} {{FileSize .PackageDescriptor.CalculateBlobSize}}
    +
    {{svg "octicon-database" 16 "tw-mr-2"}} {{ctx.Locale.TrSize .PackageDescriptor.CalculateBlobSize}}
    {{end}}
    {{if not (eq .PackageDescriptor.Package.Type "container")}} @@ -80,7 +80,7 @@ {{range .PackageDescriptor.Files}}
    {{.File.Name}} - {{FileSize .Blob.Size}} + {{ctx.Locale.TrSize .Blob.Size}}
    {{end}}

    diff --git a/templates/repo/diff/image_diff.tmpl b/templates/repo/diff/image_diff.tmpl index 9ad7916398..0612854609 100644 --- a/templates/repo/diff/image_diff.tmpl +++ b/templates/repo/diff/image_diff.tmpl @@ -30,7 +30,7 @@ {{ctx.Locale.Tr "repo.diff.file_image_height"}}:  |  - {{ctx.Locale.Tr "repo.diff.file_byte_size"}}: {{FileSize .blobBase.Size}} + {{ctx.Locale.Tr "repo.diff.file_byte_size"}}: {{ctx.Locale.TrSize .blobBase.Size}}

    {{end}} @@ -45,7 +45,7 @@ {{ctx.Locale.Tr "repo.diff.file_image_height"}}:  |  - {{ctx.Locale.Tr "repo.diff.file_byte_size"}}: {{FileSize .blobHead.Size}} + {{ctx.Locale.Tr "repo.diff.file_byte_size"}}: {{ctx.Locale.TrSize .blobHead.Size}}

    {{end}} diff --git a/templates/repo/file_info.tmpl b/templates/repo/file_info.tmpl index 86c613e3a1..5527991644 100644 --- a/templates/repo/file_info.tmpl +++ b/templates/repo/file_info.tmpl @@ -11,7 +11,7 @@ {{end}} {{if .FileSize}}
    - {{FileSize .FileSize}}{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}} + {{ctx.Locale.TrSize .FileSize}}{{if .IsLFSFile}} ({{ctx.Locale.Tr "repo.stored_lfs"}}){{end}}
    {{end}} {{if .LFSLock}} diff --git a/templates/repo/issue/view_content/attachments.tmpl b/templates/repo/issue/view_content/attachments.tmpl index 2155f78656..79085df3ab 100644 --- a/templates/repo/issue/view_content/attachments.tmpl +++ b/templates/repo/issue/view_content/attachments.tmpl @@ -19,7 +19,7 @@
    - {{.Size | FileSize}} + {{.Size | ctx.Locale.TrSize}}
    {{end -}} diff --git a/templates/repo/release/list.tmpl b/templates/repo/release/list.tmpl index f3b4bc8443..329dc932fb 100644 --- a/templates/repo/release/list.tmpl +++ b/templates/repo/release/list.tmpl @@ -81,7 +81,7 @@ {{svg "octicon-package" 16 "tw-mr-1"}}{{.Name}}
    - {{.Size | FileSize}} + {{.Size | ctx.Locale.TrSize}} {{svg "octicon-info"}} diff --git a/templates/repo/release/new.tmpl b/templates/repo/release/new.tmpl index c01f9a421b..b63ad5dcd3 100644 --- a/templates/repo/release/new.tmpl +++ b/templates/repo/release/new.tmpl @@ -64,7 +64,7 @@
    - {{.Size | FileSize}} + {{.Size | ctx.Locale.TrSize}} {{svg "octicon-info"}} diff --git a/templates/repo/settings/lfs.tmpl b/templates/repo/settings/lfs.tmpl index e0864ff221..d63de3f53c 100644 --- a/templates/repo/settings/lfs.tmpl +++ b/templates/repo/settings/lfs.tmpl @@ -16,7 +16,7 @@ {{ShortSha .Oid}} - {{FileSize .Size}} + {{ctx.Locale.TrSize .Size}} {{TimeSince .CreatedUnix.AsTime ctx.Locale}} {{ctx.Locale.Tr "repo.settings.lfs_findcommits"}} diff --git a/templates/repo/settings/options.tmpl b/templates/repo/settings/options.tmpl index 21a5471cb8..8b73afe6d0 100644 --- a/templates/repo/settings/options.tmpl +++ b/templates/repo/settings/options.tmpl @@ -14,7 +14,7 @@
    - {{FileSize .Repository.Size}} + {{ctx.Locale.TrSize .Repository.Size}}
    diff --git a/templates/repo/sub_menu.tmpl b/templates/repo/sub_menu.tmpl index e43353ef04..ddf01b33ed 100644 --- a/templates/repo/sub_menu.tmpl +++ b/templates/repo/sub_menu.tmpl @@ -13,10 +13,9 @@ {{svg "octicon-tag"}} {{ctx.Locale.TrN .NumTags "repo.n_tag_one" "repo.n_tag_few" (printf "%d" .NumTags | SafeHTML)}} {{end}} - - {{$fileSizeFormatted := FileSize .Repository.Size}}{{/* the formatted string is always "{val} {unit}" */}} - {{$fileSizeFields := StringUtils.Split $fileSizeFormatted " "}} - {{svg "octicon-database"}} {{ctx.Locale.PrettyNumber (index $fileSizeFields 0)}} {{index $fileSizeFields 1}} + + {{$fileSizeFields := ctx.Locale.TrSize .Repository.Size}} + {{svg "octicon-database"}} {{$fileSizeFields.PrettyNumber}} {{$fileSizeFields.TranslatedUnit}} {{end}}
    diff --git a/templates/user/settings/repos.tmpl b/templates/user/settings/repos.tmpl index c874ccd878..4fd0c37d57 100644 --- a/templates/user/settings/repos.tmpl +++ b/templates/user/settings/repos.tmpl @@ -24,7 +24,7 @@ {{svg "octicon-repo"}} {{end}} {{$repo.OwnerName}}/{{$repo.Name}} - {{FileSize $repo.Size}} + {{ctx.Locale.TrSize $repo.Size}} {{if $repo.IsFork}} {{ctx.Locale.Tr "repo.forked_from"}} {{$repo.BaseRepo.OwnerName}}/{{$repo.BaseRepo.Name}} @@ -97,7 +97,7 @@ {{svg "octicon-repo" 16 "tw-mr-1 iconFloat"}} {{end}} {{.OwnerName}}/{{.Name}} - {{FileSize .Size}} + {{ctx.Locale.TrSize .Size}} {{if .IsFork}} {{ctx.Locale.Tr "repo.forked_from"}} {{.BaseRepo.OwnerName}}/{{.BaseRepo.Name}} diff --git a/tests/integration/integration_test.go b/tests/integration/integration_test.go index b087281ff4..e147d6a21b 100644 --- a/tests/integration/integration_test.go +++ b/tests/integration/integration_test.go @@ -218,6 +218,16 @@ func (s *TestSession) GetCookie(name string) *http.Cookie { return nil } +func (s *TestSession) SetCookie(cookie *http.Cookie) *http.Cookie { + baseURL, err := url.Parse(setting.AppURL) + if err != nil { + return nil + } + + s.jar.SetCookies(baseURL, []*http.Cookie{cookie}) + return nil +} + func (s *TestSession) MakeRequest(t testing.TB, rw *RequestWrapper, expectedStatus int) *httptest.ResponseRecorder { t.Helper() req := rw.Request diff --git a/tests/integration/size_translations_test.go b/tests/integration/size_translations_test.go new file mode 100644 index 0000000000..0a296ad385 --- /dev/null +++ b/tests/integration/size_translations_test.go @@ -0,0 +1,115 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +package integration + +import ( + "net/http" + "net/url" + "path" + "regexp" + "strings" + "testing" + + "code.gitea.io/gitea/models/unittest" + user_model "code.gitea.io/gitea/models/user" + files_service "code.gitea.io/gitea/services/repository/files" + + "github.com/PuerkitoBio/goquery" + "github.com/stretchr/testify/assert" +) + +func TestDataSizeTranslation(t *testing.T) { + onGiteaRun(t, func(t *testing.T, giteaURL *url.URL) { + testUser := "user2" + testRepoName := "data_size_test" + noDigits := regexp.MustCompile("[0-9]+") + longString100 := `testRepoMigrate(t, session, "https://code.forgejo.org/forgejo/test_repo.git", testRepoName, struct)` + "\n" + + // Login user + user2 := unittest.AssertExistsAndLoadBean(t, &user_model.User{Name: testUser}) + session := loginUser(t, testUser) + + // Create test repo + testRepo, _, f := CreateDeclarativeRepo(t, user2, testRepoName, nil, nil, + []*files_service.ChangeRepoFile{ + { + Operation: "create", + TreePath: "137byteFile.txt", + ContentReader: strings.NewReader(longString100 + strings.Repeat("1", 36) + "\n"), + }, + { + Operation: "create", + TreePath: "1.5kibFile.txt", + ContentReader: strings.NewReader(strings.Repeat(longString100, 15) + strings.Repeat("1", 35) + "\n"), + }, + { + Operation: "create", + TreePath: "1.25mibFile.txt", + ContentReader: strings.NewReader(strings.Repeat(longString100, 13107) + strings.Repeat("1", 19) + "\n"), + }, + }) + defer f() + + // Change language from English to catch regressions that make translated sizes fall back to + // not translated, like to raw output of FileSize() or humanize.IBytes() + lang := session.GetCookie("lang") + lang.Value = "ru-RU" + session.SetCookie(lang) + + // Go to /user/settings/repos + req := NewRequest(t, "GET", "user/settings/repos") + resp := session.MakeRequest(t, req, http.StatusOK) + + // Check if repo size is translated + repos := NewHTMLParser(t, resp.Body).Find(".user-setting-content .list .item .content") + assert.True(t, repos.Length() > 0) + repos.Each(func(i int, repo *goquery.Selection) { + repoName := repo.Find("a.name").Text() + if repoName == path.Join(testUser, testRepo.Name) { + repoSize := repo.Find("span").Text() + repoSize = noDigits.ReplaceAllString(repoSize, "") + assert.Equal(t, " КиБ", repoSize) + } + }) + + // Go to /user2/repo1 + req = NewRequest(t, "GET", path.Join(testUser, testRepoName)) + resp = session.MakeRequest(t, req, http.StatusOK) + + // Check if repo size in repo summary is translated + repo := NewHTMLParser(t, resp.Body).Find(".repository-summary span") + repoSize := strings.TrimSpace(repo.Text()) + repoSize = noDigits.ReplaceAllString(repoSize, "") + assert.Equal(t, " КиБ", repoSize) + + // Check if repo sizes in the tooltip are translated + fullSize, exists := repo.Attr("data-tooltip-content") + assert.True(t, exists) + fullSize = noDigits.ReplaceAllString(fullSize, "") + assert.Equal(t, "git: КиБ, lfs: Б", fullSize) + + // Check if file sizes are correclty translated + testFileSizeTranslated(t, session, path.Join(testUser, testRepoName, "src/branch/main/137byteFile.txt"), "137 Б") + testFileSizeTranslated(t, session, path.Join(testUser, testRepoName, "src/branch/main/1.5kibFile.txt"), "1,5 КиБ") + testFileSizeTranslated(t, session, path.Join(testUser, testRepoName, "src/branch/main/1.25mibFile.txt"), "1,3 МиБ") + }) +} + +func testFileSizeTranslated(t *testing.T, session *TestSession, filePath, correctSize string) { + // Go to specified file page + req := NewRequest(t, "GET", filePath) + resp := session.MakeRequest(t, req, http.StatusOK) + + // Check if file size is translated + sizeCorrent := false + fileInfo := NewHTMLParser(t, resp.Body).Find(".file-info .file-info-entry") + fileInfo.Each(func(i int, info *goquery.Selection) { + infoText := strings.TrimSpace(info.Text()) + if infoText == correctSize { + sizeCorrent = true + } + }) + + assert.True(t, sizeCorrent) +}