mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2025-01-20 16:50:28 -05:00
Compare commits
30 commits
9f3863c610
...
359c5a5259
Author | SHA1 | Date | |
---|---|---|---|
|
359c5a5259 | ||
|
243fdb60d0 | ||
|
86c8949d9c | ||
|
ce4730f9d7 | ||
|
5f2d445d00 | ||
|
d68e0d3e39 | ||
|
e35afe475a | ||
|
d2bb86407f | ||
|
f2e63495e7 | ||
|
244ee64c30 | ||
|
f1a92de4e6 | ||
|
4c746ec653 | ||
|
401906b88e | ||
|
dbec2ed350 | ||
|
5c8db43447 | ||
|
34e1100ae2 | ||
|
a2787bb09e | ||
|
376a2e19ea | ||
|
b2a3a0411c | ||
|
387f590d9c | ||
|
cefd786685 | ||
|
907ab8bdef | ||
|
4fd56a11c8 | ||
|
909738e6f6 | ||
|
e7299eb0fb | ||
|
f7ba8f0e41 | ||
|
a975b6ab94 | ||
|
f2feb34927 | ||
|
d09b8ba9cf | ||
|
f809052193 |
104 changed files with 1343 additions and 255 deletions
|
@ -246,6 +246,7 @@ code.gitea.io/gitea/modules/translation
|
|||
MockLocale.TrString
|
||||
MockLocale.Tr
|
||||
MockLocale.TrN
|
||||
MockLocale.TrPluralString
|
||||
MockLocale.TrSize
|
||||
MockLocale.PrettyNumber
|
||||
|
||||
|
|
2
Makefile
2
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: ...
|
||||
|
|
10
go.mod
10
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
|
||||
|
|
16
go.sum
16
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=
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
41
models/migrations/v1_23/v303_test.go
Normal file
41
models/migrations/v1_23/v303_test.go
Normal file
|
@ -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))
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ package setting
|
|||
import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/42wim/httpsig"
|
||||
)
|
||||
|
||||
// Federation settings
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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}
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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; <span style="color: red; background: none;">a&b</span>`, 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=<a>%s</a>`), nil))
|
||||
require.NoError(t, ls.AddLocaleByIni("lang1", "Lang1", MockPluralRule, []byte(`key=<a>%s</a>`), 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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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), ""}
|
||||
}
|
||||
|
|
253
modules/translation/plural_rules.go
Normal file
253
modules/translation/plural_rules.go
Normal file
|
@ -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
|
||||
},
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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: <a href="%[1]s">%[2]s#%[3]d</a>`
|
||||
pulls.create = Create pull request
|
||||
pulls.title_desc_one = wants to merge %[1]d commit from <code>%[2]s</code> into <code id="%[4]s">%[3]s</code>
|
||||
pulls.title_desc_few = wants to merge %[1]d commits from <code>%[2]s</code> into <code id="%[4]s">%[3]s</code>
|
||||
pulls.merged_title_desc_one = merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
|
||||
pulls.merged_title_desc_few = merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s
|
||||
pulls.change_target_branch_at = `changed target branch from <b>%s</b> to <b>%s</b> %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
|
||||
|
|
1
options/locale_next/locale_ar.json
Normal file
1
options/locale_next/locale_ar.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
options/locale_next/locale_be.json
Normal file
1
options/locale_next/locale_be.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
10
options/locale_next/locale_bg.json
Normal file
10
options/locale_next/locale_bg.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "сля %[1]d подаване от <code>%[2]s</code> в <code>%[3]s</code> %[4]s",
|
||||
"other": "сля %[1]d подавания от <code>%[2]s</code> в <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "иска да слее %[1]d подаване от <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "иска да слее %[1]d подавания от <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>"
|
||||
}
|
||||
}
|
1
options/locale_next/locale_bn.json
Normal file
1
options/locale_next/locale_bn.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
options/locale_next/locale_bs.json
Normal file
1
options/locale_next/locale_bs.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
3
options/locale_next/locale_ca.json
Normal file
3
options/locale_next/locale_ca.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"search.milestone_kind": "Cerca fites..."
|
||||
}
|
11
options/locale_next/locale_cs-CZ.json
Normal file
11
options/locale_next/locale_cs-CZ.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "sloučil %[1]d commit z <code>%[2]s</code> do <code>%[3]s</code> %[4]s",
|
||||
"other": "sloučil %[1]d commity z větve <code>%[2]s</code> do větve <code>%[3]s</code> před %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "žádá o sloučení %[1]d commitu z <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "chce sloučit %[1]d commity z větve <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Hledat milníky..."
|
||||
}
|
3
options/locale_next/locale_da.json
Normal file
3
options/locale_next/locale_da.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"search.milestone_kind": "Søg milepæle..."
|
||||
}
|
11
options/locale_next/locale_de-DE.json
Normal file
11
options/locale_next/locale_de-DE.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "hat %[1]d Commit von <code>%[2]s</code> nach <code>%[3]s</code> %[4]s zusammengeführt",
|
||||
"other": "hat %[1]d Commits von <code>%[2]s</code> nach <code>%[3]s</code> %[4]s zusammengeführt"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "möchte %[1]d Commit von <code>%[2]s</code> nach <code id=\"%[4]s\">%[3]s</code> zusammenführen",
|
||||
"other": "möchte %[1]d Commits von <code>%[2]s</code> nach <code id=\"%[4]s\">%[3]s</code> zusammenführen"
|
||||
},
|
||||
"search.milestone_kind": "Meilensteine suchen …"
|
||||
}
|
11
options/locale_next/locale_el-GR.json
Normal file
11
options/locale_next/locale_el-GR.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "συγχώνευσε %[1]d υποβολή από τον κλάδο <code>%[2]s</code> στον κλάδο <code>%[3]s</code> %[4]s",
|
||||
"other": "συγχώνευσε %[1]d υποβολές από <code>%[2]s</code> σε <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": ": θα ήθελε να συγχωνεύσει %[1]d υποβολή από τον κλάδο <code>%[2]s</code> στον κλάδο <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "θέλει να συγχωνεύσει %[1]d υποβολές από <code>%[2]s</code> σε <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Αναζήτηση ορόσημων..."
|
||||
}
|
11
options/locale_next/locale_en-US.json
Normal file
11
options/locale_next/locale_en-US.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "merged %[1]d commit from <code>%[2]s</code> into <code>%[3]s</code> %[4]s",
|
||||
"other": "merged %[1]d commits from <code>%[2]s</code> into <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "wants to merge %[1]d commit from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "wants to merge %[1]d commits from <code>%[2]s</code> into <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Search milestones..."
|
||||
}
|
3
options/locale_next/locale_eo.json
Normal file
3
options/locale_next/locale_eo.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"search.milestone_kind": "Serĉi celojn..."
|
||||
}
|
11
options/locale_next/locale_es-ES.json
Normal file
11
options/locale_next/locale_es-ES.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "fusionó %[1]d commit de <code>%[2]s</code> en <code>%[3]s</code> %[4]s",
|
||||
"other": "fusionó %[1]d commits de <code>%[2]s</code> en <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "quiere fusionar %[1]d commit de <code>%[2]s</code> en <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "quiere fusionar %[1]d commits de <code>%[2]s</code> en <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Buscar hitos…"
|
||||
}
|
3
options/locale_next/locale_et.json
Normal file
3
options/locale_next/locale_et.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"search.milestone_kind": "Otsi verstapostid..."
|
||||
}
|
8
options/locale_next/locale_fa-IR.json
Normal file
8
options/locale_next/locale_fa-IR.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "%[1]d کامیت ادغام شده از <code>%[2]s</code> به <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "قصد ادغام %[1]d تغییر را از <code>%[2]s</code> به <code id=\"%[4]s\">%[3]s</code> دارد"
|
||||
}
|
||||
}
|
9
options/locale_next/locale_fi-FI.json
Normal file
9
options/locale_next/locale_fi-FI.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "yhdistetty %[1]d committia lähteestä <code>%[2]s</code> kohteeseen <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "haluaa yhdistää %[1]d committia lähteestä <code>%[2]s</code> kohteeseen <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Etsi merkkipaaluja..."
|
||||
}
|
11
options/locale_next/locale_fil.json
Normal file
11
options/locale_next/locale_fil.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "isinali ang %[1]d commit mula<code>%[2]s</code> patungong <code>%[3]s</code> %[4]s",
|
||||
"other": "isinali ang %[1]d mga commit mula sa <code>%[2]s</code> patungong <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "hinihiling na isama ang %[1]d commit mula <code>%[2]s</code> patungong <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "hiniling na isama ang %[1]d mga commit mula sa <code>%[2]s</code> patungong <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Maghanap ng mga milestone…"
|
||||
}
|
11
options/locale_next/locale_fr-FR.json
Normal file
11
options/locale_next/locale_fr-FR.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "fusionné %[1]d commit depuis <code>%[2]s</code> vers <code>%[3]s</code> %[4]s",
|
||||
"other": "a fusionné %[1]d révision(s) à partir de <code>%[2]s</code> vers <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "veut fusionner %[1]d commit depuis <code>%[2]s</code> vers <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "souhaite fusionner %[1]d révision(s) depuis <code>%[2]s</code> vers <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Recherche dans les jalons..."
|
||||
}
|
1
options/locale_next/locale_gl.json
Normal file
1
options/locale_next/locale_gl.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
options/locale_next/locale_hi.json
Normal file
1
options/locale_next/locale_hi.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
9
options/locale_next/locale_hu-HU.json
Normal file
9
options/locale_next/locale_hu-HU.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "egyesítve %[1]d változás(ok) a <code>%[2]s</code>-ból <code>%[3]s</code>-ba %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "egyesíteni szeretné %[1]d változás(oka)t a(z) <code>%[2]s</code>-ból <code id=\"%[4]s\">%[3]s</code>-ba"
|
||||
},
|
||||
"search.milestone_kind": "Mérföldkövek keresése..."
|
||||
}
|
8
options/locale_next/locale_id-ID.json
Normal file
8
options/locale_next/locale_id-ID.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "commit %[1]d telah digabungkan dari <code>%[2]s</code> menjadi <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "ingin menggabungkan komit %[1]d dari <code>%[2]s</code> menuju <code id=\"%[4]s\">%[3]s</code>"
|
||||
}
|
||||
}
|
5
options/locale_next/locale_is-IS.json
Normal file
5
options/locale_next/locale_is-IS.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "vill sameina %[1]d framlög frá <code>%[2]s</code> í <code id=\"%[4]s\">%[3]s</code>"
|
||||
}
|
||||
}
|
11
options/locale_next/locale_it-IT.json
Normal file
11
options/locale_next/locale_it-IT.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "ha fuso %[1]d commit da <code>%[2]s</code> in <code>%[3]s</code> %[4]s",
|
||||
"other": "ha unito %[1]d commit da <code>%[2]s</code> a <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "vuole fondere %[1]d commit da <code>%[2]s</code> in <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "vuole unire %[1]d commit da <code>%[2]s</code> a <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Ricerca tappe..."
|
||||
}
|
9
options/locale_next/locale_ja-JP.json
Normal file
9
options/locale_next/locale_ja-JP.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "が %[1]d 個のコミットを <code>%[2]s</code> から <code>%[3]s</code> へマージ %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "が <code>%[2]s</code> から <code id=\"%[4]s\">%[3]s</code> への %[1]d コミットのマージを希望しています"
|
||||
},
|
||||
"search.milestone_kind": "マイルストーンを検索..."
|
||||
}
|
8
options/locale_next/locale_ko-KR.json
Normal file
8
options/locale_next/locale_ko-KR.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "님이 <code>%[2]s</code> 에서 <code>%[3]s</code> 로 %[1]d 커밋을 %[4]s 병합함"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "<code>%[2]s</code> 에서 <code id=\"%[4]s\">%[3]s</code> 로 %[1]d개의 커밋들을 병합하려함"
|
||||
}
|
||||
}
|
3
options/locale_next/locale_lt.json
Normal file
3
options/locale_next/locale_lt.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"search.milestone_kind": "Ieškoti gairių..."
|
||||
}
|
11
options/locale_next/locale_lv-LV.json
Normal file
11
options/locale_next/locale_lv-LV.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "iekļāva %[1]d iesūtījumu no <code>%[2]s</code> <code>%[3]s</code> %[4]s",
|
||||
"other": "Iekļāva %[1]d iesūtījumus no <code>%[2]s</code> zarā <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "vēlas iekļaut %[1]d iesūtījumu no <code>%[2]s</code> <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "vēlas iekļaut %[1]d iesūtījumus no <code>%[2]s</code> zarā <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Meklēt atskaites punktus..."
|
||||
}
|
1
options/locale_next/locale_ml-IN.json
Normal file
1
options/locale_next/locale_ml-IN.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
options/locale_next/locale_nb_NO.json
Normal file
1
options/locale_next/locale_nb_NO.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
11
options/locale_next/locale_nds.json
Normal file
11
options/locale_next/locale_nds.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "hett %[1]d Kommitteren vun <code>%[2]s</code> na <code>%[3]s</code> %[4]s tosamenföhrt",
|
||||
"other": "hett %[1]d Kommitterens vun <code>%[2]s</code> na <code>%[3]s</code> %[4]s tosamenföhrt"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "will %[1]d Kommitteren vun <code>%[2]s</code> na <code id=\"%[4]s\">%[3]s</code> tosamenföhren",
|
||||
"other": "will %[1]d Kommitterens vun <code>%[2]s</code> na <code id=\"%[4]s\">%[3]s</code> tosamenföhren"
|
||||
},
|
||||
"search.milestone_kind": "In Markstenen söken …"
|
||||
}
|
11
options/locale_next/locale_nl-NL.json
Normal file
11
options/locale_next/locale_nl-NL.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "heeft %[1]d commit van <code>%[2]s</code> samengevoegd in <code>%[3]s</code> %[4]s",
|
||||
"other": "heeft %[1]d commits samengevoegd van <code>%[2]s</code> naar <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "wilt %[1]d commit van <code>%[2]s</code> samenvoegen in <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "wilt %[1]d commits van <code>%[2]s</code> samenvoegen met <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Zoek mijlpalen..."
|
||||
}
|
9
options/locale_next/locale_pl-PL.json
Normal file
9
options/locale_next/locale_pl-PL.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"many": "scala %[1]d commity/ów z <code>%[2]s</code> do <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"many": "chce scalić %[1]d commity/ów z <code>%[2]s</code> do <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Wyszukaj kamienie milowe..."
|
||||
}
|
11
options/locale_next/locale_pt-BR.json
Normal file
11
options/locale_next/locale_pt-BR.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "mesclou %[1]d commit de <code>%[2]s</code> em <code>%[3]s</code> %[4]s",
|
||||
"other": "mesclou %[1]d commits de <code>%[2]s</code> em <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "quer mesclar %[1]d commit de <code>%[2]s</code> em <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "quer mesclar %[1]d commits de <code>%[2]s</code> em <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Pesquisar marcos..."
|
||||
}
|
11
options/locale_next/locale_pt-PT.json
Normal file
11
options/locale_next/locale_pt-PT.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "integrou %[1]d cometimento do ramo <code>%[2]s</code> no ramo <code>%[3]s</code> %[4]s",
|
||||
"other": "integrou %[1]d cometimento(s) do ramo <code>%[2]s</code> no ramo <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "quer integrar %[1]d cometimento do ramo <code>%[2]s</code> no ramo <code id=\"%[4]s\">%[3]s</code>",
|
||||
"other": "quer integrar %[1]d cometimento(s) do ramo <code>%[2]s</code> no ramo <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Procurar etapas..."
|
||||
}
|
11
options/locale_next/locale_ru-RU.json
Normal file
11
options/locale_next/locale_ru-RU.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "слит %[1]d коммит из <code>%[2]s</code> в <code>%[3]s</code> %[4]s",
|
||||
"many": "слито %[1]d коммит(ов) из <code>%[2]s</code> в <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "хочет влить %[1]d коммит из <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>",
|
||||
"many": "хочет влить %[1]d коммит(ов) из <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Найти этапы..."
|
||||
}
|
8
options/locale_next/locale_si-LK.json
Normal file
8
options/locale_next/locale_si-LK.json
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "මර්ජ්%[1]d සිට <code>%[2]s</code> දක්වා <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "%[1]d සිට <code>%[2]s</code> දක්වා <code id=\"%[4]s\">%[3]s</code>"
|
||||
}
|
||||
}
|
1
options/locale_next/locale_sk-SK.json
Normal file
1
options/locale_next/locale_sk-SK.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
options/locale_next/locale_sl.json
Normal file
1
options/locale_next/locale_sl.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
options/locale_next/locale_sr-SP.json
Normal file
1
options/locale_next/locale_sr-SP.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
9
options/locale_next/locale_sv-SE.json
Normal file
9
options/locale_next/locale_sv-SE.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "sammanfogade %[1]d incheckningar från <code>%[2]s</code> in i <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "vill sammanfoga %[1]d incheckningar från <code>s[2]s</code> in i <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Sök milstolpar..."
|
||||
}
|
9
options/locale_next/locale_tr-TR.json
Normal file
9
options/locale_next/locale_tr-TR.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "%[4]s <code>%[2]s</code> içindeki %[1]d işlemeyi <code>%[3]s</code> ile birleştirdi"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "<code>%[2]s</code> içindeki %[1]d işlemeyi <code id=\"%[4]s\">%[3]s</code> ile birleştirmek istiyor"
|
||||
},
|
||||
"search.milestone_kind": "Kilometre taşlarını ara..."
|
||||
}
|
11
options/locale_next/locale_uk-UA.json
Normal file
11
options/locale_next/locale_uk-UA.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"one": "об'єднав %[1]d коміт з <code>%[2]s</code> в <code>%[3]s</code> %[4]s",
|
||||
"many": "об'єднав %[1]d комітів з <code>%[2]s</code> в <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"one": "хоче об'єднати %[1]d коміт з <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>",
|
||||
"many": "хоче об'єднати %[1]d комітів з <code>%[2]s</code> в <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "Шукати віхи..."
|
||||
}
|
1
options/locale_next/locale_vi.json
Normal file
1
options/locale_next/locale_vi.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
1
options/locale_next/locale_yi.json
Normal file
1
options/locale_next/locale_yi.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
9
options/locale_next/locale_zh-CN.json
Normal file
9
options/locale_next/locale_zh-CN.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "于 %[4]s 将 %[1]d 次代码提交从 <code>%[2]s</code>合并至 <code>%[3]s</code>"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "请求将 %[1]d 次代码提交从 <code>%[2]s</code> 合并至 <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "搜索里程碑…"
|
||||
}
|
5
options/locale_next/locale_zh-HK.json
Normal file
5
options/locale_next/locale_zh-HK.json
Normal file
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "於 %[4]s 將 %[1]d 次代碼提交從 <code>%[2]s</code>合併至 <code>%[3]s</code>"
|
||||
}
|
||||
}
|
9
options/locale_next/locale_zh-TW.json
Normal file
9
options/locale_next/locale_zh-TW.json
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"repo.pulls.merged_title_desc": {
|
||||
"other": "將 %[1]d 次提交從 <code>%[2]s</code> 合併至 <code>%[3]s</code> %[4]s"
|
||||
},
|
||||
"repo.pulls.title_desc": {
|
||||
"other": "請求將 %[1]d 次程式碼提交從 <code>%[2]s</code> 合併至 <code id=\"%[4]s\">%[3]s</code>"
|
||||
},
|
||||
"search.milestone_kind": "搜尋里程碑..."
|
||||
}
|
36
package-lock.json
generated
36
package-lock.json
generated
|
@ -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": {
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -128,9 +128,11 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div id="commit-notes-add-button" class="item">
|
||||
{{ctx.Locale.Tr "repo.diff.git-notes.add"}}
|
||||
</div>
|
||||
{{if not .NoteRendered}}
|
||||
<div id="commit-notes-add-button" class="item">
|
||||
{{ctx.Locale.Tr "repo.diff.git-notes.add"}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -85,7 +85,6 @@
|
|||
diffFileInfo.files.push(...diffDataFiles);
|
||||
window.config.pageData.diffFileInfo = diffFileInfo;
|
||||
</script>
|
||||
<div id="diff-file-list"></div>
|
||||
{{end}}
|
||||
<div id="diff-container">
|
||||
{{if $showFileTree}}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
<div class="ui dropdown tiny basic button" data-tooltip-content="{{ctx.Locale.Tr "repo.diff.options_button"}}">
|
||||
{{svg "octicon-kebab-horizontal"}}
|
||||
<div class="menu">
|
||||
<a class="item" id="show-file-list-btn">{{ctx.Locale.Tr "repo.diff.show_diff_stats"}}</a>
|
||||
{{if .Issue.Index}}
|
||||
<a class="item" href="{{$.RepoLink}}/pulls/{{.Issue.Index}}.patch" download="{{.Issue.Index}}.patch">{{ctx.Locale.Tr "repo.diff.download_patch"}}</a>
|
||||
<a class="item" href="{{$.RepoLink}}/pulls/{{.Issue.Index}}.diff" download="{{.Issue.Index}}.diff">{{ctx.Locale.Tr "repo.diff.download_diff"}}</a>
|
||||
|
|
|
@ -63,10 +63,10 @@
|
|||
{{$mergedStr:= DateUtils.TimeSince .Issue.PullRequest.MergedUnix}}
|
||||
{{if .Issue.OriginalAuthor}}
|
||||
{{.Issue.OriginalAuthor}}
|
||||
<span class="pull-desc">{{ctx.Locale.TrN .NumCommits "repo.pulls.merged_title_desc_one" "repo.pulls.merged_title_desc_few" .NumCommits $headHref $baseHref $mergedStr}}</span>
|
||||
<span class="pull-desc">{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span>
|
||||
{{else}}
|
||||
<a {{if gt .Issue.PullRequest.Merger.ID 0}}href="{{.Issue.PullRequest.Merger.HomeLink}}"{{end}}>{{.Issue.PullRequest.Merger.GetDisplayName}}</a>
|
||||
<span class="pull-desc">{{ctx.Locale.TrN .NumCommits "repo.pulls.merged_title_desc_one" "repo.pulls.merged_title_desc_few" .NumCommits $headHref $baseHref $mergedStr}}</span>
|
||||
<span class="pull-desc">{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.merged_title_desc" .NumCommits $headHref $baseHref $mergedStr}}</span>
|
||||
{{end}}
|
||||
{{if .MadeUsingAGit}}
|
||||
{{/* TODO: Move documentation link to the instructions at the bottom of the PR, show instructions when clicking label */}}
|
||||
|
@ -79,11 +79,11 @@
|
|||
{{end}}
|
||||
{{else}}
|
||||
{{if .Issue.OriginalAuthor}}
|
||||
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.TrN .NumCommits "repo.pulls.title_desc_one" "repo.pulls.title_desc_few" .NumCommits $headHref $baseHref "branch_target"}}</span>
|
||||
<span id="pull-desc-display" class="pull-desc">{{.Issue.OriginalAuthor}} {{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}}</span>
|
||||
{{else}}
|
||||
<span id="pull-desc-display" class="pull-desc">
|
||||
<a {{if gt .Issue.Poster.ID 0}}href="{{.Issue.Poster.HomeLink}}"{{end}}>{{.Issue.Poster.GetDisplayName}}</a>
|
||||
{{ctx.Locale.TrN .NumCommits "repo.pulls.title_desc_one" "repo.pulls.title_desc_few" .NumCommits $headHref $baseHref "branch_target"}}
|
||||
{{ctx.Locale.TrPluralString .NumCommits "repo.pulls.title_desc" .NumCommits $headHref $baseHref "branch_target"}}
|
||||
</span>
|
||||
{{end}}
|
||||
{{if .MadeUsingAGit}}
|
||||
|
|
|
@ -70,21 +70,13 @@
|
|||
{{end}}
|
||||
{{if .IsPull}}
|
||||
<div class="branches flex-text-inline">
|
||||
{{svg "gitea-double-chevron-right" 12}}
|
||||
<div class="branch">
|
||||
<a href="{{.PullRequest.BaseRepo.Link}}/src/branch/{{PathEscapeSegments .PullRequest.BaseBranch}}">
|
||||
{{/* inline to remove the spaces between spans */}}
|
||||
{{if ne .RepoID .PullRequest.BaseRepoID}}<span class="truncated-name">{{.PullRequest.BaseRepo.OwnerName}}</span>:{{end}}<span class="truncated-name">{{.PullRequest.BaseBranch}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{svg "gitea-double-chevron-left" 12}}
|
||||
{{if .PullRequest.HeadRepo}}
|
||||
<div class="branch">
|
||||
<a href="{{.PullRequest.HeadRepo.Link}}/src/branch/{{PathEscapeSegments .PullRequest.HeadBranch}}">
|
||||
{{/* inline to remove the spaces between spans */}}
|
||||
{{if ne .RepoID .PullRequest.HeadRepoID}}<span class="truncated-name">{{.PullRequest.HeadRepo.OwnerName}}</span>:{{end}}<span class="truncated-name">{{.PullRequest.HeadBranch}}</span>
|
||||
</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{if and .Milestone (ne $.listType "milestone")}}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
<div class="ui small fluid action input">
|
||||
{{template "shared/search/input" dict "Value" .Value "Disabled" .Disabled "Placeholder" .Placeholder}}
|
||||
<div class="ui small dropdown selection {{if .Disabled}} disabled{{end}}" data-tooltip-content="{{ctx.Locale.Tr "search.type_tooltip"}}">
|
||||
{{svg "octicon-triangle-down" 14 "dropdown icon"}}
|
||||
<div class="text">
|
||||
{{ctx.Locale.Tr (printf "search.%s" .Selected)}}
|
||||
</div>
|
||||
|
|
|
@ -71,4 +71,5 @@ test('workflow dispatch box not available for unauthenticated users', async ({pa
|
|||
await page.goto('/user2/test_workflows/actions?workflow=test-dispatch.yml&actor=0&status=0');
|
||||
|
||||
await expect(page.locator('body')).not.toContainText(workflow_trigger_notification_text);
|
||||
await save_visual(page);
|
||||
});
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
|
||||
test('copy src file path to clipboard', async ({page}, workerInfo) => {
|
||||
test.skip(['Mobile Safari', 'webkit'].includes(workerInfo.project.name), 'Apple clipboard API addon - starting at just $499!');
|
||||
|
@ -19,6 +19,7 @@ test('copy src file path to clipboard', async ({page}, workerInfo) => {
|
|||
await page.click('[data-clipboard-text]');
|
||||
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(clipboardText).toContain('README.md');
|
||||
await save_visual(page);
|
||||
});
|
||||
|
||||
test('copy diff file path to clipboard', async ({page}, workerInfo) => {
|
||||
|
@ -30,4 +31,6 @@ test('copy diff file path to clipboard', async ({page}, workerInfo) => {
|
|||
await page.click('[data-clipboard-text]');
|
||||
const clipboardText = await page.evaluate(() => navigator.clipboard.readText());
|
||||
expect(clipboardText).toContain('README.md');
|
||||
await expect(page.getByText('Copied')).toBeVisible();
|
||||
await save_visual(page);
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
import {test} from './utils_e2e.ts';
|
||||
|
||||
test.use({user: 'user2'});
|
||||
|
||||
|
@ -23,5 +23,6 @@ test('Correct link and tooltip', async ({page}, testInfo) => {
|
|||
const repoStatus = page.locator('.dashboard-repos .repo-owner-name-list > li:nth-child(1) > a:nth-child(2)');
|
||||
await expect(repoStatus).toHaveAttribute('href', '/user2/test_workflows/actions', {timeout: 10000});
|
||||
await expect(repoStatus).toHaveAttribute('data-tooltip-content', /^(Error|Failure)$/);
|
||||
await save_visual(page);
|
||||
// ToDo: Ensure stable screenshot of dashboard. Known to be flaky: https://code.forgejo.org/forgejo/visual-browser-testing/commit/206d4cfb7a4af6d8d7043026cdd4d63708798b2a
|
||||
// await save_visual(page);
|
||||
});
|
||||
|
|
|
@ -82,6 +82,7 @@ func TestE2e(t *testing.T) {
|
|||
|
||||
runArgs := []string{"npx", "playwright", "test"}
|
||||
|
||||
_, testVisual := os.LookupEnv("VISUAL_TEST")
|
||||
// To update snapshot outputs
|
||||
if _, set := os.LookupEnv("ACCEPT_VISUAL"); set {
|
||||
runArgs = append(runArgs, "--update-snapshots")
|
||||
|
@ -105,6 +106,10 @@ func TestE2e(t *testing.T) {
|
|||
onForgejoRun(t, func(*testing.T, *url.URL) {
|
||||
defer DeclareGitRepos(t)()
|
||||
thisTest := runArgs
|
||||
// when all tests are run, use unique artifacts directories per test to preserve artifacts from other tests
|
||||
if testVisual {
|
||||
thisTest = append(thisTest, "--output=tests/e2e/test-artifacts/"+testname)
|
||||
}
|
||||
thisTest = append(thisTest, path)
|
||||
cmd := exec.Command(runArgs[0], thisTest...)
|
||||
cmd.Env = os.Environ()
|
||||
|
@ -114,7 +119,7 @@ func TestE2e(t *testing.T) {
|
|||
cmd.Stderr = os.Stderr
|
||||
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
if err != nil && !testVisual {
|
||||
log.Fatal("Playwright Failed: %s", err)
|
||||
}
|
||||
})
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
|
||||
test('Load Homepage', async ({page}) => {
|
||||
const response = await page.goto('/');
|
||||
|
@ -26,6 +26,7 @@ test('Register Form', async ({page}, workerInfo) => {
|
|||
expect(page.url()).toBe(`${workerInfo.project.use.baseURL}/`);
|
||||
await expect(page.locator('.secondary-nav span>img.ui.avatar')).toBeVisible();
|
||||
await expect(page.locator('.ui.positive.message.flash-success')).toHaveText('Account was successfully created. Welcome!');
|
||||
await save_visual(page);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line playwright/no-skipped-test
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
|
||||
test('Explore view taborder', async ({page}) => {
|
||||
await page.goto('/explore/repos');
|
||||
|
@ -42,4 +42,5 @@ test('Explore view taborder', async ({page}) => {
|
|||
}
|
||||
}
|
||||
expect(res).toBe(exp);
|
||||
await save_visual(page);
|
||||
});
|
||||
|
|
|
@ -8,6 +8,9 @@ test('Change git note', async ({page}) => {
|
|||
let response = await page.goto('/user2/repo1/commit/65f1bf27bc3bf70f64657658635e66094edbcb4d');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
// An add button should not be present, because the commit already has a commit note
|
||||
await expect(page.locator('#commit-notes-add-button')).toHaveCount(0);
|
||||
|
||||
await page.locator('#commit-notes-edit-button').click();
|
||||
|
||||
let textarea = page.locator('textarea[name="notes"]');
|
||||
|
|
|
@ -77,6 +77,27 @@ test('Always focus edit tab first on edit', async ({page}) => {
|
|||
await save_visual(page);
|
||||
});
|
||||
|
||||
test('Reset content of comment edit field on cancel', async ({page}) => {
|
||||
const response = await page.goto('/user2/repo1/issues/1');
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
const editorTextarea = page.locator('[id="_combo_markdown_editor_1"]');
|
||||
|
||||
// Change the content of the edit field
|
||||
await page.click('#issue-1 .comment-container .context-menu');
|
||||
await page.click('#issue-1 .comment-container .menu>.edit-content');
|
||||
await expect(editorTextarea).toHaveValue('content for the first issue');
|
||||
await editorTextarea.fill('some random string');
|
||||
await expect(editorTextarea).toHaveValue('some random string');
|
||||
await page.click('#issue-1 .comment-container .edit .cancel');
|
||||
|
||||
// Edit again and assert that the edit field should be reset to the initial content
|
||||
await page.click('#issue-1 .comment-container .context-menu');
|
||||
await page.click('#issue-1 .comment-container .menu>.edit-content');
|
||||
await expect(editorTextarea).toHaveValue('content for the first issue');
|
||||
await save_visual(page);
|
||||
});
|
||||
|
||||
test('Quote reply', async ({page}, workerInfo) => {
|
||||
test.skip(workerInfo.project.name !== 'firefox', 'Uses Firefox specific selection quirks');
|
||||
const response = await page.goto('/user2/repo1/issues/1');
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
|
||||
test('markup with #xyz-mode-only', async ({page}) => {
|
||||
const response = await page.goto('/user2/repo1/issues/1');
|
||||
|
@ -13,4 +13,5 @@ test('markup with #xyz-mode-only', async ({page}) => {
|
|||
await expect(comment).toBeVisible();
|
||||
await expect(comment.locator('[src$="#gh-light-mode-only"]')).toBeVisible();
|
||||
await expect(comment.locator('[src$="#gh-dark-mode-only"]')).toBeHidden();
|
||||
await save_visual(page);
|
||||
});
|
||||
|
|
|
@ -49,6 +49,7 @@ test('Line Range Selection', async ({page}) => {
|
|||
// out-of-bounds end line
|
||||
await page.goto(`${filePath}#L1-L100`);
|
||||
await assertSelectedLines(page, ['1', '2', '3']);
|
||||
await save_visual(page);
|
||||
});
|
||||
|
||||
test('Readable diff', async ({page}, workerInfo) => {
|
||||
|
@ -75,6 +76,7 @@ test('Readable diff', async ({page}, workerInfo) => {
|
|||
await expect(page.getByText(thisDiff.added, {exact: true})).toHaveCSS('background-color', 'rgb(134, 239, 172)');
|
||||
}
|
||||
}
|
||||
await save_visual(page);
|
||||
});
|
||||
|
||||
test.describe('As authenticated user', () => {
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
|
||||
test('Commit graph overflow', async ({page}) => {
|
||||
await page.goto('/user2/diff-test/graph');
|
||||
|
@ -28,4 +28,5 @@ test('Switch branch', async ({page}) => {
|
|||
await expect(page.locator('#loading-indicator')).toBeHidden();
|
||||
await expect(page.locator('#rel-container')).toBeVisible();
|
||||
await expect(page.locator('#rev-container')).toBeVisible();
|
||||
await save_visual(page);
|
||||
});
|
||||
|
|
|
@ -21,7 +21,6 @@ test('Migration Progress Page', async ({page, browser}, workerInfo) => {
|
|||
await form.locator('button.primary').click({timeout: 5000});
|
||||
await expect(page).toHaveURL('user2/invalidrepo');
|
||||
await save_visual(page);
|
||||
// page screenshot of unauthenticatedPage is checked automatically after the test
|
||||
|
||||
const ctx = await test_context(browser);
|
||||
const unauthenticatedPage = await ctx.newPage();
|
||||
|
@ -37,4 +36,6 @@ test('Migration Progress Page', async ({page, browser}, workerInfo) => {
|
|||
await save_visual(page);
|
||||
await deleteModal.getByRole('button', {name: 'Delete repository'}).click();
|
||||
await expect(page).toHaveURL('/');
|
||||
// checked last to preserve the order of screenshots from first run
|
||||
await save_visual(unauthenticatedPage);
|
||||
});
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
|
||||
for (const searchTerm of ['space', 'consectetur']) {
|
||||
for (const width of [null, 2560, 4000]) {
|
||||
|
@ -23,6 +23,7 @@ for (const searchTerm of ['space', 'consectetur']) {
|
|||
await page.getByPlaceholder('Search wiki').dispatchEvent('keyup');
|
||||
// timeout is necessary because HTMX search could be slow
|
||||
await expect(page.locator('#wiki-search a[href]')).toBeInViewport({ratio: 1});
|
||||
await save_visual(page);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -36,4 +37,5 @@ test(`Search results show titles (and not file names)`, async ({page}, workerInf
|
|||
// so we manually "type" the last letter
|
||||
await page.getByPlaceholder('Search wiki').dispatchEvent('keyup');
|
||||
await expect(page.locator('#wiki-search a[href] b')).toHaveText('Page With Spaced Name');
|
||||
await save_visual(page);
|
||||
});
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
// @watch end
|
||||
|
||||
import {expect} from '@playwright/test';
|
||||
import {test} from './utils_e2e.ts';
|
||||
import {save_visual, test} from './utils_e2e.ts';
|
||||
|
||||
test.describe('desktop viewport as user 2', () => {
|
||||
test.use({user: 'user2', viewport: {width: 1920, height: 300}});
|
||||
|
@ -54,6 +54,7 @@ test.describe('desktop viewport, unauthenticated', () => {
|
|||
await expect(page.locator('.overflow-menu-items>#settings-btn')).toHaveCount(0);
|
||||
|
||||
await expect(page.locator('.overflow-menu-button')).toHaveCount(0);
|
||||
await save_visual(page);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -78,6 +79,7 @@ test.describe('small viewport', () => {
|
|||
|
||||
const items = shownItems.concat(overflowItems);
|
||||
expect(Array.from(new Set(items))).toHaveLength(items.length);
|
||||
await save_visual(page);
|
||||
});
|
||||
|
||||
test('Settings button in overflow menu of org header', async ({page}) => {
|
||||
|
@ -121,5 +123,6 @@ test.describe('small viewport, unauthenticated', () => {
|
|||
|
||||
const items = shownItems.concat(overflowItems);
|
||||
expect(Array.from(new Set(items))).toHaveLength(items.length);
|
||||
await save_visual(page);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -26,15 +26,6 @@ export const test = baseTest.extend<TestOptions>({
|
|||
},
|
||||
user: null,
|
||||
authScope: 'shared',
|
||||
// see https://playwright.dev/docs/test-fixtures#adding-global-beforeeachaftereach-hooks
|
||||
forEachTest: [async ({page}, use) => {
|
||||
await use();
|
||||
// some tests create a new page which is not yet available here
|
||||
// only operate on tests that make the URL available
|
||||
if (page.url() !== 'about:blank') {
|
||||
await save_visual(page);
|
||||
}
|
||||
}, {auto: true}],
|
||||
});
|
||||
|
||||
export async function test_context(browser: Browser, options?: BrowserContextOptions) {
|
||||
|
@ -128,6 +119,7 @@ export async function save_visual(page: Page) {
|
|||
// update order of recently created repos is not fully deterministic
|
||||
page.locator('.flex-item-main').filter({hasText: 'relative time in repo'}),
|
||||
page.locator('#activity-feed'),
|
||||
page.locator('#user-heatmap'),
|
||||
// dynamic IDs in fixed-size inputs
|
||||
page.locator('input[value*="dyn-id-"]'),
|
||||
],
|
||||
|
|
|
@ -27,6 +27,7 @@ var renderContext = markup.RenderContext{
|
|||
|
||||
func FuzzMarkdownRenderRaw(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
setting.IsInTesting = true
|
||||
setting.AppURL = "http://localhost:3000/"
|
||||
markdown.RenderRaw(&renderContext, bytes.NewReader(data), io.Discard)
|
||||
})
|
||||
|
@ -34,6 +35,7 @@ func FuzzMarkdownRenderRaw(f *testing.F) {
|
|||
|
||||
func FuzzMarkupPostProcess(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
setting.IsInTesting = true
|
||||
setting.AppURL = "http://localhost:3000/"
|
||||
markup.PostProcess(&renderContext, bytes.NewReader(data), io.Discard)
|
||||
})
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/tests"
|
||||
|
||||
"github.com/go-fed/httpsig"
|
||||
"github.com/42wim/httpsig"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
|
|
145
tools/migrate_locales.sh
Executable file
145
tools/migrate_locales.sh
Executable file
|
@ -0,0 +1,145 @@
|
|||
#!/bin/bash
|
||||
|
||||
# Copyright 2024 The Forgejo Authors. All rights reserved.
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
if [ -z "$1" ] || [ -z "$2" ]
|
||||
then
|
||||
echo "USAGE: $0 section key [key1 [keyN]]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [ -d ../options/locale_next ]
|
||||
then
|
||||
echo 'Call this script from the `tools` directory.'
|
||||
exit 1
|
||||
fi
|
||||
|
||||
destsection="$1"
|
||||
keyJSON="$destsection.$2"
|
||||
key1=""
|
||||
keyN=""
|
||||
if [ -n "$3" ]
|
||||
then
|
||||
key1="$3"
|
||||
else
|
||||
key1="$2"
|
||||
fi
|
||||
if [ -n "$4" ]
|
||||
then
|
||||
keyN="$4"
|
||||
fi
|
||||
|
||||
cd ../options/locale
|
||||
|
||||
# Migrate the string in one file.
|
||||
function process() {
|
||||
file="$1"
|
||||
exec 3<$file
|
||||
|
||||
val1=""
|
||||
valN=""
|
||||
cursection=""
|
||||
line1=0
|
||||
lineN=0
|
||||
lineNumber=0
|
||||
|
||||
# Parse the file
|
||||
while read -u 3 line
|
||||
do
|
||||
((++lineNumber))
|
||||
if [[ $line =~ ^\[[-._a-zA-Z0-9]+\]$ ]]
|
||||
then
|
||||
cursection="${line#[}"
|
||||
cursection="${cursection%]}"
|
||||
elif [ "$cursection" = "$destsection" ]
|
||||
then
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
key="$(echo $key)" # Trim leading/trailing whitespace
|
||||
value="$(echo $value)"
|
||||
|
||||
if [ "$key" = "$key1" ]
|
||||
then
|
||||
val1="$value"
|
||||
line1=$lineNumber
|
||||
fi
|
||||
if [ -n "$keyN" ] && [ "$key" = "$keyN" ]
|
||||
then
|
||||
valN="$value"
|
||||
lineN=$lineNumber
|
||||
fi
|
||||
|
||||
if [ -n "$val1" ] && ( [ -n "$valN" ] || [ -z "$keyN" ] )
|
||||
then
|
||||
# Found all desired strings
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "$val1" ] || [ -n "$valN" ]
|
||||
then
|
||||
localename="${file#locale_}"
|
||||
localename="${localename%.ini}"
|
||||
localename="${localename%-*}"
|
||||
|
||||
if [ "$file" = "locale_en-US.ini" ]
|
||||
then
|
||||
# Delete migrated string from source file
|
||||
if [ $line1 -gt 0 ] && [ $lineN -gt 0 ] && [ $lineN -ne $line1 ]
|
||||
then
|
||||
sed -i "${line1}d;${lineN}d" "$file"
|
||||
elif [ $line1 -gt 0 ]
|
||||
then
|
||||
sed -i "${line1}d" "$file"
|
||||
elif [ $lineN -gt 0 ]
|
||||
then
|
||||
sed -i "${lineN}d" "$file"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Write JSON
|
||||
jsonfile="../locale_next/${file/.ini/.json}"
|
||||
|
||||
pluralform="other"
|
||||
oneform="one"
|
||||
case $localename in
|
||||
"be" | "bs" | "cnr" | "csb" | "hr" | "lt" | "pl" | "ru" | "sr" | "szl" | "uk" | "wen")
|
||||
# These languages have no "other" form and use "many" instead.
|
||||
pluralform="many"
|
||||
;;
|
||||
"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")
|
||||
# These languages have no singular form.
|
||||
oneform=""
|
||||
;;
|
||||
*)
|
||||
;;
|
||||
esac
|
||||
|
||||
content=""
|
||||
if [ -z "$keyN" ]
|
||||
then
|
||||
content="$(jq --arg val "$val1" ".$keyJSON = \$val" < "$jsonfile")"
|
||||
else
|
||||
object='{}'
|
||||
if [ -n "$val1" ] && [ -n "$oneform" ]
|
||||
then
|
||||
object=$(jq --arg val "$val1" ".$oneform = \$val" <<< "$object")
|
||||
fi
|
||||
if [ -n "$valN" ]
|
||||
then
|
||||
object=$(jq --arg val "$valN" ".$pluralform = \$val" <<< "$object")
|
||||
fi
|
||||
content="$(jq --argjson val "$object" ".$keyJSON = \$val" < "$jsonfile")"
|
||||
fi
|
||||
jq . <<< "$content" > "$jsonfile"
|
||||
fi
|
||||
}
|
||||
|
||||
for file in *.ini
|
||||
do
|
||||
process "$file" &
|
||||
done
|
||||
wait
|
||||
|
|
@ -1709,26 +1709,6 @@ td .commit-summary {
|
|||
max-width: initial; /* remove fomantic over 100% width */
|
||||
}
|
||||
|
||||
.repository .diff-stats {
|
||||
clear: both;
|
||||
margin-bottom: 5px;
|
||||
max-height: 200px;
|
||||
height: fit-content;
|
||||
overflow: auto;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.repository .diff-stats li {
|
||||
list-style: none;
|
||||
padding-bottom: 4px;
|
||||
margin-bottom: 4px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.repository .diff-stats li + li {
|
||||
border-top: 1px solid var(--color-secondary);
|
||||
}
|
||||
|
||||
.repository .repo-search-result {
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
|
@ -1839,10 +1819,6 @@ details.repo-search-result summary::marker {
|
|||
color: var(--color-success-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerified.message .pull-right {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.repository .ui.attached.isSigned.isVerified.message .ui.text {
|
||||
color: var(--color-success-text);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue