diff --git a/.deadcode-out b/.deadcode-out index 64741ec7ac..a44599b6f1 100644 --- a/.deadcode-out +++ b/.deadcode-out @@ -246,6 +246,7 @@ code.gitea.io/gitea/modules/translation MockLocale.TrString MockLocale.Tr MockLocale.TrN + MockLocale.TrPluralString MockLocale.TrSize MockLocale.PrettyNumber diff --git a/Makefile b/Makefile index 3f05ae0b93..5865262d1a 100644 --- a/Makefile +++ b/Makefile @@ -49,7 +49,7 @@ GOVULNCHECK_PACKAGE ?= golang.org/x/vuln/cmd/govulncheck@v1 # renovate: datasour DEADCODE_PACKAGE ?= golang.org/x/tools/cmd/deadcode@v0.29.0 # renovate: datasource=go GOMOCK_PACKAGE ?= go.uber.org/mock/mockgen@v0.4.0 # renovate: datasource=go GOPLS_PACKAGE ?= golang.org/x/tools/gopls@v0.17.1 # renovate: datasource=go -RENOVATE_NPM_PACKAGE ?= renovate@39.106.0 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate +RENOVATE_NPM_PACKAGE ?= renovate@39.115.4 # renovate: datasource=docker packageName=data.forgejo.org/renovate/renovate # https://github.com/disposable-email-domains/disposable-email-domains/commits/main/ DISPOSABLE_EMAILS_SHA ?= 0c27e671231d27cf66370034d7f6818037416989 # renovate: ... diff --git a/go.mod b/go.mod index e069a33996..bc93365ff2 100644 --- a/go.mod +++ b/go.mod @@ -17,6 +17,7 @@ require ( codeberg.org/gusted/mcaptcha v0.0.0-20220723083913-4f3072e1d570 connectrpc.com/connect v1.17.0 gitea.com/lunny/levelqueue v0.4.2-0.20230414023320-3c0159fe0fe4 + github.com/42wim/httpsig v1.2.2 github.com/42wim/sshsig v0.0.0-20211121163825-841cf5bbc121 github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 github.com/ProtonMail/go-crypto v1.1.4 @@ -26,7 +27,7 @@ require ( github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb github.com/blevesearch/bleve/v2 v2.4.4 github.com/buildkite/terminal-to-html/v3 v3.16.4 - github.com/caddyserver/certmagic v0.21.6 + github.com/caddyserver/certmagic v0.21.7 github.com/chi-middleware/proxy v1.1.1 github.com/djherbis/buffer v1.2.0 github.com/djherbis/nio/v3 v3.0.1 @@ -43,7 +44,6 @@ require ( github.com/go-chi/cors v1.2.1 github.com/go-co-op/gocron v1.37.0 github.com/go-enry/go-enry/v2 v2.9.1 - github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e github.com/go-git/go-git/v5 v5.13.1 github.com/go-ldap/ldap/v3 v3.4.6 github.com/go-openapi/spec v0.20.14 @@ -109,7 +109,7 @@ require ( golang.org/x/sys v0.29.0 golang.org/x/text v0.21.0 google.golang.org/grpc v1.69.2 - google.golang.org/protobuf v1.36.2 + google.golang.org/protobuf v1.36.3 gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df gopkg.in/ini.v1 v1.67.0 gopkg.in/yaml.v3 v3.0.1 @@ -131,7 +131,6 @@ require ( dario.cat/mergo v1.0.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect git.sr.ht/~mariusor/go-xsd-duration v0.0.0-20220703122237-02e73435a078 // indirect - github.com/42wim/httpsig v1.2.2 // indirect github.com/DataDog/zstd v1.5.5 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.24.2 // indirect @@ -184,6 +183,7 @@ require ( github.com/go-ap/errors v0.0.0-20231003111023-183eef4b31b7 // indirect github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect github.com/go-enry/go-oniguruma v1.2.1 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.6.1 // indirect github.com/go-ini/ini v1.67.0 // indirect @@ -221,7 +221,7 @@ require ( github.com/markbates/going v1.0.3 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect - github.com/mholt/acmez/v3 v3.0.0 // indirect + github.com/mholt/acmez/v3 v3.0.1 // indirect github.com/miekg/dns v1.1.62 // indirect github.com/minio/md5-simd v1.1.2 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect diff --git a/go.sum b/go.sum index 58abf76d25..3064c26ba9 100644 --- a/go.sum +++ b/go.sum @@ -770,8 +770,8 @@ github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= github.com/buildkite/terminal-to-html/v3 v3.16.4 h1:QFYO8IGvRnp7tGgiQb8g9uFU8kY9wOzxsFFx17+yy6Q= github.com/buildkite/terminal-to-html/v3 v3.16.4/go.mod h1:r/J7cC9c3EzBzP3/wDz0RJLPwv5PUAMp+KF2w+ntMc0= -github.com/caddyserver/certmagic v0.21.6 h1:1th6GfprVfsAtFNOu4StNMF5IxK5XiaI0yZhAHlZFPE= -github.com/caddyserver/certmagic v0.21.6/go.mod h1:n1sCo7zV1Ez2j+89wrzDxo4N/T1Ws/Vx8u5NvuBFabw= +github.com/caddyserver/certmagic v0.21.7 h1:66KJioPFJwttL43KYSWk7ErSmE6LfaJgCQuhm8Sg6fg= +github.com/caddyserver/certmagic v0.21.7/go.mod h1:LCPG3WLxcnjVKl/xpjzM0gqh0knrKKKiO5WVttX2eEI= github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+YTAyA= github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -919,8 +919,8 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw= github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw= github.com/go-faster/errors v0.7.1 h1:MkJTnDoEdi9pDabt1dpWf7AA8/BaSYZqibYyhZ20AYg= github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo= -github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e h1:oRq/fiirun5HqlEWMLIcDmLpIELlG4iGbd0s8iqgPi8= -github.com/go-fed/httpsig v1.1.1-0.20201223112313-55836744818e/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= @@ -1253,8 +1253,8 @@ github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBW github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/meilisearch/meilisearch-go v0.29.0 h1:HZ9NEKN59USINQ/DXJge/aaXq8IrsKbXGTdAoBaaDz4= github.com/meilisearch/meilisearch-go v0.29.0/go.mod h1:2cRCAn4ddySUsFfNDLVPod/plRibQsJkXF/4gLhxbOk= -github.com/mholt/acmez/v3 v3.0.0 h1:r1NcjuWR0VaKP2BTjDK9LRFBw/WvURx3jlaEUl9Ht8E= -github.com/mholt/acmez/v3 v3.0.0/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= +github.com/mholt/acmez/v3 v3.0.1 h1:4PcjKjaySlgXK857aTfDuRbmnM5gb3Ruz3tvoSJAUp8= +github.com/mholt/acmez/v3 v3.0.1/go.mod h1:L1wOU06KKvq7tswuMDwKdcHeKpFFgkppZy/y0DFxagQ= github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= github.com/miekg/dns v1.1.62 h1:cN8OuEF1/x5Rq6Np+h1epln8OiyPWV+lROx9LxcGgIQ= @@ -2179,8 +2179,8 @@ google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk= gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/models/issues/issue.go b/models/issues/issue.go index fbbc4828a2..1e969790d7 100644 --- a/models/issues/issue.go +++ b/models/issues/issue.go @@ -268,6 +268,9 @@ func (issue *Issue) loadCommentsByType(ctx context.Context, tp CommentType) (err IssueID: issue.ID, Type: tp, }) + for _, comment := range issue.Comments { + comment.Issue = issue + } return err } diff --git a/models/migrations/v1_23/v303.go b/models/migrations/v1_23/v303.go index e3ee180539..2fb37ac843 100644 --- a/models/migrations/v1_23/v303.go +++ b/models/migrations/v1_23/v303.go @@ -1,23 +1,27 @@ -// Copyright 2024 The Forgejo Authors. -// SPDX-License-Identifier: MIT +// Copyright 2025 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later package v1_23 //nolint import ( - "fmt" - "code.gitea.io/gitea/models/migrations/base" "xorm.io/xorm" + "xorm.io/xorm/schemas" ) func GiteaLastDrop(x *xorm.Engine) error { + tables, err := x.DBMetas() + if err != nil { + return err + } + sess := x.NewSession() defer sess.Close() for _, drop := range []struct { - table string - field string + table string + column string }{ {"badge", "slug"}, {"oauth2_application", "skip_secondary_authorization"}, @@ -29,10 +33,25 @@ func GiteaLastDrop(x *xorm.Engine) error { {"protected_branch", "force_push_allowlist_team_i_ds"}, {"protected_branch", "force_push_allowlist_deploy_keys"}, } { - if _, err := sess.Exec(fmt.Sprintf("SELECT `%s` FROM `%s` WHERE 0 = 1", drop.field, drop.table)); err != nil { + var table *schemas.Table + found := false + + for _, table = range tables { + if table.Name == drop.table { + found = true + break + } + } + + if !found { continue } - if err := base.DropTableColumns(sess, drop.table, drop.field); err != nil { + + if table.GetColumn(drop.column) == nil { + continue + } + + if err := base.DropTableColumns(sess, drop.table, drop.column); err != nil { return err } } diff --git a/models/migrations/v1_23/v303_test.go b/models/migrations/v1_23/v303_test.go new file mode 100644 index 0000000000..752eacee0c --- /dev/null +++ b/models/migrations/v1_23/v303_test.go @@ -0,0 +1,41 @@ +// Copyright 2025 The Forgejo Authors. +// SPDX-License-Identifier: GPL-3.0-or-later + +package v1_23 //nolint + +import ( + "testing" + + migration_tests "code.gitea.io/gitea/models/migrations/test" + + "github.com/stretchr/testify/require" + "xorm.io/xorm/schemas" +) + +func Test_GiteaLastDrop(t *testing.T) { + type Badge struct { + ID int64 `xorm:"pk autoincr"` + Slug string + } + + x, deferable := migration_tests.PrepareTestEnv(t, 0, new(Badge)) + defer deferable() + if x == nil || t.Failed() { + return + } + + getColumn := func() *schemas.Column { + tables, err := x.DBMetas() + require.NoError(t, err) + require.Len(t, tables, 1) + table := tables[0] + require.Equal(t, "badge", table.Name) + return table.GetColumn("slug") + } + + require.NotNil(t, getColumn(), "slug column exists") + require.NoError(t, GiteaLastDrop(x)) + require.Nil(t, getColumn(), "slug column was deleted") + // idempotent + require.NoError(t, GiteaLastDrop(x)) +} diff --git a/modules/activitypub/client.go b/modules/activitypub/client.go index 064d8984c1..645a4b8c8d 100644 --- a/modules/activitypub/client.go +++ b/modules/activitypub/client.go @@ -22,7 +22,7 @@ import ( "code.gitea.io/gitea/modules/proxy" "code.gitea.io/gitea/modules/setting" - "github.com/go-fed/httpsig" + "github.com/42wim/httpsig" ) const ( diff --git a/modules/git/remote.go b/modules/git/remote.go index 3585313f6a..eea57dd8e0 100644 --- a/modules/git/remote.go +++ b/modules/git/remote.go @@ -5,6 +5,7 @@ package git import ( "context" + "strings" giturl "code.gitea.io/gitea/modules/git/url" ) @@ -37,3 +38,12 @@ func GetRemoteURL(ctx context.Context, repoPath, remoteName string) (*giturl.Git } return giturl.Parse(addr) } + +// IsRemoteNotExistError checks the prefix of the error message to see whether a remote does not exist. +func IsRemoteNotExistError(err error) bool { + // see: https://github.com/go-gitea/gitea/issues/32889#issuecomment-2571848216 + // Should not add space in the end, sometimes git will add a `:` + prefix1 := "exit status 128 - fatal: No such remote" // git < 2.30 + prefix2 := "exit status 2 - error: No such remote" // git >= 2.30 + return strings.HasPrefix(err.Error(), prefix1) || strings.HasPrefix(err.Error(), prefix2) +} diff --git a/modules/setting/federation.go b/modules/setting/federation.go index aeb30683ea..edb18e0054 100644 --- a/modules/setting/federation.go +++ b/modules/setting/federation.go @@ -6,7 +6,7 @@ package setting import ( "code.gitea.io/gitea/modules/log" - "github.com/go-fed/httpsig" + "github.com/42wim/httpsig" ) // Federation settings diff --git a/modules/translation/i18n/dummy.go b/modules/translation/i18n/dummy.go index fe15c250f4..861672c619 100644 --- a/modules/translation/i18n/dummy.go +++ b/modules/translation/i18n/dummy.go @@ -22,20 +22,7 @@ func (k *KeyLocale) HasKey(trKey string) bool { // TrHTML implements Locale. func (k *KeyLocale) TrHTML(trKey string, trArgs ...any) template.HTML { - args := slices.Clone(trArgs) - for i, v := range args { - switch v := v.(type) { - case nil, bool, int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64, template.HTML: - // for most basic types (including template.HTML which is safe), just do nothing and use it - case string: - args[i] = template.HTMLEscapeString(v) - case fmt.Stringer: - args[i] = template.HTMLEscapeString(v.String()) - default: - args[i] = template.HTMLEscapeString(fmt.Sprint(v)) - } - } - return template.HTML(k.TrString(trKey, args...)) + return template.HTML(k.TrString(trKey, PrepareArgsForHTML(trArgs...)...)) } // TrString implements Locale. @@ -43,6 +30,11 @@ func (k *KeyLocale) TrString(trKey string, trArgs ...any) string { return FormatDummy(trKey, trArgs...) } +// TrPluralString implements Locale. +func (k *KeyLocale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { + return template.HTML(FormatDummy(trKey, PrepareArgsForHTML(trArgs...)...)) +} + func FormatDummy(trKey string, args ...any) string { if len(args) == 0 { return fmt.Sprintf("(%s)", trKey) diff --git a/modules/translation/i18n/errors.go b/modules/translation/i18n/errors.go index 7f64ccf908..ee9436a8f7 100644 --- a/modules/translation/i18n/errors.go +++ b/modules/translation/i18n/errors.go @@ -8,6 +8,8 @@ import ( ) var ( - ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist} - ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument} + ErrLocaleAlreadyExist = util.SilentWrap{Message: "lang already exists", Err: util.ErrAlreadyExist} + ErrLocaleDoesNotExist = util.SilentWrap{Message: "lang does not exist", Err: util.ErrNotExist} + ErrTranslationDoesNotExist = util.SilentWrap{Message: "translation does not exist", Err: util.ErrNotExist} + ErrUncertainArguments = util.SilentWrap{Message: "arguments to i18n should not contain uncertain slices", Err: util.ErrInvalidArgument} ) diff --git a/modules/translation/i18n/i18n.go b/modules/translation/i18n/i18n.go index 1555cd961e..e447502a3b 100644 --- a/modules/translation/i18n/i18n.go +++ b/modules/translation/i18n/i18n.go @@ -8,11 +8,28 @@ import ( "io" ) +type ( + PluralFormIndex uint8 + PluralFormRule func(int64) PluralFormIndex +) + +const ( + PluralFormZero PluralFormIndex = iota + PluralFormOne + PluralFormTwo + PluralFormFew + PluralFormMany + PluralFormOther +) + var DefaultLocales = NewLocaleStore() type Locale interface { // TrString translates a given key and arguments for a language TrString(trKey string, trArgs ...any) string + // TrPluralString translates a given pluralized key and arguments for a language. + // This function returns an error if new-style support for the given key is not available. + TrPluralString(count any, trKey string, trArgs ...any) template.HTML // TrHTML translates a given key and arguments for a language, string arguments are escaped to HTML TrHTML(trKey string, trArgs ...any) template.HTML // HasKey reports if a locale has a translation for a given key @@ -31,8 +48,10 @@ type LocaleStore interface { Locale(langName string) (Locale, bool) // HasLang returns whether a given language is present in the store HasLang(langName string) bool - // AddLocaleByIni adds a new language to the store - AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error + // AddLocaleByIni adds a new old-style language to the store + AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error + // AddLocaleByJSON adds new-style content to an existing language to the store + AddToLocaleFromJSON(langName string, source []byte) error } // ResetDefaultLocales resets the current default locales diff --git a/modules/translation/i18n/i18n_test.go b/modules/translation/i18n/i18n_test.go index 244f6ffbb3..41f85931aa 100644 --- a/modules/translation/i18n/i18n_test.go +++ b/modules/translation/i18n/i18n_test.go @@ -12,6 +12,26 @@ import ( "github.com/stretchr/testify/require" ) +var MockPluralRule PluralFormRule = func(n int64) PluralFormIndex { + if n == 0 { + return PluralFormZero + } + if n == 1 { + return PluralFormOne + } + if n >= 2 && n <= 4 { + return PluralFormFew + } + return PluralFormOther +} + +var MockPluralRuleEnglish PluralFormRule = func(n int64) PluralFormIndex { + if n == 1 { + return PluralFormOne + } + return PluralFormOther +} + func TestLocaleStore(t *testing.T) { testData1 := []byte(` .dot.name = Dot Name @@ -27,11 +47,48 @@ fmt = %[2]s %[1]s [section] sub = Changed Sub String +commits = fallback value for commits +`) + + testDataJSON2 := []byte(` +{ + "section.json": "the JSON is %s", + "section.commits": { + "one": "one %d commit", + "few": "some %d commits", + "other": "lots of %d commits" + }, + "section.incomplete": { + "few": "some %d objects (translated)" + }, + "nested": { + "outer": { + "inner": { + "json": "Hello World", + "issue": { + "one": "one %d issue", + "few": "some %d issues", + "other": "lots of %d issues" + } + } + } + } +} +`) + testDataJSON1 := []byte(` +{ + "section.incomplete": { + "one": "[untranslated] some %d object", + "other": "[untranslated] some %d objects" + } +} `) ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, nil)) - require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", testData2, nil)) + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRuleEnglish, testData1, nil)) + require.NoError(t, ls.AddLocaleByIni("lang2", "Lang2", MockPluralRule, testData2, nil)) + require.NoError(t, ls.AddToLocaleFromJSON("lang1", testDataJSON1)) + require.NoError(t, ls.AddToLocaleFromJSON("lang2", testDataJSON2)) ls.SetDefaultLang("lang1") lang1, _ := ls.Locale("lang1") @@ -56,6 +113,45 @@ sub = Changed Sub String result2 := lang2.TrHTML("section.mixed", "a&b") assert.EqualValues(t, `test value; a&b`, result2) + result = lang2.TrString("section.json", "valid") + assert.Equal(t, "the JSON is valid", result) + + result = lang2.TrString("nested.outer.inner.json") + assert.Equal(t, "Hello World", result) + + result = lang2.TrString("section.commits") + assert.Equal(t, "lots of %d commits", result) + + result2 = lang2.TrPluralString(1, "section.commits", 1) + assert.EqualValues(t, "one 1 commit", result2) + + result2 = lang2.TrPluralString(3, "section.commits", 3) + assert.EqualValues(t, "some 3 commits", result2) + + result2 = lang2.TrPluralString(8, "section.commits", 8) + assert.EqualValues(t, "lots of 8 commits", result2) + + result2 = lang2.TrPluralString(0, "section.commits") + assert.EqualValues(t, "section.commits", result2) + + result2 = lang2.TrPluralString(1, "nested.outer.inner.issue", 1) + assert.EqualValues(t, "one 1 issue", result2) + + result2 = lang2.TrPluralString(3, "nested.outer.inner.issue", 3) + assert.EqualValues(t, "some 3 issues", result2) + + result2 = lang2.TrPluralString(9, "nested.outer.inner.issue", 9) + assert.EqualValues(t, "lots of 9 issues", result2) + + result2 = lang2.TrPluralString(3, "section.incomplete", 3) + assert.EqualValues(t, "some 3 objects (translated)", result2) + + result2 = lang2.TrPluralString(1, "section.incomplete", 1) + assert.EqualValues(t, "[untranslated] some 1 object", result2) + + result2 = lang2.TrPluralString(7, "section.incomplete", 7) + assert.EqualValues(t, "[untranslated] some 7 objects", result2) + langs, descs := ls.ListLangNameDesc() assert.ElementsMatch(t, []string{"lang1", "lang2"}, langs) assert.ElementsMatch(t, []string{"Lang1", "Lang2"}, descs) @@ -77,7 +173,7 @@ c=22 `) ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", testData1, testData2)) + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, testData1, testData2)) lang1, _ := ls.Locale("lang1") assert.Equal(t, "11", lang1.TrString("a")) assert.Equal(t, "21", lang1.TrString("b")) @@ -118,7 +214,7 @@ func (e *errorPointerReceiver) Error() string { func TestLocaleWithTemplate(t *testing.T) { ls := NewLocaleStore() - require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", []byte(`key=%s`), nil)) + require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, []byte(`key=%s`), nil)) lang1, _ := ls.Locale("lang1") tmpl := template.New("test").Funcs(template.FuncMap{"tr": lang1.TrHTML}) @@ -181,7 +277,7 @@ func TestLocaleStoreQuirks(t *testing.T) { for _, testData := range testDataList { ls := NewLocaleStore() - err := ls.AddLocaleByIni("lang1", "Lang1", []byte("a="+testData.in), nil) + err := ls.AddLocaleByIni("lang1", "Lang1", nil, []byte("a="+testData.in), nil) lang1, _ := ls.Locale("lang1") require.NoError(t, err, testData.hint) assert.Equal(t, testData.out, lang1.TrString("a"), testData.hint) diff --git a/modules/translation/i18n/localestore.go b/modules/translation/i18n/localestore.go index 0e6ddab401..e80b2592ae 100644 --- a/modules/translation/i18n/localestore.go +++ b/modules/translation/i18n/localestore.go @@ -8,8 +8,10 @@ import ( "html/template" "slices" + "code.gitea.io/gitea/modules/json" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" + "code.gitea.io/gitea/modules/util" ) // This file implements the static LocaleStore that will not watch for changes @@ -18,6 +20,9 @@ type locale struct { store *localeStore langName string idxToMsgMap map[int]string // the map idx is generated by store's trKeyToIdxMap + + newStyleMessages map[string]string + pluralRule PluralFormRule } var _ Locale = (*locale)(nil) @@ -38,8 +43,19 @@ func NewLocaleStore() LocaleStore { return &localeStore{localeMap: make(map[string]*locale), trKeyToIdxMap: make(map[string]int)} } +const ( + PluralFormSeparator string = "\036" +) + +// A note about pluralization rules. +// go-i18n supports plural rules in theory. +// In practice, it relies on another library that hardcodes a list of common languages +// and their plural rules, and does not support languages not hardcoded there. +// So we pretend that all languages are English and use our own function to extract +// the correct plural form for a given count and language. + // AddLocaleByIni adds locale by ini into the store -func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, moreSource []byte) error { +func (store *localeStore) AddLocaleByIni(langName, langDesc string, pluralRule PluralFormRule, source, moreSource []byte) error { if _, ok := store.localeMap[langName]; ok { return ErrLocaleAlreadyExist } @@ -47,7 +63,7 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more store.langNames = append(store.langNames, langName) store.langDescs = append(store.langDescs, langDesc) - l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string)} + l := &locale{store: store, langName: langName, idxToMsgMap: make(map[int]string), pluralRule: pluralRule, newStyleMessages: make(map[string]string)} store.localeMap[l.langName] = l iniFile, err := setting.NewConfigProviderForLocale(source, moreSource) @@ -78,6 +94,98 @@ func (store *localeStore) AddLocaleByIni(langName, langDesc string, source, more return nil } +func RecursivelyAddTranslationsFromJSON(locale *locale, object map[string]any, prefix string) error { + for key, value := range object { + var fullkey string + if prefix != "" { + fullkey = prefix + "." + key + } else { + fullkey = key + } + + switch v := value.(type) { + case string: + // Check whether we are adding a plural form to the parent object, or a new nested JSON object. + + if key == "zero" || key == "one" || key == "two" || key == "few" || key == "many" { + locale.newStyleMessages[prefix+PluralFormSeparator+key] = v + } else if key == "other" { + locale.newStyleMessages[prefix] = v + } else { + locale.newStyleMessages[fullkey] = v + } + + case map[string]any: + err := RecursivelyAddTranslationsFromJSON(locale, v, fullkey) + if err != nil { + return err + } + + case nil: + default: + return fmt.Errorf("Unrecognized JSON value '%s'", value) + } + } + + return nil +} + +func (store *localeStore) AddToLocaleFromJSON(langName string, source []byte) error { + locale, ok := store.localeMap[langName] + if !ok { + return ErrLocaleDoesNotExist + } + + var result map[string]any + if err := json.Unmarshal(source, &result); err != nil { + return err + } + + return RecursivelyAddTranslationsFromJSON(locale, result, "") +} + +func (l *locale) LookupNewStyleMessage(trKey string) string { + if msg, ok := l.newStyleMessages[trKey]; ok { + return msg + } + return "" +} + +func (l *locale) LookupPlural(trKey string, count any) string { + n, err := util.ToInt64(count) + if err != nil { + log.Error("Invalid plural count '%s'", count) + return "" + } + + pluralForm := l.pluralRule(n) + suffix := "" + switch pluralForm { + case PluralFormZero: + suffix = PluralFormSeparator + "zero" + case PluralFormOne: + suffix = PluralFormSeparator + "one" + case PluralFormTwo: + suffix = PluralFormSeparator + "two" + case PluralFormFew: + suffix = PluralFormSeparator + "few" + case PluralFormMany: + suffix = PluralFormSeparator + "many" + case PluralFormOther: + // No suffix for the "other" string. + default: + log.Error("Invalid plural form index %d for count %d", pluralForm, count) + return "" + } + + if result, ok := l.newStyleMessages[trKey+suffix]; ok { + return result + } + + log.Error("Missing translation for plural form index %d for count %d", pluralForm, count) + return "" +} + func (store *localeStore) HasLang(langName string) bool { _, ok := store.localeMap[langName] return ok @@ -113,22 +221,37 @@ func (store *localeStore) Close() error { func (l *locale) TrString(trKey string, trArgs ...any) string { format := trKey - idx, ok := l.store.trKeyToIdxMap[trKey] - found := false - if ok { - if msg, ok := l.idxToMsgMap[idx]; ok { - format = msg // use the found translation - found = true - } else if def, ok := l.store.localeMap[l.store.defaultLang]; ok { - // try to use default locale's translation - if msg, ok := def.idxToMsgMap[idx]; ok { - format = msg + if msg := l.LookupNewStyleMessage(trKey); msg != "" { + format = msg + } else { + // First fallback: old-style translation + idx, ok := l.store.trKeyToIdxMap[trKey] + found := false + if ok { + if msg, ok := l.idxToMsgMap[idx]; ok { + format = msg // use the found translation found = true } } - } - if !found { - log.Error("Missing translation %q", trKey) + + if !found { + // Second fallback: new-style default language + if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok { + if msg := defaultLang.LookupNewStyleMessage(trKey); msg != "" { + format = msg + } else { + // Third fallback: old-style default language + if msg, ok := defaultLang.idxToMsgMap[idx]; ok { + format = msg + found = true + } + } + } + + if !found { + log.Error("Missing translation %q", trKey) + } + } } msg, err := Format(format, trArgs...) @@ -138,7 +261,7 @@ func (l *locale) TrString(trKey string, trArgs ...any) string { return msg } -func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { +func PrepareArgsForHTML(trArgs ...any) []any { args := slices.Clone(trArgs) for i, v := range args { switch v := v.(type) { @@ -152,7 +275,30 @@ func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { args[i] = template.HTMLEscapeString(fmt.Sprint(v)) } } - return template.HTML(l.TrString(trKey, args...)) + return args +} + +func (l *locale) TrHTML(trKey string, trArgs ...any) template.HTML { + return template.HTML(l.TrString(trKey, PrepareArgsForHTML(trArgs...)...)) +} + +func (l *locale) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { + message := l.LookupPlural(trKey, count) + + if message == "" { + if defaultLang, ok := l.store.localeMap[l.store.defaultLang]; ok { + message = defaultLang.LookupPlural(trKey, count) + } + if message == "" { + message = trKey + } + } + + message, err := Format(message, PrepareArgsForHTML(trArgs...)...) + if err != nil { + log.Error("Error whilst formatting %q in %s: %v", trKey, l.langName, err) + } + return template.HTML(message) } // HasKey returns whether a key is present in this locale or not diff --git a/modules/translation/mock.go b/modules/translation/mock.go index fe3a1502ea..4d9acce26f 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) TrPluralString(count any, trKey string, trArgs ...any) template.HTML { + return template.HTML(trKey) +} + func (l MockLocale) TrSize(s int64) ReadableSize { return ReadableSize{fmt.Sprint(s), ""} } diff --git a/modules/translation/plural_rules.go b/modules/translation/plural_rules.go new file mode 100644 index 0000000000..b8c00ceef7 --- /dev/null +++ b/modules/translation/plural_rules.go @@ -0,0 +1,253 @@ +// Copyright 2024 The Forgejo Authors. All rights reserved. +// SPDX-License-Identifier: MIT + +// Some useful links: +// https://www.unicode.org/cldr/charts/46/supplemental/language_plural_rules.html +// https://translate.codeberg.org/languages/$LANGUAGE_CODE/#information +// https://github.com/WeblateOrg/language-data/blob/main/languages.csv +// Note that in some cases there is ambiguity about the correct form for a given language. In this case, ask the locale's translators. + +package translation + +import ( + "strings" + + "code.gitea.io/gitea/modules/log" + "code.gitea.io/gitea/modules/translation/i18n" +) + +// The constants refer to indices below in `PluralRules` and also in i18n.js, keep them in sync! +const ( + PluralRuleDefault = 0 + PluralRuleBengali = 1 + PluralRuleIcelandic = 2 + PluralRuleFilipino = 3 + PluralRuleOneForm = 4 + PluralRuleCzech = 5 + PluralRuleRussian = 6 + PluralRulePolish = 7 + PluralRuleLatvian = 8 + PluralRuleLithuanian = 9 + PluralRuleFrench = 10 + PluralRuleCatalan = 11 + PluralRuleSlovenian = 12 + PluralRuleArabic = 13 +) + +func GetPluralRuleImpl(langName string) int { + // First, check for languages with country-specific plural rules. + switch langName { + case "pt-BR": + return PluralRuleFrench + + case "pt-PT": + return PluralRuleCatalan + + default: + break + } + + // Remove the country portion of the locale name. + langName = strings.Split(strings.Split(langName, "_")[0], "-")[0] + + // When adding a new language not in the list, add its plural rule definition here. + switch langName { + case "en", "aa", "ab", "abr", "ada", "ae", "aeb", "af", "afh", "aii", "ain", "akk", "ale", "aln", "alt", "ami", "an", "ang", "anp", "apc", "arc", "arp", "arq", "arw", "arz", "asa", "ast", "av", "avk", "awa", "ayc", "az", "azb", "ba", "bal", "ban", "bar", "bas", "bbc", "bci", "bej", "bem", "ber", "bew", "bez", "bg", "bgc", "bgn", "bhb", "bhi", "bi", "bik", "bin", "bjj", "bjn", "bla", "bnt", "bqi", "bra", "brb", "brh", "brx", "bua", "bug", "bum", "byn", "cad", "cak", "car", "ce", "cgg", "ch", "chb", "chg", "chk", "chm", "chn", "cho", "chp", "chr", "chy", "ckb", "co", "cop", "cpe", "cpf", "cr", "crp", "cu", "cv", "da", "dak", "dar", "dcc", "de", "del", "den", "dgr", "din", "dje", "dnj", "dnk", "dru", "dry", "dua", "dum", "dv", "dyu", "ee", "efi", "egl", "egy", "eka", "el", "elx", "enm", "eo", "et", "eu", "ewo", "ext", "fan", "fat", "fbl", "ffm", "fi", "fj", "fo", "fon", "frk", "frm", "fro", "frr", "frs", "fuq", "fur", "fuv", "fvr", "fy", "gaa", "gay", "gba", "gbm", "gez", "gil", "gl", "glk", "gmh", "gn", "goh", "gom", "gon", "gor", "got", "grb", "gsw", "guc", "gum", "gur", "guz", "gwi", "ha", "hai", "haw", "haz", "hil", "hit", "hmn", "hnd", "hne", "hno", "ho", "hoc", "hoj", "hrx", "ht", "hu", "hup", "hus", "hz", "ia", "iba", "ibb", "ie", "ik", "ilo", "inh", "io", "jam", "jgo", "jmc", "jpr", "jrb", "ka", "kaa", "kac", "kaj", "kam", "kaw", "kbd", "kcg", "kfr", "kfy", "kg", "kha", "khn", "kho", "ki", "kj", "kk", "kkj", "kl", "kln", "kmb", "kmr", "kok", "kpe", "kr", "krc", "kri", "krl", "kru", "ks", "ksb", "ku", "kum", "kut", "kv", "kxm", "ky", "la", "lad", "laj", "lam", "lb", "lez", "lfn", "lg", "li", "lij", "ljp", "lki", "lmn", "lmo", "lol", "loz", "lrc", "lu", "lua", "lui", "lun", "luo", "lus", "luy", "luz", "mad", "mag", "mai", "mak", "man", "mas", "mdf", "mdh", "mdr", "men", "mer", "mfa", "mga", "mgh", "mgo", "mh", "mhr", "mic", "min", "mjw", "ml", "mn", "mnc", "mni", "mnw", "moe", "moh", "mos", "mr", "mrh", "mtr", "mus", "mwk", "mwl", "mwr", "mxc", "myv", "myx", "mzn", "na", "nah", "nap", "nb", "nd", "ndc", "nds", "ne", "new", "ng", "ngl", "nia", "nij", "niu", "nl", "nn", "nnh", "nod", "noe", "nog", "non", "nr", "nuk", "nv", "nwc", "ny", "nym", "nyn", "nyo", "nzi", "oj", "om", "or", "os", "ota", "otk", "ovd", "pag", "pal", "pam", "pap", "pau", "pbb", "pdt", "peo", "phn", "pi", "pms", "pon", "pro", "ps", "pwn", "qu", "quc", "qug", "qya", "raj", "rap", "rar", "rcf", "rej", "rhg", "rif", "rkt", "rm", "rmt", "rn", "rng", "rof", "rom", "rue", "rup", "rw", "rwk", "sad", "sai", "sam", "saq", "sas", "sc", "sck", "sco", "sd", "sdh", "sef", "seh", "sel", "sga", "sgn", "sgs", "shn", "sid", "sjd", "skr", "sm", "sml", "sn", "snk", "so", "sog", "sou", "sq", "srn", "srr", "ss", "ssy", "st", "suk", "sus", "sux", "sv", "sw", "swg", "swv", "sxu", "syc", "syl", "syr", "szy", "ta", "tay", "tcy", "te", "tem", "teo", "ter", "tet", "tig", "tiv", "tk", "tkl", "tli", "tly", "tmh", "tn", "tog", "tr", "trv", "ts", "tsg", "tsi", "tsj", "tts", "tum", "tvl", "tw", "ty", "tyv", "tzj", "tzl", "udm", "ug", "uga", "umb", "und", "unr", "ur", "uz", "vai", "ve", "vls", "vmf", "vmw", "vo", "vot", "vro", "vun", "wae", "wal", "war", "was", "wbq", "wbr", "wep", "wtm", "xal", "xh", "xnr", "xog", "yao", "yap", "yi", "yua", "za", "zap", "zbl", "zen", "zgh", "zun", "zza": + return PluralRuleDefault + + case "ach", "ady", "ak", "am", "arn", "as", "bh", "bho", "bn", "csw", "doi", "fa", "ff", "frc", "frp", "gu", "gug", "gun", "guw", "hi", "hy", "kab", "kn", "ln", "mfe", "mg", "mi", "mia", "nso", "oc", "pa", "pcm", "pt", "qdt", "qtp", "si", "tg", "ti", "wa", "zu": + return PluralRuleBengali + + case "is": + return PluralRuleIcelandic + + case "fil": + return PluralRuleFilipino + + case "ace", "ay", "bm", "bo", "cdo", "cpx", "crh", "dz", "gan", "hak", "hnj", "hsn", "id", "ig", "ii", "ja", "jbo", "jv", "kde", "kea", "km", "ko", "kos", "lkt", "lo", "lzh", "ms", "my", "nan", "nqo", "osa", "sah", "ses", "sg", "son", "su", "th", "tlh", "to", "tok", "tpi", "tt", "vi", "wo", "wuu", "yo", "yue", "zh": + return PluralRuleOneForm + + case "cpp", "cs", "sk": + return PluralRuleCzech + + case "be", "bs", "cnr", "hr", "ru", "sr", "uk", "wen": + return PluralRuleRussian + + case "csb", "pl", "szl": + return PluralRulePolish + + case "lv", "prg": + return PluralRuleLatvian + + case "lt": + return PluralRuleLithuanian + + case "fr": + return PluralRuleFrench + + case "ca", "es", "it": + return PluralRuleCatalan + + case "sl": + return PluralRuleSlovenian + + case "ar": + return PluralRuleArabic + + default: + break + } + + log.Error("No plural rule defined for language %s", langName) + return PluralRuleDefault +} + +var PluralRules = []i18n.PluralFormRule{ + // [ 0] Common 2-form, e.g. English, German + func(n int64) i18n.PluralFormIndex { + if n != 1 { + return i18n.PluralFormOther + } + return i18n.PluralFormOne + }, + + // [ 1] Bengali + func(n int64) i18n.PluralFormIndex { + if n > 1 { + return i18n.PluralFormOther + } + return i18n.PluralFormOne + }, + + // [ 2] Icelandic + func(n int64) i18n.PluralFormIndex { + if n%10 != 1 || n%100 == 11 { + return i18n.PluralFormOther + } + return i18n.PluralFormOne + }, + + // [ 3] Filipino + func(n int64) i18n.PluralFormIndex { + if n != 1 && n != 2 && n != 3 && (n%10 == 4 || n%10 == 6 || n%10 == 9) { + return i18n.PluralFormOther + } + return i18n.PluralFormOne + }, + + // [ 4] OneForm + func(n int64) i18n.PluralFormIndex { + return i18n.PluralFormOther + }, + + // [ 5] Czech + func(n int64) i18n.PluralFormIndex { + if n == 1 { + return i18n.PluralFormOne + } + if n >= 2 && n <= 4 { + return i18n.PluralFormFew + } + return i18n.PluralFormOther + }, + + // [ 6] Russian + func(n int64) i18n.PluralFormIndex { + if n%10 == 1 && n%100 != 11 { + return i18n.PluralFormOne + } + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { + return i18n.PluralFormFew + } + return i18n.PluralFormMany + }, + + // [ 7] Polish + func(n int64) i18n.PluralFormIndex { + if n == 1 { + return i18n.PluralFormOne + } + if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) { + return i18n.PluralFormFew + } + return i18n.PluralFormMany + }, + + // [ 8] Latvian + func(n int64) i18n.PluralFormIndex { + if n%10 == 0 || n%100 >= 11 && n%100 <= 19 { + return i18n.PluralFormZero + } + if n%10 == 1 && n%100 != 11 { + return i18n.PluralFormOne + } + return i18n.PluralFormOther + }, + + // [ 9] Lithuanian + func(n int64) i18n.PluralFormIndex { + if n%10 == 1 && (n%100 < 11 || n%100 > 19) { + return i18n.PluralFormOne + } + if n%10 >= 2 && n%10 <= 9 && (n%100 < 11 || n%100 > 19) { + return i18n.PluralFormFew + } + return i18n.PluralFormMany + }, + + // [10] French + func(n int64) i18n.PluralFormIndex { + if n == 0 || n == 1 { + return i18n.PluralFormOne + } + if n != 0 && n%1000000 == 0 { + return i18n.PluralFormMany + } + return i18n.PluralFormOther + }, + + // [11] Catalan + func(n int64) i18n.PluralFormIndex { + if n == 1 { + return i18n.PluralFormOne + } + if n != 0 && n%1000000 == 0 { + return i18n.PluralFormMany + } + return i18n.PluralFormOther + }, + + // [12] Slovenian + func(n int64) i18n.PluralFormIndex { + if n%100 == 1 { + return i18n.PluralFormOne + } + if n%100 == 2 { + return i18n.PluralFormTwo + } + if n%100 == 3 || n%100 == 4 { + return i18n.PluralFormFew + } + return i18n.PluralFormOther + }, + + // [13] Arabic + func(n int64) i18n.PluralFormIndex { + if n == 0 { + return i18n.PluralFormZero + } + if n == 1 { + return i18n.PluralFormOne + } + if n == 2 { + return i18n.PluralFormTwo + } + if n%100 >= 3 && n%100 <= 10 { + return i18n.PluralFormFew + } + if n%100 >= 11 { + return i18n.PluralFormMany + } + return i18n.PluralFormOther + }, +} diff --git a/modules/translation/translation.go b/modules/translation/translation.go index 6687d3d817..7d1c627c84 100644 --- a/modules/translation/translation.go +++ b/modules/translation/translation.go @@ -32,6 +32,9 @@ type Locale interface { TrString(string, ...any) string Tr(key string, args ...any) template.HTML + // New-style pluralized strings + TrPluralString(count any, trKey string, trArgs ...any) template.HTML + // Old-style pseudo-pluralized strings, deprecated TrN(cnt any, key1, keyN string, args ...any) template.HTML TrSize(size int64) ReadableSize @@ -100,8 +103,17 @@ func InitLocales(ctx context.Context) { } key := "locale_" + setting.Langs[i] + ".ini" - if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], localeDataBase, localeData[key]); err != nil { - log.Error("Failed to set messages to %s: %v", setting.Langs[i], err) + if err = i18n.DefaultLocales.AddLocaleByIni(setting.Langs[i], setting.Names[i], PluralRules[GetPluralRuleImpl(setting.Langs[i])], localeDataBase, localeData[key]); err != nil { + log.Error("Failed to set old-style messages to %s: %v", setting.Langs[i], err) + } + + key = "locale_next/locale_" + setting.Langs[i] + ".json" + if bytes, err := options.AssetFS().ReadFile(key); err == nil { + if err = i18n.DefaultLocales.AddToLocaleFromJSON(setting.Langs[i], bytes); err != nil { + log.Error("Failed to add new-style messages to %s: %v", setting.Langs[i], err) + } + } else { + log.Error("Failed to open new-style messages for %s: %v", setting.Langs[i], err) } } if len(setting.Langs) != 0 { diff --git a/modules/translation/translation_test.go b/modules/translation/translation_test.go index bffbb155ca..5b3eefb355 100644 --- a/modules/translation/translation_test.go +++ b/modules/translation/translation_test.go @@ -48,3 +48,111 @@ func TestPrettyNumber(t *testing.T) { assert.EqualValues(t, "1,000,000", l.PrettyNumber(1000000)) assert.EqualValues(t, "1,000,000.1", l.PrettyNumber(1000000.1)) } + +func TestGetPluralRule(t *testing.T) { + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en")) + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en-US")) + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("en_UK")) + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("nds")) + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("de-DE")) + + assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("zh")) + assert.Equal(t, PluralRuleOneForm, GetPluralRuleImpl("ja")) + + assert.Equal(t, PluralRuleBengali, GetPluralRuleImpl("bn")) + + assert.Equal(t, PluralRuleIcelandic, GetPluralRuleImpl("is")) + + assert.Equal(t, PluralRuleFilipino, GetPluralRuleImpl("fil")) + + assert.Equal(t, PluralRuleCzech, GetPluralRuleImpl("cs")) + + assert.Equal(t, PluralRuleRussian, GetPluralRuleImpl("ru")) + + assert.Equal(t, PluralRulePolish, GetPluralRuleImpl("pl")) + + assert.Equal(t, PluralRuleLatvian, GetPluralRuleImpl("lv")) + + assert.Equal(t, PluralRuleLithuanian, GetPluralRuleImpl("lt")) + + assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("fr")) + + assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("ca")) + + assert.Equal(t, PluralRuleSlovenian, GetPluralRuleImpl("sl")) + + assert.Equal(t, PluralRuleArabic, GetPluralRuleImpl("ar")) + + assert.Equal(t, PluralRuleCatalan, GetPluralRuleImpl("pt-PT")) + assert.Equal(t, PluralRuleFrench, GetPluralRuleImpl("pt-BR")) + + assert.Equal(t, PluralRuleDefault, GetPluralRuleImpl("invalid")) +} + +func TestApplyPluralRule(t *testing.T) { + testCases := []struct { + expect i18n.PluralFormIndex + pluralRule int + values []int64 + }{ + {i18n.PluralFormOne, PluralRuleDefault, []int64{1}}, + {i18n.PluralFormOther, PluralRuleDefault, []int64{0, 2, 10, 256}}, + + {i18n.PluralFormOther, PluralRuleOneForm, []int64{0, 1, 2}}, + + {i18n.PluralFormOne, PluralRuleBengali, []int64{0, 1}}, + {i18n.PluralFormOther, PluralRuleBengali, []int64{2, 10, 256}}, + + {i18n.PluralFormOne, PluralRuleIcelandic, []int64{1, 21, 31}}, + {i18n.PluralFormOther, PluralRuleIcelandic, []int64{0, 2, 11, 15, 256}}, + + {i18n.PluralFormOne, PluralRuleFilipino, []int64{0, 1, 2, 3, 5, 7, 8, 10, 11, 12, 257}}, + {i18n.PluralFormOther, PluralRuleFilipino, []int64{4, 6, 9, 14, 16, 19, 256}}, + + {i18n.PluralFormOne, PluralRuleCzech, []int64{1}}, + {i18n.PluralFormFew, PluralRuleCzech, []int64{2, 3, 4}}, + {i18n.PluralFormOther, PluralRuleCzech, []int64{5, 0, 12, 78, 254}}, + + {i18n.PluralFormOne, PluralRuleRussian, []int64{1, 21, 31}}, + {i18n.PluralFormFew, PluralRuleRussian, []int64{2, 23, 34}}, + {i18n.PluralFormMany, PluralRuleRussian, []int64{0, 5, 11, 37, 111, 256}}, + + {i18n.PluralFormOne, PluralRulePolish, []int64{1}}, + {i18n.PluralFormFew, PluralRulePolish, []int64{2, 23, 34}}, + {i18n.PluralFormMany, PluralRulePolish, []int64{0, 5, 11, 21, 37, 256}}, + + {i18n.PluralFormZero, PluralRuleLatvian, []int64{0, 10, 11, 17}}, + {i18n.PluralFormOne, PluralRuleLatvian, []int64{1, 21, 71}}, + {i18n.PluralFormOther, PluralRuleLatvian, []int64{2, 7, 22, 23, 256}}, + + {i18n.PluralFormOne, PluralRuleLithuanian, []int64{1, 21, 31}}, + {i18n.PluralFormFew, PluralRuleLithuanian, []int64{2, 5, 9, 23, 34, 256}}, + {i18n.PluralFormMany, PluralRuleLithuanian, []int64{0, 10, 11, 18}}, + + {i18n.PluralFormOne, PluralRuleFrench, []int64{0, 1}}, + {i18n.PluralFormMany, PluralRuleFrench, []int64{1000000, 2000000}}, + {i18n.PluralFormOther, PluralRuleFrench, []int64{2, 4, 10, 256}}, + + {i18n.PluralFormOne, PluralRuleCatalan, []int64{1}}, + {i18n.PluralFormMany, PluralRuleCatalan, []int64{1000000, 2000000}}, + {i18n.PluralFormOther, PluralRuleCatalan, []int64{0, 2, 4, 10, 256}}, + + {i18n.PluralFormOne, PluralRuleSlovenian, []int64{1, 101, 201, 501}}, + {i18n.PluralFormTwo, PluralRuleSlovenian, []int64{2, 102, 202, 502}}, + {i18n.PluralFormFew, PluralRuleSlovenian, []int64{3, 103, 203, 503, 4, 104, 204, 504}}, + {i18n.PluralFormOther, PluralRuleSlovenian, []int64{0, 5, 11, 12, 20, 256}}, + + {i18n.PluralFormZero, PluralRuleArabic, []int64{0}}, + {i18n.PluralFormOne, PluralRuleArabic, []int64{1}}, + {i18n.PluralFormTwo, PluralRuleArabic, []int64{2}}, + {i18n.PluralFormFew, PluralRuleArabic, []int64{3, 4, 9, 10, 103, 104}}, + {i18n.PluralFormMany, PluralRuleArabic, []int64{11, 12, 13, 14, 17, 111, 256}}, + {i18n.PluralFormOther, PluralRuleArabic, []int64{100, 101, 102}}, + } + + for _, tc := range testCases { + for _, n := range tc.values { + assert.Equal(t, tc.expect, PluralRules[tc.pluralRule](n), "Testcase for plural rule %d, value %d", tc.pluralRule, n) + } + } +} diff --git a/options/locale/locale_en-US.ini b/options/locale/locale_en-US.ini index eaeab11a9d..501e98131e 100644 --- a/options/locale/locale_en-US.ini +++ b/options/locale/locale_en-US.ini @@ -190,7 +190,6 @@ commit_kind = Search commits... runner_kind = Search runners... no_results = No matching results found. issue_kind = Search issues... -milestone_kind = Search milestones... pull_kind = Search pulls... keyword_search_unavailable = Searching by keyword is currently not available. Please contact the site administrator. @@ -1887,10 +1886,6 @@ pulls.nothing_to_compare_have_tag = The selected branch/tag are equal. pulls.nothing_to_compare_and_allow_empty_pr = These branches are equal. This PR will be empty. pulls.has_pull_request = `A pull request between these branches already exists: %[2]s#%[3]d` pulls.create = Create pull request -pulls.title_desc_one = wants to merge %[1]d commit from %[2]s into %[3]s -pulls.title_desc_few = wants to merge %[1]d commits from %[2]s into %[3]s -pulls.merged_title_desc_one = merged %[1]d commit from %[2]s into %[3]s %[4]s -pulls.merged_title_desc_few = merged %[1]d commits from %[2]s into %[3]s %[4]s pulls.change_target_branch_at = `changed target branch from %s to %s %s` pulls.tab_conversation = Conversation pulls.tab_commits = Commits @@ -2648,7 +2643,6 @@ diff.git-notes.remove-header = Remove note diff.git-notes.remove-body = This note will be removed. diff.data_not_available = Diff content is not available diff.options_button = Diff options -diff.show_diff_stats = Show stats diff.download_patch = Download patch file diff.download_diff = Download diff file diff.show_split_view = Split view diff --git a/options/locale_next/locale_ar.json b/options/locale_next/locale_ar.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_ar.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_be.json b/options/locale_next/locale_be.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_be.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_bg.json b/options/locale_next/locale_bg.json new file mode 100644 index 0000000000..02144c8b38 --- /dev/null +++ b/options/locale_next/locale_bg.json @@ -0,0 +1,10 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "сля %[1]d подаване от %[2]s в %[3]s %[4]s", + "other": "сля %[1]d подавания от %[2]s в %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "иска да слее %[1]d подаване от %[2]s в %[3]s", + "other": "иска да слее %[1]d подавания от %[2]s в %[3]s" + } +} diff --git a/options/locale_next/locale_bn.json b/options/locale_next/locale_bn.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_bn.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_bs.json b/options/locale_next/locale_bs.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_bs.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_ca.json b/options/locale_next/locale_ca.json new file mode 100644 index 0000000000..c639c28a99 --- /dev/null +++ b/options/locale_next/locale_ca.json @@ -0,0 +1,3 @@ +{ + "search.milestone_kind": "Cerca fites..." +} diff --git a/options/locale_next/locale_cs-CZ.json b/options/locale_next/locale_cs-CZ.json new file mode 100644 index 0000000000..8d028b8367 --- /dev/null +++ b/options/locale_next/locale_cs-CZ.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "sloučil %[1]d commit z %[2]s do %[3]s %[4]s", + "other": "sloučil %[1]d commity z větve %[2]s do větve %[3]s před %[4]s" + }, + "repo.pulls.title_desc": { + "one": "žádá o sloučení %[1]d commitu z %[2]s do %[3]s", + "other": "chce sloučit %[1]d commity z větve %[2]s do %[3]s" + }, + "search.milestone_kind": "Hledat milníky..." +} diff --git a/options/locale_next/locale_da.json b/options/locale_next/locale_da.json new file mode 100644 index 0000000000..0c2c9a25ea --- /dev/null +++ b/options/locale_next/locale_da.json @@ -0,0 +1,3 @@ +{ + "search.milestone_kind": "Søg milepæle..." +} diff --git a/options/locale_next/locale_de-DE.json b/options/locale_next/locale_de-DE.json new file mode 100644 index 0000000000..f4a15ecdb9 --- /dev/null +++ b/options/locale_next/locale_de-DE.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "hat %[1]d Commit von %[2]s nach %[3]s %[4]s zusammengeführt", + "other": "hat %[1]d Commits von %[2]s nach %[3]s %[4]s zusammengeführt" + }, + "repo.pulls.title_desc": { + "one": "möchte %[1]d Commit von %[2]s nach %[3]s zusammenführen", + "other": "möchte %[1]d Commits von %[2]s nach %[3]s zusammenführen" + }, + "search.milestone_kind": "Meilensteine suchen …" +} diff --git a/options/locale_next/locale_el-GR.json b/options/locale_next/locale_el-GR.json new file mode 100644 index 0000000000..54ee504201 --- /dev/null +++ b/options/locale_next/locale_el-GR.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "συγχώνευσε %[1]d υποβολή από τον κλάδο %[2]s στον κλάδο %[3]s %[4]s", + "other": "συγχώνευσε %[1]d υποβολές από %[2]s σε %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": ": θα ήθελε να συγχωνεύσει %[1]d υποβολή από τον κλάδο %[2]s στον κλάδο %[3]s", + "other": "θέλει να συγχωνεύσει %[1]d υποβολές από %[2]s σε %[3]s" + }, + "search.milestone_kind": "Αναζήτηση ορόσημων..." +} diff --git a/options/locale_next/locale_en-US.json b/options/locale_next/locale_en-US.json new file mode 100644 index 0000000000..f8b2bcd0f6 --- /dev/null +++ b/options/locale_next/locale_en-US.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "merged %[1]d commit from %[2]s into %[3]s %[4]s", + "other": "merged %[1]d commits from %[2]s into %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "wants to merge %[1]d commit from %[2]s into %[3]s", + "other": "wants to merge %[1]d commits from %[2]s into %[3]s" + }, + "search.milestone_kind": "Search milestones..." +} diff --git a/options/locale_next/locale_eo.json b/options/locale_next/locale_eo.json new file mode 100644 index 0000000000..2c76a8562b --- /dev/null +++ b/options/locale_next/locale_eo.json @@ -0,0 +1,3 @@ +{ + "search.milestone_kind": "Serĉi celojn..." +} diff --git a/options/locale_next/locale_es-ES.json b/options/locale_next/locale_es-ES.json new file mode 100644 index 0000000000..e95e91f696 --- /dev/null +++ b/options/locale_next/locale_es-ES.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "fusionó %[1]d commit de %[2]s en %[3]s %[4]s", + "other": "fusionó %[1]d commits de %[2]s en %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "quiere fusionar %[1]d commit de %[2]s en %[3]s", + "other": "quiere fusionar %[1]d commits de %[2]s en %[3]s" + }, + "search.milestone_kind": "Buscar hitos…" +} diff --git a/options/locale_next/locale_et.json b/options/locale_next/locale_et.json new file mode 100644 index 0000000000..f8846a15b8 --- /dev/null +++ b/options/locale_next/locale_et.json @@ -0,0 +1,3 @@ +{ + "search.milestone_kind": "Otsi verstapostid..." +} diff --git a/options/locale_next/locale_fa-IR.json b/options/locale_next/locale_fa-IR.json new file mode 100644 index 0000000000..0a703d22d7 --- /dev/null +++ b/options/locale_next/locale_fa-IR.json @@ -0,0 +1,8 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "%[1]d کامیت ادغام شده از %[2]s به %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "other": "قصد ادغام %[1]d تغییر را از %[2]s به %[3]s دارد" + } +} diff --git a/options/locale_next/locale_fi-FI.json b/options/locale_next/locale_fi-FI.json new file mode 100644 index 0000000000..c4c7e1f7dc --- /dev/null +++ b/options/locale_next/locale_fi-FI.json @@ -0,0 +1,9 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "yhdistetty %[1]d committia lähteestä %[2]s kohteeseen %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "other": "haluaa yhdistää %[1]d committia lähteestä %[2]s kohteeseen %[3]s" + }, + "search.milestone_kind": "Etsi merkkipaaluja..." +} diff --git a/options/locale_next/locale_fil.json b/options/locale_next/locale_fil.json new file mode 100644 index 0000000000..9c57ef16cc --- /dev/null +++ b/options/locale_next/locale_fil.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "isinali ang %[1]d commit mula%[2]s patungong %[3]s %[4]s", + "other": "isinali ang %[1]d mga commit mula sa %[2]s patungong %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "hinihiling na isama ang %[1]d commit mula %[2]s patungong %[3]s", + "other": "hiniling na isama ang %[1]d mga commit mula sa %[2]s patungong %[3]s" + }, + "search.milestone_kind": "Maghanap ng mga milestone…" +} diff --git a/options/locale_next/locale_fr-FR.json b/options/locale_next/locale_fr-FR.json new file mode 100644 index 0000000000..173b10de21 --- /dev/null +++ b/options/locale_next/locale_fr-FR.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "fusionné %[1]d commit depuis %[2]s vers %[3]s %[4]s", + "other": "a fusionné %[1]d révision(s) à partir de %[2]s vers %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "veut fusionner %[1]d commit depuis %[2]s vers %[3]s", + "other": "souhaite fusionner %[1]d révision(s) depuis %[2]s vers %[3]s" + }, + "search.milestone_kind": "Recherche dans les jalons..." +} diff --git a/options/locale_next/locale_gl.json b/options/locale_next/locale_gl.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_gl.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_hi.json b/options/locale_next/locale_hi.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_hi.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_hu-HU.json b/options/locale_next/locale_hu-HU.json new file mode 100644 index 0000000000..60c8cfacd3 --- /dev/null +++ b/options/locale_next/locale_hu-HU.json @@ -0,0 +1,9 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "egyesítve %[1]d változás(ok) a %[2]s-ból %[3]s-ba %[4]s" + }, + "repo.pulls.title_desc": { + "other": "egyesíteni szeretné %[1]d változás(oka)t a(z) %[2]s-ból %[3]s-ba" + }, + "search.milestone_kind": "Mérföldkövek keresése..." +} diff --git a/options/locale_next/locale_id-ID.json b/options/locale_next/locale_id-ID.json new file mode 100644 index 0000000000..f2dac8114f --- /dev/null +++ b/options/locale_next/locale_id-ID.json @@ -0,0 +1,8 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "commit %[1]d telah digabungkan dari %[2]s menjadi %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "other": "ingin menggabungkan komit %[1]d dari %[2]s menuju %[3]s" + } +} diff --git a/options/locale_next/locale_is-IS.json b/options/locale_next/locale_is-IS.json new file mode 100644 index 0000000000..a92d924232 --- /dev/null +++ b/options/locale_next/locale_is-IS.json @@ -0,0 +1,5 @@ +{ + "repo.pulls.title_desc": { + "other": "vill sameina %[1]d framlög frá %[2]s í %[3]s" + } +} diff --git a/options/locale_next/locale_it-IT.json b/options/locale_next/locale_it-IT.json new file mode 100644 index 0000000000..ba90fa154f --- /dev/null +++ b/options/locale_next/locale_it-IT.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "ha fuso %[1]d commit da %[2]s in %[3]s %[4]s", + "other": "ha unito %[1]d commit da %[2]s a %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "vuole fondere %[1]d commit da %[2]s in %[3]s", + "other": "vuole unire %[1]d commit da %[2]s a %[3]s" + }, + "search.milestone_kind": "Ricerca tappe..." +} diff --git a/options/locale_next/locale_ja-JP.json b/options/locale_next/locale_ja-JP.json new file mode 100644 index 0000000000..f72d1a3fb6 --- /dev/null +++ b/options/locale_next/locale_ja-JP.json @@ -0,0 +1,9 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "が %[1]d 個のコミットを %[2]s から %[3]s へマージ %[4]s" + }, + "repo.pulls.title_desc": { + "other": "が %[2]s から %[3]s への %[1]d コミットのマージを希望しています" + }, + "search.milestone_kind": "マイルストーンを検索..." +} diff --git a/options/locale_next/locale_ko-KR.json b/options/locale_next/locale_ko-KR.json new file mode 100644 index 0000000000..2acaca6084 --- /dev/null +++ b/options/locale_next/locale_ko-KR.json @@ -0,0 +1,8 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "님이 %[2]s 에서 %[3]s 로 %[1]d 커밋을 %[4]s 병합함" + }, + "repo.pulls.title_desc": { + "other": "%[2]s 에서 %[3]s 로 %[1]d개의 커밋들을 병합하려함" + } +} diff --git a/options/locale_next/locale_lt.json b/options/locale_next/locale_lt.json new file mode 100644 index 0000000000..d81780a2ab --- /dev/null +++ b/options/locale_next/locale_lt.json @@ -0,0 +1,3 @@ +{ + "search.milestone_kind": "Ieškoti gairių..." +} diff --git a/options/locale_next/locale_lv-LV.json b/options/locale_next/locale_lv-LV.json new file mode 100644 index 0000000000..a16e3aaf8a --- /dev/null +++ b/options/locale_next/locale_lv-LV.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "iekļāva %[1]d iesūtījumu no %[2]s %[3]s %[4]s", + "other": "Iekļāva %[1]d iesūtījumus no %[2]s zarā %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "vēlas iekļaut %[1]d iesūtījumu no %[2]s %[3]s", + "other": "vēlas iekļaut %[1]d iesūtījumus no %[2]s zarā %[3]s" + }, + "search.milestone_kind": "Meklēt atskaites punktus..." +} diff --git a/options/locale_next/locale_ml-IN.json b/options/locale_next/locale_ml-IN.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_ml-IN.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_nb_NO.json b/options/locale_next/locale_nb_NO.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_nb_NO.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_nds.json b/options/locale_next/locale_nds.json new file mode 100644 index 0000000000..564302820a --- /dev/null +++ b/options/locale_next/locale_nds.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "hett %[1]d Kommitteren vun %[2]s na %[3]s %[4]s tosamenföhrt", + "other": "hett %[1]d Kommitterens vun %[2]s na %[3]s %[4]s tosamenföhrt" + }, + "repo.pulls.title_desc": { + "one": "will %[1]d Kommitteren vun %[2]s na %[3]s tosamenföhren", + "other": "will %[1]d Kommitterens vun %[2]s na %[3]s tosamenföhren" + }, + "search.milestone_kind": "In Markstenen söken …" +} diff --git a/options/locale_next/locale_nl-NL.json b/options/locale_next/locale_nl-NL.json new file mode 100644 index 0000000000..fbb78e9280 --- /dev/null +++ b/options/locale_next/locale_nl-NL.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "heeft %[1]d commit van %[2]s samengevoegd in %[3]s %[4]s", + "other": "heeft %[1]d commits samengevoegd van %[2]s naar %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "wilt %[1]d commit van %[2]s samenvoegen in %[3]s", + "other": "wilt %[1]d commits van %[2]s samenvoegen met %[3]s" + }, + "search.milestone_kind": "Zoek mijlpalen..." +} diff --git a/options/locale_next/locale_pl-PL.json b/options/locale_next/locale_pl-PL.json new file mode 100644 index 0000000000..2edcb573c4 --- /dev/null +++ b/options/locale_next/locale_pl-PL.json @@ -0,0 +1,9 @@ +{ + "repo.pulls.merged_title_desc": { + "many": "scala %[1]d commity/ów z %[2]s do %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "many": "chce scalić %[1]d commity/ów z %[2]s do %[3]s" + }, + "search.milestone_kind": "Wyszukaj kamienie milowe..." +} diff --git a/options/locale_next/locale_pt-BR.json b/options/locale_next/locale_pt-BR.json new file mode 100644 index 0000000000..4de44582a5 --- /dev/null +++ b/options/locale_next/locale_pt-BR.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "mesclou %[1]d commit de %[2]s em %[3]s %[4]s", + "other": "mesclou %[1]d commits de %[2]s em %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "quer mesclar %[1]d commit de %[2]s em %[3]s", + "other": "quer mesclar %[1]d commits de %[2]s em %[3]s" + }, + "search.milestone_kind": "Pesquisar marcos..." +} diff --git a/options/locale_next/locale_pt-PT.json b/options/locale_next/locale_pt-PT.json new file mode 100644 index 0000000000..cf72ef3e09 --- /dev/null +++ b/options/locale_next/locale_pt-PT.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "integrou %[1]d cometimento do ramo %[2]s no ramo %[3]s %[4]s", + "other": "integrou %[1]d cometimento(s) do ramo %[2]s no ramo %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "quer integrar %[1]d cometimento do ramo %[2]s no ramo %[3]s", + "other": "quer integrar %[1]d cometimento(s) do ramo %[2]s no ramo %[3]s" + }, + "search.milestone_kind": "Procurar etapas..." +} diff --git a/options/locale_next/locale_ru-RU.json b/options/locale_next/locale_ru-RU.json new file mode 100644 index 0000000000..36f54f405b --- /dev/null +++ b/options/locale_next/locale_ru-RU.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "слит %[1]d коммит из %[2]s в %[3]s %[4]s", + "many": "слито %[1]d коммит(ов) из %[2]s в %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "хочет влить %[1]d коммит из %[2]s в %[3]s", + "many": "хочет влить %[1]d коммит(ов) из %[2]s в %[3]s" + }, + "search.milestone_kind": "Найти этапы..." +} diff --git a/options/locale_next/locale_si-LK.json b/options/locale_next/locale_si-LK.json new file mode 100644 index 0000000000..0cdd44acd0 --- /dev/null +++ b/options/locale_next/locale_si-LK.json @@ -0,0 +1,8 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "මර්ජ්%[1]d සිට %[2]s දක්වා %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "other": "%[1]d සිට %[2]s දක්වා %[3]s" + } +} diff --git a/options/locale_next/locale_sk-SK.json b/options/locale_next/locale_sk-SK.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_sk-SK.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_sl.json b/options/locale_next/locale_sl.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_sl.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_sr-SP.json b/options/locale_next/locale_sr-SP.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_sr-SP.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_sv-SE.json b/options/locale_next/locale_sv-SE.json new file mode 100644 index 0000000000..de7099ad3a --- /dev/null +++ b/options/locale_next/locale_sv-SE.json @@ -0,0 +1,9 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "sammanfogade %[1]d incheckningar från %[2]s in i %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "other": "vill sammanfoga %[1]d incheckningar från s[2]s in i %[3]s" + }, + "search.milestone_kind": "Sök milstolpar..." +} diff --git a/options/locale_next/locale_tr-TR.json b/options/locale_next/locale_tr-TR.json new file mode 100644 index 0000000000..f98b339245 --- /dev/null +++ b/options/locale_next/locale_tr-TR.json @@ -0,0 +1,9 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "%[4]s %[2]s içindeki %[1]d işlemeyi %[3]s ile birleştirdi" + }, + "repo.pulls.title_desc": { + "other": "%[2]s içindeki %[1]d işlemeyi %[3]s ile birleştirmek istiyor" + }, + "search.milestone_kind": "Kilometre taşlarını ara..." +} diff --git a/options/locale_next/locale_uk-UA.json b/options/locale_next/locale_uk-UA.json new file mode 100644 index 0000000000..ce37b9bef3 --- /dev/null +++ b/options/locale_next/locale_uk-UA.json @@ -0,0 +1,11 @@ +{ + "repo.pulls.merged_title_desc": { + "one": "об'єднав %[1]d коміт з %[2]s в %[3]s %[4]s", + "many": "об'єднав %[1]d комітів з %[2]s в %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "one": "хоче об'єднати %[1]d коміт з %[2]s в %[3]s", + "many": "хоче об'єднати %[1]d комітів з %[2]s в %[3]s" + }, + "search.milestone_kind": "Шукати віхи..." +} diff --git a/options/locale_next/locale_vi.json b/options/locale_next/locale_vi.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_vi.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_yi.json b/options/locale_next/locale_yi.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/options/locale_next/locale_yi.json @@ -0,0 +1 @@ +{} diff --git a/options/locale_next/locale_zh-CN.json b/options/locale_next/locale_zh-CN.json new file mode 100644 index 0000000000..10b1c9a4be --- /dev/null +++ b/options/locale_next/locale_zh-CN.json @@ -0,0 +1,9 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "于 %[4]s 将 %[1]d 次代码提交从 %[2]s合并至 %[3]s" + }, + "repo.pulls.title_desc": { + "other": "请求将 %[1]d 次代码提交从 %[2]s 合并至 %[3]s" + }, + "search.milestone_kind": "搜索里程碑…" +} diff --git a/options/locale_next/locale_zh-HK.json b/options/locale_next/locale_zh-HK.json new file mode 100644 index 0000000000..6baf89e022 --- /dev/null +++ b/options/locale_next/locale_zh-HK.json @@ -0,0 +1,5 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "於 %[4]s 將 %[1]d 次代碼提交從 %[2]s合併至 %[3]s" + } +} diff --git a/options/locale_next/locale_zh-TW.json b/options/locale_next/locale_zh-TW.json new file mode 100644 index 0000000000..d04e04c264 --- /dev/null +++ b/options/locale_next/locale_zh-TW.json @@ -0,0 +1,9 @@ +{ + "repo.pulls.merged_title_desc": { + "other": "將 %[1]d 次提交從 %[2]s 合併至 %[3]s %[4]s" + }, + "repo.pulls.title_desc": { + "other": "請求將 %[1]d 次程式碼提交從 %[2]s 合併至 %[3]s" + }, + "search.milestone_kind": "搜尋里程碑..." +} diff --git a/package-lock.json b/package-lock.json index 223dfbd310..445721e70f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", "@github/quote-selection": "2.1.0", - "@github/relative-time-element": "4.4.4", + "@github/relative-time-element": "4.4.5", "@github/text-expander-element": "2.8.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", @@ -30,14 +30,14 @@ "htmx.org": "1.9.12", "idiomorph": "0.3.0", "jquery": "3.7.1", - "katex": "0.16.20", + "katex": "0.16.21", "mermaid": "11.4.1", "mini-css-extract-plugin": "2.9.2", "minimatch": "10.0.1", "monaco-editor": "0.52.2", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-loader": "8.1.1", "postcss-nesting": "13.0.1", "pretty-ms": "9.0.0", @@ -89,7 +89,7 @@ "happy-dom": "16.3.0", "license-checker-rseidelsohn": "4.4.2", "markdownlint-cli": "0.43.0", - "postcss-html": "1.7.0", + "postcss-html": "1.8.0", "stylelint": "16.12.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.6", @@ -2854,9 +2854,9 @@ "license": "MIT" }, "node_modules/@github/relative-time-element": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.4.tgz", - "integrity": "sha512-Oi8uOL8O+ZWLD7dHRWCkm2cudcTYtB3VyOYf9BtzCgDGm+OKomyOREtItNMtWl1dxvec62BTKErq36uy+RYxQg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@github/relative-time-element/-/relative-time-element-4.4.5.tgz", + "integrity": "sha512-9ejPtayBDIJfEU8x1fg/w2o5mahHkkp1SC6uObDtoKs4Gn+2a1vNK8XIiNDD8rMeEfpvDjydgSZZ+uk+7N0VsQ==", "license": "MIT" }, "node_modules/@github/text-expander-element": { @@ -10402,9 +10402,9 @@ "license": "MIT" }, "node_modules/katex": { - "version": "0.16.20", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.20.tgz", - "integrity": "sha512-jjuLaMGD/7P8jUTpdKhA9IoqnH+yMFB3sdAFtq5QdAqeP2PjiSbnC3EaguKPNtv6dXXanHxp1ckwvF4a86LBig==", + "version": "0.16.21", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.21.tgz", + "integrity": "sha512-XvqR7FgOHtWupfMiigNzmh+MgUVmDGU2kXZm899ZkPfcuoPuFxyHmXsgATDpFZDAXCI8tvinaVcDo8PIIJSo4A==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" @@ -11867,9 +11867,9 @@ } }, "node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", "funding": [ { "type": "opencollective", @@ -11886,7 +11886,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.7", + "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -11895,15 +11895,15 @@ } }, "node_modules/postcss-html": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.7.0.tgz", - "integrity": "sha512-MfcMpSUIaR/nNgeVS8AyvyDugXlADjN9AcV7e5rDfrF1wduIAGSkL4q2+wgrZgA3sHVAHLDO9FuauHhZYW2nBw==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-1.8.0.tgz", + "integrity": "sha512-5mMeb1TgLWoRKxZ0Xh9RZDfwUUIqRrcxO2uXO+Ezl1N5lqpCiSU5Gk6+1kZediBfBHFtPCdopr2UZ2SgUsKcgQ==", "dev": true, "license": "MIT", "dependencies": { "htmlparser2": "^8.0.0", "js-tokens": "^9.0.0", - "postcss": "^8.4.0", + "postcss": "^8.5.0", "postcss-safe-parser": "^6.0.0" }, "engines": { diff --git a/package.json b/package.json index 86b1b17029..95129444fb 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "@citation-js/plugin-software-formats": "0.6.1", "@github/markdown-toolbar-element": "2.2.3", "@github/quote-selection": "2.1.0", - "@github/relative-time-element": "4.4.4", + "@github/relative-time-element": "4.4.5", "@github/text-expander-element": "2.8.0", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@primer/octicons": "19.14.0", @@ -29,14 +29,14 @@ "htmx.org": "1.9.12", "idiomorph": "0.3.0", "jquery": "3.7.1", - "katex": "0.16.20", + "katex": "0.16.21", "mermaid": "11.4.1", "mini-css-extract-plugin": "2.9.2", "minimatch": "10.0.1", "monaco-editor": "0.52.2", "monaco-editor-webpack-plugin": "7.1.0", "pdfobject": "2.3.0", - "postcss": "8.4.49", + "postcss": "8.5.1", "postcss-loader": "8.1.1", "postcss-nesting": "13.0.1", "pretty-ms": "9.0.0", @@ -88,7 +88,7 @@ "happy-dom": "16.3.0", "license-checker-rseidelsohn": "4.4.2", "markdownlint-cli": "0.43.0", - "postcss-html": "1.7.0", + "postcss-html": "1.8.0", "stylelint": "16.12.0", "stylelint-declaration-block-no-ignored-properties": "2.8.0", "stylelint-declaration-strict-value": "1.10.6", diff --git a/renovate.json b/renovate.json index 64e610cd57..c34ae1aaba 100644 --- a/renovate.json +++ b/renovate.json @@ -110,34 +110,19 @@ "matchUpdateTypes": ["patch"], "automerge": true }, - { - "description": "Automerge renovate updates", - "matchDatasources": ["docker"], - "matchPackageNames": ["code.forgejo.org/forgejo-contrib/renovate"], - "matchUpdateTypes": ["minor", "patch", "digest"], - "automerge": true - }, { "description": "Add reviewer and additional labels to renovate PRs", "matchDatasources": ["docker"], - "matchPackageNames": ["code.forgejo.org/forgejo-contrib/renovate"], + "matchPackageNames": ["data.forgejo.org/renovate/renovate"], "reviewers": ["viceice"], "addLabels": ["forgejo/ci", "test/not-needed"] }, - { - "description": "Update renovate with higher prio to come through rate limit", - "matchDatasources": ["docker"], - "matchPackageNames": ["code.forgejo.org/forgejo-contrib/renovate"], - "extends": ["schedule:weekly"], - "prPriority": 10, - "groupName": "renovate" - }, { "description": "Disable renovate self-updates for release branches", "matchBaseBranches": ["/^v\\d+\\.\\d+\\/forgejo$/"], - "matchDatasources": ["docker"], "matchPackageNames": [ "code.forgejo.org/forgejo-contrib/renovate", + "data.forgejo.org/renovate/renovate", "ghcr.io/visualon/renovate" ], "enabled": false diff --git a/routers/api/v1/activitypub/reqsignature.go b/routers/api/v1/activitypub/reqsignature.go index 6003f664a0..19d167b50b 100644 --- a/routers/api/v1/activitypub/reqsignature.go +++ b/routers/api/v1/activitypub/reqsignature.go @@ -18,8 +18,8 @@ import ( "code.gitea.io/gitea/modules/setting" gitea_context "code.gitea.io/gitea/services/context" + "github.com/42wim/httpsig" ap "github.com/go-ap/activitypub" - "github.com/go-fed/httpsig" ) func getPublicKeyFromResponse(b []byte, keyID *url.URL) (p crypto.PublicKey, err error) { diff --git a/services/auth/httpsign.go b/services/auth/httpsign.go index b604349f80..83a36bef23 100644 --- a/services/auth/httpsign.go +++ b/services/auth/httpsign.go @@ -17,7 +17,7 @@ import ( "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/setting" - "github.com/go-fed/httpsig" + "github.com/42wim/httpsig" "golang.org/x/crypto/ssh" ) @@ -205,7 +205,7 @@ func doVerify(verifier httpsig.Verifier, sshPublicKeys []ssh.PublicKey) error { case strings.HasPrefix(publicKey.Type(), "ssh-ed25519"): algos = []httpsig.Algorithm{httpsig.ED25519} case strings.HasPrefix(publicKey.Type(), "ssh-rsa"): - algos = []httpsig.Algorithm{httpsig.RSA_SHA1, httpsig.RSA_SHA256, httpsig.RSA_SHA512} + algos = []httpsig.Algorithm{httpsig.RSA_SHA256, httpsig.RSA_SHA512} } for _, algo := range algos { if err := verifier.Verify(cryptoPubkey, algo); err == nil { diff --git a/services/mirror/mirror_pull.go b/services/mirror/mirror_pull.go index 53a2cff13f..bce8386f54 100644 --- a/services/mirror/mirror_pull.go +++ b/services/mirror/mirror_pull.go @@ -40,7 +40,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error repoPath := m.GetRepository(ctx).RepoPath() // Remove old remote _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } @@ -51,7 +51,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, addr, repoPath)) } _, _, err = cmd.RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } @@ -60,7 +60,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error wikiRemotePath := repo_module.WikiRemoteURL(ctx, addr) // Remove old remote of wiki _, _, err = git.NewCommand(ctx, "remote", "rm").AddDynamicArguments(remoteName).RunStdString(&git.RunOpts{Dir: wikiPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } @@ -71,7 +71,7 @@ func UpdateAddress(ctx context.Context, m *repo_model.Mirror, addr string) error cmd.SetDescription(fmt.Sprintf("remote add %s --mirror=fetch %s [repo_path: %s]", remoteName, wikiRemotePath, wikiPath)) } _, _, err = cmd.RunStdString(&git.RunOpts{Dir: wikiPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } } diff --git a/services/repository/migrate.go b/services/repository/migrate.go index 39ced04ae3..b5735ac5b2 100644 --- a/services/repository/migrate.go +++ b/services/repository/migrate.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "net/http" - "strings" "time" "code.gitea.io/gitea/models/db" @@ -253,10 +252,10 @@ func MigrateRepositoryGitData(ctx context.Context, u *user_model.User, func cleanUpMigrateGitConfig(ctx context.Context, repoPath string) error { cmd := git.NewCommand(ctx, "remote", "rm", "origin") // if the origin does not exist - _, stderr, err := cmd.RunStdString(&git.RunOpts{ + _, _, err := cmd.RunStdString(&git.RunOpts{ Dir: repoPath, }) - if err != nil && !strings.HasPrefix(stderr, "fatal: No such remote") { + if err != nil && !git.IsRemoteNotExistError(err) { return err } return nil @@ -275,7 +274,7 @@ func CleanUpMigrateInfo(ctx context.Context, repo *repo_model.Repository) (*repo } _, _, err := git.NewCommand(ctx, "remote", "rm", "origin").RunStdString(&git.RunOpts{Dir: repoPath}) - if err != nil && !strings.HasPrefix(err.Error(), "exit status 128 - fatal: No such remote ") { + if err != nil && !git.IsRemoteNotExistError(err) { return repo, fmt.Errorf("CleanUpMigrateInfo: %w", err) } diff --git a/templates/repo/commit_page.tmpl b/templates/repo/commit_page.tmpl index 66be0c143d..36de789dd1 100644 --- a/templates/repo/commit_page.tmpl +++ b/templates/repo/commit_page.tmpl @@ -128,9 +128,11 @@ -
- {{ctx.Locale.Tr "repo.diff.git-notes.add"}} -
+ {{if not .NoteRendered}} +
+ {{ctx.Locale.Tr "repo.diff.git-notes.add"}} +
+ {{end}} {{end}} diff --git a/templates/repo/diff/box.tmpl b/templates/repo/diff/box.tmpl index 49483433f4..e24c880746 100644 --- a/templates/repo/diff/box.tmpl +++ b/templates/repo/diff/box.tmpl @@ -85,7 +85,6 @@ diffFileInfo.files.push(...diffDataFiles); window.config.pageData.diffFileInfo = diffFileInfo; -
{{end}}
{{if $showFileTree}} diff --git a/templates/repo/diff/options_dropdown.tmpl b/templates/repo/diff/options_dropdown.tmpl index 97dc3ba940..44b0743e09 100644 --- a/templates/repo/diff/options_dropdown.tmpl +++ b/templates/repo/diff/options_dropdown.tmpl @@ -1,7 +1,6 @@