fillSettings(n, 'gmail')">Gmail
fillSettings(n, 'ses')">Amazon SES
fillSettings(n, 'mailgun')">Mailgun
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
index 730056a0c..4a45f7e5c 100644
--- a/frontend/vite.config.js
+++ b/frontend/vite.config.js
@@ -28,6 +28,9 @@ export default defineConfig(({ _, mode }) => {
'^/(api|webhooks|subscription|public|health)': {
target: env.LISTMONK_API_URL || 'http://127.0.0.1:9000',
},
+ '^/admin/login': {
+ target: env.LISTMONK_API_URL || 'http://127.0.0.1:9000',
+ },
'^/(admin\/custom\.(css|js))': {
target: env.LISTMONK_API_URL || 'http://127.0.0.1:9000',
},
diff --git a/frontend/yarn.lock b/frontend/yarn.lock
index 9515efe0f..673f29791 100644
--- a/frontend/yarn.lock
+++ b/frontend/yarn.lock
@@ -2709,7 +2709,7 @@ pretty-bytes@^5.6.0:
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
-prismjs@^1.14.0:
+prismjs@^1.14.0, prismjs@^1.29.0:
version "1.29.0"
resolved "https://registry.yarnpkg.com/prismjs/-/prismjs-1.29.0.tgz#f113555a8fa9b57c35e637bba27509dcf802dd12"
integrity sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==
diff --git a/go.mod b/go.mod
index 46d42da20..6f57f5f87 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.20
require (
github.com/Masterminds/sprig/v3 v3.2.3
+ github.com/coreos/go-oidc/v3 v3.9.0
github.com/disintegration/imaging v1.6.2
github.com/emersion/go-message v0.16.0
github.com/gdgvda/cron v0.2.0
@@ -31,7 +32,10 @@ require (
github.com/spf13/pflag v1.0.5
github.com/yuin/goldmark v1.6.0
github.com/zerodha/easyjson v1.0.0
+ github.com/zerodha/simplesessions/stores/postgres/v3 v3.0.0
+ github.com/zerodha/simplesessions/v3 v3.0.0
golang.org/x/mod v0.17.0
+ golang.org/x/oauth2 v0.13.0
gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b
)
@@ -40,7 +44,9 @@ require (
github.com/Masterminds/semver/v3 v3.2.0 // indirect
github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594 // indirect
github.com/fsnotify/fsnotify v1.6.0 // indirect
+ github.com/go-jose/go-jose/v3 v3.0.1 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/huandu/xstrings v1.4.0 // indirect
github.com/imdario/mergo v0.3.14 // indirect
@@ -63,6 +69,9 @@ require (
golang.org/x/sys v0.18.0 // indirect
golang.org/x/text v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect
+ google.golang.org/appengine v1.6.8 // indirect
+ google.golang.org/protobuf v1.31.0 // indirect
+ gopkg.in/yaml.v2 v2.3.0 // indirect
)
replace github.com/imdario/mergo => github.com/imdario/mergo v0.3.8
diff --git a/go.sum b/go.sum
index d8a2ad195..e2e6bcd9c 100644
--- a/go.sum
+++ b/go.sum
@@ -4,6 +4,8 @@ github.com/Masterminds/semver/v3 v3.2.0 h1:3MEsd0SM6jqZojhjLWWeBY+Kcjy9i6MQAeY7Y
github.com/Masterminds/semver/v3 v3.2.0/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ=
github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA=
github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM=
+github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
+github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -20,6 +22,8 @@ github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
github.com/gdgvda/cron v0.2.0 h1:oX8qdLZq4tC5StnCsZsTNs2BIzaRjcjmPZ4o+BArKX4=
github.com/gdgvda/cron v0.2.0/go.mod h1:VEwidZXB255kESB5DcUGRWTYZS8KkOBYD1YBn8Wiyx8=
+github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
+github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE=
github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
@@ -28,7 +32,13 @@ github.com/gofrs/uuid/v5 v5.0.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
-github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -124,10 +134,11 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
-github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
@@ -137,7 +148,12 @@ github.com/yuin/goldmark v1.6.0 h1:boZcn2GTjpsynOsC0iJHnBWa4Bi0qzfJjthwauItG68=
github.com/yuin/goldmark v1.6.0/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/zerodha/easyjson v1.0.0 h1:3u1lvS8C+8ntnb4lXHc7ZzfQ8txUdzBAH5t9AwF7bUs=
github.com/zerodha/easyjson v1.0.0/go.mod h1:mA8d8Xs8Yp4Q95ppRb4dRGROERgKSLQIK9Y7iuC5mog=
+github.com/zerodha/simplesessions/stores/postgres/v3 v3.0.0 h1:50BNRW/VYOgCf5v6vbhKMT40sFA+yZ7xUrdM/vbI1G8=
+github.com/zerodha/simplesessions/stores/postgres/v3 v3.0.0/go.mod h1:PifZh0lGfmx4sN3+YvDCjkIDrTzZoILL9jkczV1SsiA=
+github.com/zerodha/simplesessions/v3 v3.0.0 h1:seHwxVNnlCbp5nG8GFxSsRUdiHnfb39QdEW3J536O9Y=
+github.com/zerodha/simplesessions/v3 v3.0.0/go.mod h1:lAK+CJmZRlbvfq+OnkB8Iyf6LWgjzvUuWYKX1XA51P0=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190911031432-227b76d455e7/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
@@ -148,6 +164,7 @@ golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -155,9 +172,12 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY=
+golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -176,6 +196,7 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4=
golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI=
@@ -185,13 +206,20 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
+google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8=
+google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b h1:P+3+n9hUbqSDkSdtusWHVPQRrpRpLiLFzlZ02xXskM0=
gopkg.in/volatiletech/null.v6 v6.0.0-20170828023728-0bef4e07ae1b/go.mod h1:0LRKfykySnChgQpG3Qpk+bkZFWazQ+MMfc5oldQCwnY=
-gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/i18n/ca.json b/i18n/ca.json
index a9ba15d01..0782f4ccf 100644
--- a/i18n/ca.json
+++ b/i18n/ca.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Habilitat",
"globals.buttons.insert": "Inserta",
"globals.buttons.learnMore": "Saber-nes més",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Més",
"globals.buttons.new": "Nou",
"globals.buttons.ok": "D'acord",
"globals.buttons.remove": "Elimina",
"globals.buttons.save": "Desa",
"globals.buttons.saveChanges": "Desa els canvis",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Visualitzar",
"globals.days.0": "dg.",
"globals.days.1": "dg.",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "No s'ha trobat {name} ",
"globals.messages.passwordChange": "Introduïu un valor per canviar",
"globals.messages.passwordChangeFull": "Buida i torna a introduir la contrasenya completa a '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Les consultes lentes s'estan emmagatzemant en memòria cau. Algunes xifres en aquesta pàgina no seran actuals.",
"globals.messages.updated": "\"{name}\" actualitzat",
"globals.months.1": "gen.",
@@ -232,6 +235,8 @@
"globals.terms.template": "Plantilla | Plantilles",
"globals.terms.templates": "Plantilles",
"globals.terms.tx": "Transaccional | Transaccionals",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Any | Anys",
"import.alreadyRunning": "Ja s'està executant una importació. Espereu que acabi o atureu-lo abans de tornar-ho a provar.",
"import.blocklist": "Llista de bloqueig",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} registres",
"import.stopImport": "Atura la importació",
"import.subscribe": "Subscriu",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importa subscriptors",
"import.upload": "Carrega",
"lists.confirmDelete": "Estàs segur? Això no elimina els subscriptors.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Error en desar la miniatura: {error}",
"media.errorUploading": "Error en carregar el fitxer: {error}",
"media.invalidFile": "Fitxer no vàlid: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Mèdia",
"media.unsupportedFileType": "El tipus de fitxer ({type}) no és compatible",
"media.upload": "Carrega",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Registra l'adreça IP de l'opt-in",
"settings.privacy.recordOptinIPHelp": "Registra l'adreça IP dels opt-ins dobles en els atributs del subscrit.",
"settings.restart": "Reinicia",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Clau del lloc hCaptcha.com",
"settings.security.captchaKeyHelp": "Visiteu www.hcaptcha.com per obtenir la clau i el secret.",
"settings.security.captchaSecret": "Secret del lloc hCaptcha.com",
"settings.security.enableCaptcha": "Habilita el CAPTCHA",
"settings.security.enableCaptchaHelp": "Habilita el CAPTCHA al formulari públic de subscripció.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Seguretat",
"settings.smtp.customHeaders": "Capçaleres personalitzades",
"settings.smtp.customHeadersHelp": "Matriu opcional de capçaleres de correu electrònic per incloure en tots els missatges enviats des d'aquest servidor. p. ex.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Previsualització",
"templates.rawHTML": "Codi HTML",
"templates.subject": "Assumpte",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Inicia sessió",
- "users.logout": "Tanca sessió"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Tanca sessió",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/cs-cz.json b/i18n/cs-cz.json
index e2aa874c3..48a303a57 100644
--- a/i18n/cs-cz.json
+++ b/i18n/cs-cz.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Povoleno",
"globals.buttons.insert": "Vložit",
"globals.buttons.learnMore": "Další informace",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Více",
"globals.buttons.new": "Nový",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Odebrat",
"globals.buttons.save": "Uložit",
"globals.buttons.saveChanges": "Uložit změny",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Zobrazit",
"globals.days.0": "Ne",
"globals.days.1": "Ne",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} nebyl nalezen",
"globals.messages.passwordChange": "Zadejte hodnotu ke změně",
"globals.messages.passwordChangeFull": "Vymazat a zadat úplné heslo znovu v '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Pomalé dotazy jsou ukládány do mezipaměti. Některá čísla na této stránce nemusí být aktuální.",
"globals.messages.updated": "\"{name}\" aktualizován",
"globals.months.1": "Led",
@@ -232,6 +235,8 @@
"globals.terms.template": "Šablona | Šablony",
"globals.terms.templates": "Šablony",
"globals.terms.tx": "Transakční | Transakční",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Rok | Roky",
"import.alreadyRunning": "Import již běží. Počkejte na jeho dokončení nebo jej zastavte před dalším pokusem.",
"import.blocklist": "Seznam blokovaných",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} záznamů",
"import.stopImport": "Zastavit import ",
"import.subscribe": "Odebírat",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importovat odběratele",
"import.upload": "Odeslat",
"lists.confirmDelete": "Jste si jisti? Tímto se neodstraní odběratelé.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Chyba při ukládání miniatury: {error}",
"media.errorUploading": "Chyba při odesílání souboru: {error}",
"media.invalidFile": "Neplatný soubor: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Médium",
"media.unsupportedFileType": "Nepodporovaný typ souboru ({type})",
"media.upload": "Odeslat",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Zaznamenávat IP adresy pro opt-in",
"settings.privacy.recordOptinIPHelp": "Zaznamenávat IP adresy pro dvojí opt-in v atributu odběratele.",
"settings.restart": "Restartovat",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Klíč z hCaptcha.com",
"settings.security.captchaKeyHelp": "Navštivte www.hcaptcha.com pro získání klíče a tajného kódu.",
"settings.security.captchaSecret": "Tajný kód z hCaptcha.com",
"settings.security.enableCaptcha": "Povolit CAPTCHA",
"settings.security.enableCaptchaHelp": "Povolit CAPTCHA na veřejném formuláři pro přihlášení.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Zabezpečení",
"settings.smtp.customHeaders": "Vlastní záhlaví",
"settings.smtp.customHeadersHelp": "Volitelné pole e-mailových záhlaví, která se mají zahrnout do všech zpráv odeslaných z tohoto serveru. Např.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Náhled",
"templates.rawHTML": "Kód HTML",
"templates.subject": "Předmět",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Přihlásit",
- "users.logout": "Odhlásit"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Odhlásit",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/cy.json b/i18n/cy.json
index 1e26a51eb..24acd55e7 100644
--- a/i18n/cy.json
+++ b/i18n/cy.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Wedi galluogi",
"globals.buttons.insert": "Mewnosod",
"globals.buttons.learnMore": "Dysgu mwy",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Mwy",
"globals.buttons.new": "Newydd",
"globals.buttons.ok": "Iawn",
"globals.buttons.remove": "Dileu",
"globals.buttons.save": "Cadw",
"globals.buttons.saveChanges": "Cadw'r newidiadau",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Ymddangos",
"globals.days.0": "Sul",
"globals.days.1": "Sul",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "Heb ddod o hyd i {enw]",
"globals.messages.passwordChange": "Rhoi gwerth i'w newid",
"globals.messages.passwordChangeFull": "Clirio ac ailgyflwyno'r cyfrinair llawn yn '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Mae ymholiadau araf yn cael eu cadw. Ni fydd rhai rhifau ar y dudalen hon yn ddiweddar.",
"globals.messages.updated": "Wedi diweddaru “{name}”",
"globals.months.1": "Ion",
@@ -232,6 +235,8 @@
"globals.terms.template": "Templed | Templedi",
"globals.terms.templates": "Templedi",
"globals.terms.tx": "Trafodion",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Blwyddyn | Blynyddoedd",
"import.alreadyRunning": "Mae rhywbeth wrthi'n cael ei fewngludo. Arhoswch iddo orffen neu ei stopio cyn rhoi cynnig arall arni.",
"import.blocklist": "Rhestr rwystro",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} cofnod",
"import.stopImport": "Rhoi'r gorau i fewngludo",
"import.subscribe": "Tanysgrifio",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Mewngludo tanysgrifwyr",
"import.upload": "Llwytho i fyny",
"lists.confirmDelete": "Ydych chi'n siŵr? Nid yw hyn yn dileu tanysgrifwyr.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Gwall wrth arbed mân-lun: {error}",
"media.errorUploading": "Gwall wrth lwytho ffeil i fyny: {error}",
"media.invalidFile": "Ffeil annilys: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Cyfryngau",
"media.unsupportedFileType": "Math o ffeil nad yw'n cael ei gefnogi ({type})",
"media.upload": "Llwytho i fyny",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Cofnodi cyfeiriad IP dewis mewn",
"settings.privacy.recordOptinIPHelp": "Cofnodi cyfeiriad IP ar bwyntio dwbl yn manylion tanysgrifiwr.",
"settings.restart": "Ailgychwyn",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Allwedd Safle hCaptcha.com",
"settings.security.captchaKeyHelp": "Ewch i www.hcaptcha.com i gael yr allwedd a'r hymwerydd.",
"settings.security.captchaSecret": "Cyfrinach Safle hCaptcha.com",
"settings.security.enableCaptcha": "Galluogi CAPTCHA",
"settings.security.enableCaptchaHelp": "Galluogi CAPTCHA ar y ffurflen tanysgrifiad cyhoeddus.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Diogelwch",
"settings.smtp.customHeaders": "Penynnau personol",
"settings.smtp.customHeadersHelp": "Ystod eang o bennynau e-bost i'w cynnwys mewn negeseuon a anfonir gan y gweinydd hwn. ee: [{\"\"X-Custom\"\": \"\"gwerth\"\"}",
@@ -589,6 +604,39 @@
"templates.preview": "Rhagolwg",
"templates.rawHTML": "HTML crai",
"templates.subject": "Pwnc",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Mewngofnodi",
- "users.logout": "Allgofnodi"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Allgofnodi",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/da.json b/i18n/da.json
index 060c1f700..990d6290d 100644
--- a/i18n/da.json
+++ b/i18n/da.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Aktiveret",
"globals.buttons.insert": "Indsætte",
"globals.buttons.learnMore": "Lær mere",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Mere",
"globals.buttons.new": "Ny",
"globals.buttons.ok": "Ok",
"globals.buttons.remove": "Fjerne",
"globals.buttons.save": "Spare",
"globals.buttons.saveChanges": "Gem ændringer",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Vis",
"globals.days.0": "Søn",
"globals.days.1": "Søn",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} ikke fundet",
"globals.messages.passwordChange": "Indtast en værdi, der skal ændres",
"globals.messages.passwordChangeFull": "Ryd og indtast den fulde adgangskode igen i '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Langsomme forespørgsler bliver gemt i cache. Nogle tal på denne side vil eventuelt ikke være opdateret.",
"globals.messages.updated": "\"{name}\" opdateret",
"globals.months.1": "Jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Skabelon | Skabeloner",
"globals.terms.templates": "Skabeloner",
"globals.terms.tx": "Transaktionel | Transaktionel",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "År | År",
"import.alreadyRunning": "Der kører allerede en import. Vent på, at den er færdig eller stopper, før du prøver igen.",
"import.blocklist": "Blokeringsliste",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} poster",
"import.stopImport": "Stop importen",
"import.subscribe": "Abonnér",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importer abonnenter",
"import.upload": "Upload",
"lists.confirmDelete": "Er du sikker? Dette sletter ikke abonnenter.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Fejl ved lagring af miniaturebillede: {error}",
"media.errorUploading": "Fejl ved upload af fil: {error}",
"media.invalidFile": "Ugyldig fil: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Medie",
"media.unsupportedFileType": "Ikke-understøttet filtype ({type})",
"media.upload": "Upload",
@@ -505,11 +512,19 @@
"settings.privacy.recordOptinIP": "Optag opt-in IP-adresse",
"settings.privacy.recordOptinIPHelp": "Optag IP-adressen for dobbelt opt-ins i abonnentattributter.",
"settings.restart": "Genstart",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Besøg www.hcaptcha.com for at få nøglen og hemmeligheden.",
"settings.security.captchaSecret": "hCaptcha.com hemmelighed",
"settings.security.enableCaptcha": "Aktiver CAPTCHA",
"settings.security.enableCaptchaHelp": "Aktivér CAPTCHA på den offentlige abonnementsformular.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Sikkerhed",
"settings.smtp.customHeaders": "Brugerdefinerede overskrifter",
"settings.smtp.customHeadersHelp": "Valgfrit udvalg af e-mail-brevhoveder, der skal medtages i alle meddelelser, der sendes fra denne server. f.eks.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -585,6 +600,39 @@
"templates.preview": "Forhåndsvisning",
"templates.rawHTML": "Rå HTML",
"templates.subject": "Emne",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Log ind",
- "users.logout": "Log ud"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Log ud",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/de.json b/i18n/de.json
index b15220a3d..0f7b196aa 100644
--- a/i18n/de.json
+++ b/i18n/de.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Aktiviert",
"globals.buttons.insert": "Einfügen",
"globals.buttons.learnMore": "Erfahre mehr",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Mehr",
"globals.buttons.new": "Neu",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Entfernen",
"globals.buttons.save": "Speichern",
"globals.buttons.saveChanges": "Änderungen speichern",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Anzeigen",
"globals.days.0": "So",
"globals.days.1": "So",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} nicht gefunden",
"globals.messages.passwordChange": "Gib dein Passwort für die Änderung ein",
"globals.messages.passwordChangeFull": "Löschen und das vollständige Passwort in '{name}' erneut eingeben.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Langsame Abfragen werden zwischengespeichert. Einige Zahlen auf dieser Seite werden möglicherweise nicht aktuell sein.",
"globals.messages.updated": "\"{name}\" aktualisiert",
"globals.months.1": "Jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Vorlage | Vorlagen",
"globals.terms.templates": "Vorlagen",
"globals.terms.tx": "Transaktion | Transaktionen",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Jahr | Jahre",
"import.alreadyRunning": "Bitte warte bis der aktuelle Importvorgang beendet wurde.",
"import.blocklist": "Sperrliste",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} Einträge",
"import.stopImport": "Import stoppen",
"import.subscribe": "Abonnieren",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Abonnenten importieren",
"import.upload": "Hochladen",
"lists.confirmDelete": "Bist du sicher? Das Löschen einer Liste löscht keine Abonnenten.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Fehler beim Speichern des Thumbnails: {error}",
"media.errorUploading": "Fehler beim Hochladen der Datei: {error}",
"media.invalidFile": "Ungültige Datei: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Medien",
"media.unsupportedFileType": "Nicht unterstützter Dateityp ({type})",
"media.upload": "Hochladen",
@@ -505,11 +512,19 @@
"settings.privacy.recordOptinIP": "Opt-in-IP-Adresse protokollieren",
"settings.privacy.recordOptinIPHelp": "Protokollieren Sie die IP-Adresse der doppelten Einwilligung in den Abonnentenattributen.",
"settings.restart": "Neustarten",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Besuchen Sie www.hcaptcha.com, um den Schlüssel und das Geheimnis zu erhalten.",
"settings.security.captchaSecret": "hCaptcha.com Geheimnis",
"settings.security.enableCaptcha": "CAPTCHA aktivieren",
"settings.security.enableCaptchaHelp": "Aktivieren Sie CAPTCHA auf dem öffentlichen Anmeldeformular.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Sicherheit",
"settings.smtp.customHeaders": "Benutzerdefinierte Header",
"settings.smtp.customHeadersHelp": "(Optional) Array von benutzerdefinierten E-Mail Headern, welche in die Nachricht eingefügt werden sollen. Z.B.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -585,6 +600,39 @@
"templates.preview": "Vorschau",
"templates.rawHTML": "HTML",
"templates.subject": "Betreff",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Anmelden",
- "users.logout": "Abmelden"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Abmelden",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/el.json b/i18n/el.json
index f65f6f172..da091a7cd 100644
--- a/i18n/el.json
+++ b/i18n/el.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Ενεργοποιημένο",
"globals.buttons.insert": "Εισαγωγή",
"globals.buttons.learnMore": "Μάθετε περισσότερα",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Περισσότερα",
"globals.buttons.new": "Νέο",
"globals.buttons.ok": "Εντάξει",
"globals.buttons.remove": "Αφαίρεση",
"globals.buttons.save": "Αποθήκευση",
"globals.buttons.saveChanges": "Αποθήκευση αλλαγών",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Προβολή",
"globals.days.0": "Κυρ",
"globals.days.1": "Κυρ",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "Το {name} δεν βρέθηκε",
"globals.messages.passwordChange": "Εισάγετε νέο περιεχόμενο για αλλαγή",
"globals.messages.passwordChangeFull": "Εκκαθάριση και επανεισαγωγή του συνθηματικού στο '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Οι αργές ερωτήσεις αποθηκεύονται στην μνήμη cache. Ορισμένοι αριθμοί σε αυτήν τη σελίδα δεν θα είναι ενημερωμένοι.",
"globals.messages.updated": "Το \"{name}\" ενημερώθηκε",
"globals.months.1": "Ιαν",
@@ -232,6 +235,8 @@
"globals.terms.template": "Προσχέδιο | Προσχέδια",
"globals.terms.templates": "Προσχέδια",
"globals.terms.tx": "Συναλλακτική | Συναλλακτικές",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Έτος | Έτη",
"import.alreadyRunning": "Μια εισαγωγή εκτελείται ήδη. Περιμένετε να ολοκληρωθεί ή σταματήστε την πριν προσπαθήσετε ξανά.",
"import.blocklist": "Λίστα αποκλεισμού",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} εγγραφές",
"import.stopImport": "Διακοπή εισαγωγής",
"import.subscribe": "Εγγραφή",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Εισαγωγή συνδρομητών",
"import.upload": "Μεταφόρτωση",
"lists.confirmDelete": "Σίγουρα; Αυτό δεν διαγράφει τους συνδρομητές.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Σφάλμα αποθήκευσης μικρογραφίας: {error}",
"media.errorUploading": "Σφάλμα μεταφόρτωσης αρχείου: {error}",
"media.invalidFile": "Μη έγκυρο αρχείο: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Πολυμέσα",
"media.unsupportedFileType": "Μη υποστηριζόμενος τύπος αρχείου ({type})",
"media.upload": "Μεταφόρτωση",
@@ -505,11 +512,19 @@
"settings.privacy.recordOptinIP": "Καταγραφή διεύθυνσης IP με τη συγκατάθεση",
"settings.privacy.recordOptinIPHelp": "Καταγράψτε τη διεύθυνση IP της διπλής συγκατάθεσης στα χαρακτηριστικά των συνδρομητών.",
"settings.restart": "Επανεκίννηση",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "SiteKey του hCaptcha.com",
"settings.security.captchaKeyHelp": "Επισκεφθείτε το www.hcaptcha.com για να λάβετε το κλειδί και το μυστικό.",
"settings.security.captchaSecret": "Μυστικό (secret) του hCaptcha.com",
"settings.security.enableCaptcha": "Ενεργοποίηση CAPTCHA",
"settings.security.enableCaptchaHelp": "Ενεργοποιήστε το CAPTCHA στη δημόσια φόρμα εγγραφής.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Ασφάλεια",
"settings.smtp.customHeaders": "Προσαρμοσμένες επικεφαλίδες",
"settings.smtp.customHeadersHelp": "Προαιρετικός πίνακας κεφαλίδων e-mail που πρέπει να περιλαμβάνονται σε όλα τα μηνύματα που αποστέλλονται από αυτόν τον διακομιστή. π.χ.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -585,6 +600,39 @@
"templates.preview": "Προεπισκόπηση",
"templates.rawHTML": "Ακατέργαστη HTML",
"templates.subject": "Θέμα",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Σύνδεση",
- "users.logout": "Αποσύνδεση"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Αποσύνδεση",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/en.json b/i18n/en.json
index 19f4eea00..32d231614 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Enabled",
"globals.buttons.insert": "Insert",
"globals.buttons.learnMore": "Learn more",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "More",
"globals.buttons.new": "New",
"globals.buttons.ok": "Ok",
"globals.buttons.remove": "Remove",
"globals.buttons.save": "Save",
"globals.buttons.saveChanges": "Save changes",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "View",
"globals.days.0": "Sun",
"globals.days.1": "Sun",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} not found",
"globals.messages.passwordChange": "Enter a value to change",
"globals.messages.passwordChangeFull": "Clear and re-enter the full password in '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Slow queries are being cached. Some numbers on this page will not be up-to-date.",
"globals.messages.updated": "\"{name}\" updated",
"globals.months.1": "Jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Template | Templates",
"globals.terms.templates": "Templates",
"globals.terms.tx": "Transactional | Transactional",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Year | Years",
"import.alreadyRunning": "An import is already running. Wait for it to finish or stop it before trying again.",
"import.blocklist": "Blocklist",
@@ -259,7 +264,7 @@
"import.recordsCount": "{num} / {total} records",
"import.stopImport": "Stop import",
"import.subscribe": "Subscribe",
- "import.subscribeWarning":"Overwriting will re-subscribe unusbscribed e-mails. Continue?",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Import subscribers",
"import.upload": "Upload",
"lists.confirmDelete": "Are you sure? This does not delete subscribers.",
@@ -507,11 +512,19 @@
"settings.privacy.recordOptinIP": "Record opt-in IP address",
"settings.privacy.recordOptinIPHelp": "Record IP address of double opt-ins in subscriber attributes.",
"settings.restart": "Restart",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Visit www.hcaptcha.com to obtain the key and secret.",
"settings.security.captchaSecret": "hCaptcha.com secret",
"settings.security.enableCaptcha": "Enable CAPTCHA",
"settings.security.enableCaptchaHelp": "Enable CAPTCHA on the public subscription form.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Security",
"settings.smtp.customHeaders": "Custom headers",
"settings.smtp.customHeadersHelp": "Optional array of e-mail headers to include in all messages sent from this server. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -587,6 +600,39 @@
"templates.preview": "Preview",
"templates.rawHTML": "Raw HTML",
"templates.subject": "Subject",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Login",
- "users.logout": "Logout"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Logout",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/eo.json b/i18n/eo.json
index 09af8fd0d..a36f5bc82 100644
--- a/i18n/eo.json
+++ b/i18n/eo.json
@@ -1,594 +1,642 @@
-{
- "_.code": "eo",
- "_.name": "Esperanto (eo)",
- "admin.errorMarshallingConfig": "Agorderaro de klasado: {error}",
- "analytics.count": "Kalkulado",
- "analytics.fromDate": "De",
- "analytics.invalidDates": "Datoj `de` aŭ `ĝis` nevalidaj.",
- "analytics.isUnique": "Kalkuladoj estas solaj por ĉiu abonanto.",
- "analytics.links": "Ligiloj",
- "analytics.nonUnique": "La kalkuladoj ne estas solaj, ĉar la sekvado de abonantoj ne estas aktiva.",
- "analytics.title": "Indikiloj",
- "analytics.toDate": "Ĝis",
- "bounces.complaint": "Reklamacio",
- "bounces.hard": "Malmola",
- "bounces.soft": "Mola",
- "bounces.source": "Origino",
- "bounces.unknownService": "Nekonata servo",
- "bounces.view": "Vidi robotojn",
- "campaigns.addAltText": "Aldonu alt-tekston",
- "campaigns.addAttachments": "Aldonu kunsendaĵojn",
- "campaigns.archive": "Arĥivo",
- "campaigns.archiveEnable": "Publikigu en la publika arĥivo",
- "campaigns.archiveHelp": "Publikugu (sendata, haltigita, finita) la mesaĝon de kampajno en la publika arĥivo ",
- "campaigns.archiveMeta": "Metadatumoj de la kampajno",
- "campaigns.archiveMetaHelp": "Datumoj de la test-abonanto kiun oni povas uzi en la publika mesaĝo, inkluzive nomo, respoŝtadreso kaj ia ajn atributo uzata en la mesaĝo de kampajno aŭ ŝablono.",
- "campaigns.archiveSlug": "URL-nomo",
- "campaigns.archiveSlugHelp": "Mallonga nomo por la paĝo, kiu estos uzita en la publika URL, ekzemple: mia-bulteno-2",
- "campaigns.attachments": "Kunsendaĵoj",
- "campaigns.cantUpdate": "Oni ne povas ĝisdatigi kurantan kampajnon aŭ finitan kampajnon.",
- "campaigns.clicks": "Klakoj",
- "campaigns.confirmDelete": "Forviŝu {name}",
- "campaigns.confirmSchedule": "Tiu kampajno ekkomencos aŭtomate je la dato kaj horo progamitaj. Ĉu vi volas programi ĝin nun?",
- "campaigns.confirmSwitchFormat": "Enhavo povas perdi aranĝon. Ĉu vi volas daŭrigi?",
- "campaigns.content": "Enhavo",
- "campaigns.contentHelp": "Enhavo tie",
- "campaigns.continue": "Daŭrigu",
- "campaigns.copyOf": "Kopio de {name}",
- "campaigns.customHeadersHelp": "Matriu de capçaleres personalitzades per adjuntar als missatges de sortida. p. ex.: [{\"X-Custom\": \"valor\"}, {\"X-Custom2\": \"value\"}]",
- "campaigns.dateAndTime": "Data i hora",
- "campaigns.ended": "Finalitzada",
- "campaigns.errorSendTest": "S'ha produit un error en enviar la prova: {error}",
- "campaigns.fieldInvalidBody": "S'ha produït un error en compilar el cos de la campanya: {error}",
- "campaigns.fieldInvalidFromEmail": "`from_email` no vàlid.",
- "campaigns.fieldInvalidListIDs": "Identificadors de llista no vàlids.",
- "campaigns.fieldInvalidMessenger": "Canal desconegut {name}.",
- "campaigns.fieldInvalidName": "La longitud del nom no és vàlida.",
- "campaigns.fieldInvalidSendAt": "La data prevista hauria de ser en el futur.",
- "campaigns.fieldInvalidSubject": "Longitud no vàlida per a l'assumpte.",
- "campaigns.formatHTML": "Format HTML",
- "campaigns.fromAddress": "Adreça remitent",
- "campaigns.fromAddressPlaceholder": "El teu nom
",
- "campaigns.invalid": "Campanya invàlida",
- "campaigns.invalidCustomHeaders": "Capçaleres personalitzades no vàlides: {error}",
- "campaigns.markdown": "Markdown",
- "campaigns.needsSendAt": "La campanya necessita una data per ser programada.",
- "campaigns.newCampaign": "Nova campanya",
- "campaigns.noKnownSubsToTest": "No hi ha subscriptors coneguts per fer una prova.",
- "campaigns.noOptinLists": "No s'han trobat llistes opt-in per crear una campanya.",
- "campaigns.noSubs": "No hi ha subscriptors a les llistes seleccionades per crear la campanya.",
- "campaigns.noSubsToTest": "No hi ha subscriptors a qui enviar.",
- "campaigns.notFound": "No s'ha trobat la campanya.",
- "campaigns.onlyActiveCancel": "Només es poden cancel·lar les campanyes actives.",
- "campaigns.onlyActivePause": "Només es poden posar en pausa les campanyes actives.",
- "campaigns.onlyDraftAsScheduled": "Només es poden programar les campanyes en esborrany.",
- "campaigns.onlyPausedDraft": "Només es poden iniciar campanyes en pausa o en esborrany.",
- "campaigns.onlyScheduledAsDraft": "Només les campanyes programades es poden desar com a esborranys.",
- "campaigns.pause": "Pausa",
- "campaigns.plainText": "Text pla",
- "campaigns.preview": "Prèvia",
- "campaigns.progress": "Progrés",
- "campaigns.queryPlaceholder": "Nom o assumpte",
- "campaigns.rateMinuteShort": "min",
- "campaigns.rawHTML": "Codi HTML ",
- "campaigns.removeAltText": "Elimina el missatge de text pla alternatiu",
- "campaigns.richText": "Text enriquit",
- "campaigns.schedule": "Programa campanya",
- "campaigns.scheduled": "Programada",
- "campaigns.send": "Envia",
- "campaigns.sendLater": "Envia més tard",
- "campaigns.sendTest": "Envia missatge de prova",
- "campaigns.sendTestHelp": "Premeu Intro després d'escriure una adreça per afegir diversos destinataris. Les adreces han de pertànyer als subscriptors existents.",
- "campaigns.sendToLists": "Llistes a les quals s'envia",
- "campaigns.sent": "Enviada",
- "campaigns.start": "Inicia campanya",
- "campaigns.started": "\"{name}\" iniciada",
- "campaigns.startedAt": "Iniciada",
- "campaigns.stats": "Indicadors",
- "campaigns.status.cancelled": "Cancel·lada",
- "campaigns.status.draft": "Esborrany",
- "campaigns.status.finished": "Finalitzada",
- "campaigns.status.paused": "Pausada",
- "campaigns.status.running": "En curs",
- "campaigns.status.scheduled": "Programada",
- "campaigns.statusChanged": "\"{name}\" està {status}",
- "campaigns.subject": "Assumpte",
- "campaigns.templatingRef": "Referència de plantilles",
- "campaigns.testEmails": "Adreces de correu electrònic",
- "campaigns.testSent": "S'ha enviat el missatge de prova",
- "campaigns.timestamps": "Segells de temps",
- "campaigns.trackLink": "Enllaç de seguiment",
- "campaigns.views": "Visualitzacions",
- "dashboard.campaignViews": "Visualitzacions de la campanya",
- "dashboard.linkClicks": "Clics a enllaços",
- "dashboard.messagesSent": "Missatges enviats",
- "dashboard.orphanSubs": "Orfes",
- "email.data.info": "S'adjunta una còpia de totes les dades enregistrades sobre la teva persona en un fitxer en format JSON. Es pot veure en un editor de text.",
- "email.data.title": "Les teves dades ",
- "email.optin.confirmSub": "Confirma la subscripció",
- "email.optin.confirmSubHelp": "Confirmeu la terva subscripció fent clic al botó següent.",
- "email.optin.confirmSubInfo": "Heu estat afegit a les llistes següents:",
- "email.optin.confirmSubTitle": "Confirmació de la subscrpció",
- "email.optin.confirmSubWelcome": "Hola",
- "email.optin.privateList": "Llista privada",
- "email.status.campaignReason": "Motiu",
- "email.status.campaignSent": "Enviada",
- "email.status.campaignUpdateTitle": "Campanya actualitzada",
- "email.status.importFile": "Fitxer",
- "email.status.importRecords": "Registres",
- "email.status.importTitle": "Importació actualitzada",
- "email.status.status": "Estat",
- "email.unsub": "Desubscripció",
- "email.unsubHelp": "No voleu rebre aquests correus electrònics?",
- "email.viewInBrowser": "Veure al navegador",
- "forms.formHTML": "Format HTML",
- "forms.formHTMLHelp": "Utilitzeu l'HTML següent per mostrar un formulari de subscripció en una pàgina web externa. El formulari hauria de tenir el camp de correu electrònic i un o més camps `l` (llista UUID). El camp del nom és opcional.",
- "forms.noPublicLists": "No hi ha llistes públiques per generar formularis.",
- "forms.publicLists": "Llistes públiques",
- "forms.publicSubPage": "Pàgina de subscripció pública",
- "forms.selectHelp": "Selecciona les llistes que vols afegir al formulari.",
- "forms.title": "Formularis",
- "globals.buttons.add": "Afegeix",
- "globals.buttons.addNew": "Afegeix nou",
- "globals.buttons.back": "Enrere",
- "globals.buttons.cancel": "Cancel·la",
- "globals.buttons.clear": "Esborra",
- "globals.buttons.clearAll": "Esborra tot",
- "globals.buttons.clone": "Clona",
- "globals.buttons.close": "Tanca",
- "globals.buttons.continue": "Continua",
- "globals.buttons.copy": "Copiar",
- "globals.buttons.delete": "Esborra",
- "globals.buttons.deleteAll": "Esborra tot",
- "globals.buttons.edit": "Edita",
- "globals.buttons.enabled": "Habilitat",
- "globals.buttons.insert": "Inserta",
- "globals.buttons.learnMore": "Saber-nes més",
- "globals.buttons.more": "Més",
- "globals.buttons.new": "Nou",
- "globals.buttons.ok": "D'acord",
- "globals.buttons.remove": "Elimina",
- "globals.buttons.save": "Desa",
- "globals.buttons.saveChanges": "Desa els canvis",
- "globals.buttons.view": "Visualitzar",
- "globals.days.0": "dg.",
- "globals.days.1": "dg.",
- "globals.days.2": "dl.",
- "globals.days.3": "dt.",
- "globals.days.4": "dc.",
- "globals.days.5": "dj.",
- "globals.days.6": "dv.",
- "globals.days.7": "ds.",
- "globals.fields.createdAt": "Creat",
- "globals.fields.description": "Descripció",
- "globals.fields.id": "ID",
- "globals.fields.name": "Nom",
- "globals.fields.status": "Estat",
- "globals.fields.type": "Tipus",
- "globals.fields.updatedAt": "Actualitzat",
- "globals.fields.uuid": "UUID",
- "globals.messages.confirm": "Estàs segur?",
- "globals.messages.confirmDiscard": "Vols descartar els canvis?",
- "globals.messages.copied": "Copiat",
- "globals.messages.created": "\"{name}\" ha estat creat",
- "globals.messages.deleted": "\"{name}\" ha estat esborrat",
- "globals.messages.deletedCount": "{name} ({num}) ha estat esborrat",
- "globals.messages.done": "Fet",
- "globals.messages.emptyState": "No hi ha res aquí",
- "globals.messages.errorCreating": "Error en crear {name}: {error}",
- "globals.messages.errorDeleting": "Error en esborrar {name}: {error}",
- "globals.messages.errorFetching": "Error en obtenir {name}: {error}",
- "globals.messages.errorInvalidIDs": "Un o més identificadors no són vàlids: {error}",
- "globals.messages.errorUUID": "Error en generar UUID: {error}",
- "globals.messages.errorUpdating": "Error en actualitzar {name}: {error}",
- "globals.messages.internalError": "Error del servidor intern",
- "globals.messages.invalidData": "Dades no vàlides",
- "globals.messages.invalidFields": "Camps no vàlids: {name}",
- "globals.messages.invalidID": "ID(s) no vàlid",
- "globals.messages.invalidUUID": "UUID(s) no vàlid",
- "globals.messages.missingFields": "Falten camps: {name}",
- "globals.messages.notFound": "No s'ha trobat {name} ",
- "globals.messages.passwordChange": "Introduïu un valor per canviar",
- "globals.messages.passwordChangeFull": "Buida i torna a introduir la contrasenya completa a '{name}'.",
- "globals.messages.slowQueriesCached": "Les consultes lentes s'estan emmagatzemant en memòria cau. Algunes xifres en aquesta pàgina no seran actuals.",
- "globals.messages.updated": "\"{name}\" actualitzat",
- "globals.months.1": "gen.",
- "globals.months.10": "oct.",
- "globals.months.11": "nov.",
- "globals.months.12": "des.",
- "globals.months.2": "febr.",
- "globals.months.3": "març",
- "globals.months.4": "abr.",
- "globals.months.5": "maig",
- "globals.months.6": "juny",
- "globals.months.7": "jul.",
- "globals.months.8": "ag.",
- "globals.months.9": "set.",
- "globals.states.off": "Apagat",
- "globals.terms.all": "Tot",
- "globals.terms.analytics": "Indicadors",
- "globals.terms.bounce": "Rebot | Rebots",
- "globals.terms.bounces": "Rebots",
- "globals.terms.campaign": "Campanya | Campanyes",
- "globals.terms.campaigns": "Campanyes",
- "globals.terms.dashboard": "Taulell",
- "globals.terms.day": "Dia | Dies",
- "globals.terms.hour": "Hora | Hores",
- "globals.terms.list": "Llista | Llistes",
- "globals.terms.lists": "Llistes",
- "globals.terms.media": "Mèdia | Mèdia",
- "globals.terms.messenger": "Canal | Canals",
- "globals.terms.messengers": "Canals",
- "globals.terms.minute": "Minut | Minuts",
- "globals.terms.month": "Mes | Mesos",
- "globals.terms.none": "Cap",
- "globals.terms.second": "Segon | Segons",
- "globals.terms.settings": "Configuració",
- "globals.terms.subscriber": "Subscriptor | Subscriptors",
- "globals.terms.subscribers": "Subscriptors",
- "globals.terms.subscriptions": "Subscripció | Subscripcions",
- "globals.terms.tag": "Etiqueta | Etiquetes",
- "globals.terms.tags": "Etiquetes",
- "globals.terms.template": "Plantilla | Plantilles",
- "globals.terms.templates": "Plantilles",
- "globals.terms.tx": "Transaccional | Transaccionals",
- "globals.terms.year": "Any | Anys",
- "import.alreadyRunning": "Ja s'està executant una importació. Espereu que acabi o atureu-lo abans de tornar-ho a provar.",
- "import.blocklist": "Llista de bloqueig",
- "import.csvDelim": "Delimitador CSV",
- "import.csvDelimHelp": "El delimitador predeterminat és la coma.",
- "import.csvExample": "Exemple de CSV en brut",
- "import.csvFile": "Fitxer CSV o ZIP",
- "import.csvFileHelp": "Feu clic o arrossegueu un fitxer CSV o ZIP aquí",
- "import.errorCopyingFile": "Error en copiar el fitxer: {error}",
- "import.errorProcessingZIP": "Error en processar el fitxer ZIP: {error}",
- "import.errorStarting": "Error en iniciar la importació: {error}",
- "import.importDone": "Fet",
- "import.importStarted": "S'ha iniciat la importació",
- "import.instructions": "Instruccions",
- "import.instructionsHelp": "Carrega un fitxer CSV o un fitxer ZIP amb un únic fitxer CSV per importar subscriptors de forma massiva. El fitxer CSV hauria de tenir les capçaleres següents amb els noms exactes de les columnes. els atributs (opcional) han de ser una cadena JSON vàlida amb cometes dobles.",
- "import.invalidDelim": "El delimitador ha de ser un sol caràcter.",
- "import.invalidFile": "Fitxer no vàlid: {error}",
- "import.invalidMode": "Mode no vàlid",
- "import.invalidParams": "Paràmetres no vàlids: {error}",
- "import.invalidSubStatus": "Estat de subscripció no vàlid",
- "import.listSubHelp": "Llistes a les quals subscriure's.",
- "import.mode": "Mode",
- "import.overwrite": "Vols sobreescriure?",
- "import.overwriteHelp": "Vols sobreescriure el nom, els atributs i l'estat de la subscripció dels subscriptors existents?",
- "import.recordsCount": "{num} / {total} registres",
- "import.stopImport": "Atura la importació",
- "import.subscribe": "Subscriu",
- "import.title": "Importa subscriptors",
- "import.upload": "Carrega",
- "lists.confirmDelete": "Estàs segur? Això no elimina els subscriptors.",
- "lists.confirmSub": "Confirmeu les subscripcions a {name}",
- "lists.invalidName": "Nom no vàlid",
- "lists.newList": "Nova llista",
- "lists.optin": "Opt-in",
- "lists.optinHelp": "El doble opt-in envia un correu electrònic al subscriptor demanant confirmació. A les llistes de doble subscripció, les campanyes només s'envien als subscriptors confirmats.",
- "lists.optinTo": "Fes opt-in a {name}",
- "lists.optins.double": "Doble opt-in",
- "lists.optins.single": "Opt-in simple",
- "lists.sendCampaign": "Envia campanya",
- "lists.sendOptinCampaign": "Envia campanya opt-in ",
- "lists.type": "Tipus",
- "lists.typeHelp": "Les llistes públiques estan obertes a tothom per subscriure's i els seus noms poden aparèixer a pàgines públiques com ara la pàgina de gestió de subscripcions.",
- "lists.types.private": "Privatt",
- "lists.types.public": "Públic",
- "logs.title": "Registres",
- "maintenance.help": "Algunes accions poden trigar una estona a completar-se en funció de la quantitat de dades.",
- "maintenance.maintenance.unconfirmedOptins": "Subscripcions opt-in no confirmades",
- "maintenance.olderThan": "Més antic de",
- "maintenance.orphanHelp": "Orfes = subscriptors sense llistes",
- "maintenance.title": "Manteniment",
- "maintenance.unconfirmedSubs": "Subscripcions no confirmades més antigues de {name} dies.",
- "media.errorReadingFile": "Error en llegir el fitxer: {error}",
- "media.errorResizing": "Error en canviar la mida de la imatge: {error}",
- "media.errorSavingThumbnail": "Error en desar la miniatura: {error}",
- "media.errorUploading": "Error en carregar el fitxer: {error}",
- "media.invalidFile": "Fitxer no vàlid: {error}",
- "media.title": "Mèdia",
- "media.unsupportedFileType": "El tipus de fitxer ({type}) no és compatible",
- "media.upload": "Carrega",
- "media.uploadHelp": "Fes clic o arrossega una o més imatges aquí",
- "media.uploadImage": "Carrega la imatge",
- "menu.allCampaigns": "Totes les campanyes",
- "menu.allLists": "Totes les llistes",
- "menu.allSubscribers": "Tots els subscriptors",
- "menu.dashboard": "Taulell",
- "menu.forms": "Formularis",
- "menu.import": "Importació",
- "menu.logs": "Registres",
- "menu.maintenance": "Manteniment",
- "menu.media": "Mèdia",
- "menu.newCampaign": "Crea nova",
- "menu.settings": "Configuració",
- "public.archiveEmpty": "Sense missatges arxivats actualment.",
- "public.archiveTitle": "Arxiu de la llista de correu",
- "public.blocklisted": "Desubscrit de forma permanent.",
- "public.campaignNotFound": "No s'ha trobat el missatge de correu electrònic.",
- "public.confirmOptinSubTitle": "Confirmació de la subscripció",
- "public.confirmSub": "Confirma la subscripció",
- "public.confirmSubInfo": "Has estat afegit a les llistes següents:",
- "public.confirmSubTitle": "Confirmació",
- "public.dataRemoved": "S'han eliminat les vostres subscripcions i totes les dades associades.",
- "public.dataRemovedTitle": "Eliminació de dades",
- "public.dataSent": "Les teves dades t'han estat enviades per correu electrònic com a fitxer adjunt.",
- "public.dataSentTitle": "Dades enviades per correu electrònic",
- "public.errorFetchingCampaign": "S'ha produït un error en obtenir el missatge de correu electrònic.",
- "public.errorFetchingEmail": "No s'ha trobat el missatge de correu electrònic",
- "public.errorFetchingLists": "S'ha produït un error en obtenir les llistes. Si us plau, torna-ho a provar.",
- "public.errorProcessingRequest": "S'ha produït un error en processar la sol·licitud. Si us plau, torna-ho a provar.",
- "public.errorTitle": "Error",
- "public.invalidCaptcha": "CAPTCHA no vàlid.",
- "public.invalidFeature": "Aquesta funció no està disponible.",
- "public.invalidLink": "Enllaç no vàlid",
- "public.managePrefs": "Gestiona les preferències",
- "public.managePrefsUnsub": "Desmarca les llistes de les quals vols fer-ne la desubscripció.",
- "public.noListsAvailable": "No hi ha llistes disponibles per subscriure's.",
- "public.noListsSelected": "No s'han seleccionat llistes vàlides per subscriure's.",
- "public.noSubInfo": "No hi ha subscripcions per confirmar.",
- "public.noSubTitle": "No hi ha subscripcions ",
- "public.notFoundTitle": "No trobat",
- "public.poweredBy": "Desenvolupat per",
- "public.prefsSaved": "Les teves preferències han estat desades.",
- "public.privacyConfirmWipe": "Estàs segur que vols suprimir totes les dades de la teva subscripció de manera permanent?",
- "public.privacyExport": "Exporta les teves dades",
- "public.privacyExportHelp": "Se t'enviarà per correu electrònic una còpia de les teves dades.",
- "public.privacyTitle": "Privadesa i dades",
- "public.privacyWipe": "Esborra permanentment les teves dades",
- "public.privacyWipeHelp": "Suprimeix totes les teves subscripcions i dades relacionades de la base de dades de manera permanent.",
- "public.sub": "Subscriu",
- "public.subConfirmed": "T'has subscrit correctament.",
- "public.subConfirmedTitle": "Confirmat",
- "public.subName": "Nom (opcional)",
- "public.subNotFound": "No s'ha trobat la subscripció.",
- "public.subOptinPending": "S'ha enviat un correu electrònic per confirmar les teves subscripcions.",
- "public.subPrivateList": "Llista privada",
- "public.subTitle": "Subscripció",
- "public.unsub": "Desubscriu",
- "public.unsubFull": "També dona't de baixa de tots els futurs correus electrònics.",
- "public.unsubHelp": "Vols donar-te de baixa d'aquesta llista de correu?",
- "public.unsubTitle": "Desubscriu",
- "public.unsubbedInfo": "Has cancel·lat la subscripció correctament.",
- "public.unsubbedTitle": "Desubscrit",
- "public.unsubscribeTitle": "Cancel·lació de la subscripció a la llista de correu",
- "settings.appearance.adminHelp": "CSS personalitzat per aplicar a la interfície d'administració.",
- "settings.appearance.adminName": "Administrador",
- "settings.appearance.customCSS": "CSS personalitzats",
- "settings.appearance.customJS": "JavaScript personalitzat",
- "settings.appearance.name": "Aparença",
- "settings.appearance.publicHelp": "CSS i JavaScript personalitzats per aplicar-los a les pàgines públiques.",
- "settings.appearance.publicName": "Públic",
- "settings.bounces.action": "Acció",
- "settings.bounces.blocklist": "Llista de bloqueig",
- "settings.bounces.complaint": "Complaint",
- "settings.bounces.count": "Recompte de rebots",
- "settings.bounces.countHelp": "Nombre de rebots per subscriptor",
- "settings.bounces.delete": "Esborra",
- "settings.bounces.enable": "Activa el processament de rebots",
- "settings.bounces.enableMailbox": "Activa la bústia de rebots",
- "settings.bounces.enablePostmark": "Activa Postmark",
- "settings.bounces.enableSES": "Activa SES",
- "settings.bounces.enableSendgrid": "Activa SendGrid",
- "settings.bounces.enableWebhooks": "Activa els webhooks pels rebots",
- "settings.bounces.enabled": "Activat",
- "settings.bounces.folder": "Carpeta",
- "settings.bounces.folderHelp": "Nom de la carpeta IMAP a escanejar. Ex: Safata d'entrada.",
- "settings.bounces.hard": "Hard",
- "settings.bounces.invalidScanInterval": "L'interval d'escaneig ha de ser com a mínim d'1 minut.",
- "settings.bounces.name": "Rebots",
- "settings.bounces.none": "Cap",
- "settings.bounces.postmarkPassword": "Contrasenya de Postmark",
- "settings.bounces.postmarkUsername": "Nom d'usuari de Postmark",
- "settings.bounces.postmarkUsernameHelp": "Postmark permet activar l'autorització bàsica per als webhooks. Assegureu-vos d'introduir les mateixes credencials aquí i en la configuració del webhook de Postmark.",
- "settings.bounces.scanInterval": "Interval d'escaneig",
- "settings.bounces.scanIntervalHelp": "Interval en què s'hauria d'escanejar la bústia de rebot (s per segon, m per minut).",
- "settings.bounces.sendgridKey": "Clau SendGrid ",
- "settings.bounces.soft": "Soft",
- "settings.bounces.type": "Tipus",
- "settings.bounces.username": "Usuari",
- "settings.confirmRestart": "Assegura't que les campanyes en curs estiguin en pausa. Reinicia?",
- "settings.duplicateMessengerName": "Nom del canal duplicat: {name}",
- "settings.errorEncoding": "Error en la configuració de codificació: {error}",
- "settings.errorNoSMTP": "S'ha d'habilitar almenys un bloc SMTP",
- "settings.general.adminNotifEmails": "Correu electrònic de notificació de l'administrador",
- "settings.general.adminNotifEmailsHelp": "Llista d'adreces de correu electrònic separades per comes a les quals s'han d'enviar notificacions d'administrador, com ara actualitzacions d'importació, finalització de campanya, errors, etc.",
- "settings.general.checkUpdates": "Busca actualitzacions",
- "settings.general.checkUpdatesHelp": "Comprova periòdicament si hi ha noves versions d'aplicacions i notifica-ho.",
- "settings.general.enablePublicArchive": "Enable public mailing list archive page",
- "settings.general.enablePublicArchiveHelp": "Publica les campanyes on arxivar està habilitat en el lloc web públic.",
- "settings.general.enablePublicArchiveRSSContent": "Mostra tot el contingut a l'arxiu RSS públic",
- "settings.general.enablePublicArchiveRSSContentHelp": "Mostra el contingut complet del correu electrònic a l'aliment RSS. Si està desactivat, només es mostren els elements del títol i l'enllaç.",
- "settings.general.enablePublicSubPage": "Activa la pàgina de subscripció pública",
- "settings.general.enablePublicSubPageHelp": "Mostra una pàgina de subscripció pública amb totes les llistes públiques perquè la gent es subscrigui.",
- "settings.general.faviconURL": "URL del favicon",
- "settings.general.faviconURLHelp": "(Opcional) URL completa del favicon estàtic que serà visible a l'usuari, com ara la pàgina de cancel·lació de la subscripció.",
- "settings.general.fromEmail": "Correu electrònic \"Remitent\" per defecte",
- "settings.general.fromEmailHelp": "El correu electrònic `remitent` es mostra per defecte als correus electrònics de campanya sortints. Això es pot canviar per cada campanya.",
- "settings.general.language": "Idioma",
- "settings.general.logoURL": "URL del logotip",
- "settings.general.logoURLHelp": "(Opcional) URL completa del logotip estàtic que serà visible a l'usuari, com ara la pàgina de cancel·lació de la subscripció.",
- "settings.general.name": "General",
- "settings.general.rootURL": "URL arrel",
- "settings.general.rootURLHelp": "URL públic de la instal·lació (sense barra inclinada).",
- "settings.general.sendOptinConfirm": "Envia opt-in de confirmació",
- "settings.general.sendOptinConfirmHelp": "Envia un correu electrònic de confirmació de l'opt-in quan els subscriptors s'inscriguin mitjançant el formulari públic o quan l'administrador els afegeixi.",
- "settings.general.siteName": "Nom del lloc web",
- "settings.invalidMessengerName": "Nom de canal no vàlid",
- "settings.mailserver.authProtocol": "Protocol d'autenticació",
- "settings.mailserver.host": "Amfitrió",
- "settings.mailserver.hostHelp": "Adreça host del servidor SMTP.",
- "settings.mailserver.idleTimeout": "Temps d'espera d'inactivitat",
- "settings.mailserver.idleTimeoutHelp": "Temps d'inactivitat per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la de la grup (s per segon, m per minut).",
- "settings.mailserver.maxConns": "Connexions màximes",
- "settings.mailserver.maxConnsHelp": "Màxim de connexions concurrents al servidor.",
- "settings.mailserver.password": "Contrasenya",
- "settings.mailserver.passwordHelp": "Fes intro per canviar",
- "settings.mailserver.port": "Port",
- "settings.mailserver.portHelp": "Port del servidor SMTP.",
- "settings.mailserver.skipTLS": "Omet la verificació TLS",
- "settings.mailserver.skipTLSHelp": "Omet la comprovació del hostname al certificat TLS.",
- "settings.mailserver.tls": "TLS",
- "settings.mailserver.tlsHelp": "Xifratge TLS/SSL. STARTTLS s'utilitza habitualment.",
- "settings.mailserver.username": "Usuari",
- "settings.mailserver.waitTimeout": "Espera el timeout",
- "settings.mailserver.waitTimeoutHelp": "Temps per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la del grup (s per segon, m per minut).",
- "settings.maintenance.cron": "Interval de cron",
- "settings.media.provider": "Proveïdor",
- "settings.media.s3.bucket": "Contenidor",
- "settings.media.s3.bucketPath": "Ruta del contenidor",
- "settings.media.s3.bucketPathHelp": "Ruta dins del contenidor per carregar fitxers. El valor per defecte és /",
- "settings.media.s3.bucketType": "Tipus de contenidor",
- "settings.media.s3.bucketTypePrivate": "Privat",
- "settings.media.s3.bucketTypePublic": "Públic",
- "settings.media.s3.key": "Clau d'accés AWS",
- "settings.media.s3.publicURL": "URL públic personalitzada (opcional)",
- "settings.media.s3.publicURLHelp": "Domini S3 personalitzat per ser usat en enllaços a imatges en lloc de l'URL backend S3 predeterminada.",
- "settings.media.s3.region": "Regió",
- "settings.media.s3.secret": "Secret d'accés AWS",
- "settings.media.s3.uploadExpiry": "Caducitat de la càrrega",
- "settings.media.s3.uploadExpiryHelp": "(Opcional) Especifica TTL per a l'URL presignada generada. Només aplicable a contenidors privats (s, m, h, d per a segons, minuts, hores, dies).",
- "settings.media.s3.url": "URL del backend S3",
- "settings.media.s3.urlHelp": "Canvia només si fas servir un backend personalitzat compatible amb S3 com Minio.",
- "settings.media.title": "Càrrega de mèdia",
- "settings.media.upload.extensions": "Extensions de fitxers permeses",
- "settings.media.upload.extensionsHelp": "Afegiu * per permetre totes les extensions",
- "settings.media.upload.path": "Ruta de càrrega",
- "settings.media.upload.pathHelp": "Ruta al directori on es carregaran els mèdia.",
- "settings.media.upload.uri": "Carrega URI",
- "settings.media.upload.uriHelp": "Carrega un URI visible per al tothom. Els mèdia carregats a upload_path seran accessibles públicament a {root_url}, per exemple, https://listmonk.yoursite.com/upload",
- "settings.messengers.maxConns": "Connexions màxiomes",
- "settings.messengers.maxConnsHelp": "Màxim nombre de connexions concurrents al servidor.",
- "settings.messengers.messageSaved": "S'ha desat la configuració. S'està tornant a carregar l'aplicació...",
- "settings.messengers.name": "Canals",
- "settings.messengers.nameHelp": "ex: my-sms. Alfanumèric / guió.",
- "settings.messengers.password": "Contrasenya",
- "settings.messengers.retries": "Reintents",
- "settings.messengers.retriesHelp": "Nombre de vegades que cal tornar a intentar quan un missatge falla.",
- "settings.messengers.skipTLSHelp": "Omet la comprovació del hostname al certificat TLS.",
- "settings.messengers.timeout": "Temps d'espera d'inactivitat",
- "settings.messengers.timeoutHelp": "Temps per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la del grup (s per segon, m per minut).",
- "settings.messengers.url": "URL",
- "settings.messengers.urlHelp": "URL arrel del servidor Postback.",
- "settings.messengers.username": "Usuari",
- "settings.needsRestart": "La configuració ha canviat. Posa en pausa totes les campanyes en curs i reinicia l'aplicació",
- "settings.performance.batchSize": "Mida del lot",
- "settings.performance.batchSizeHelp": "El nombre de subscriptors que cal extreure de la base de dades en una sola iteració. Cada iteració extreu subscriptors de la base de dades, els envia missatges i després passa a la següent iteració per extreure el següent lot. Idealment, hauria de ser superior al rendiment màxim possible (concurrency * message_rate).",
- "settings.performance.cacheSlowQueries": "Memòria cau de consultes lentes a la base de dades",
- "settings.performance.cacheSlowQueriesHelp": "Només habiliteu-ho en bases de dades grans que s'hagin tornat significativament més lentes. Emmagatzema en memòria el compte de subscriptors de llista, les estadístiques del tauler de comandament, etc.",
- "settings.performance.concurrency": "Concurrència",
- "settings.performance.concurrencyHelp": "Màxim treballador concurrent (fils) que intentarà enviar missatges simultàniament.",
- "settings.performance.maxErrThreshold": "Llindar d'error màxim",
- "settings.performance.maxErrThresholdHelp": "El nombre d'errors (p. ex.: temps d'espera SMTP durant l'enviament de correu electrònic) que ha de tolerar una campanya en execució abans d'aturar-la per a una investigació o intervenció manual. Estableix a 0 per no fer mai una pausa.",
- "settings.performance.messageRate": "Rati de missatges",
- "settings.performance.messageRateHelp": "Nombre màxim de missatges a enviar per segon per treballador en un segon. Si concurrència = 10 i message_rate = 10, es poden enviar fins a 10x10 = 100 missatges cada segon. Això, juntament amb la concurrència, s'hauria d'ajustar per mantenir els missatges nets sortint per segon sota els límits dels servidors de missatges objectiu, si n'hi ha.",
- "settings.performance.name": "Rendiment",
- "settings.performance.slidingWindow": "Activa el límit de la finestra lliscant",
- "settings.performance.slidingWindowDuration": "Durada",
- "settings.performance.slidingWindowDurationHelp": "Durada del període de la finestra lliscant (m per minut, h per hora).",
- "settings.performance.slidingWindowHelp": "Limita el nombre total de missatges que s'envien en un període determinat. Quan s'arriba a aquest límit, els missatges es retenen des de l'enviament fins que s'esborra la finestra de temps.",
- "settings.performance.slidingWindowRate": "Missatges màxims",
- "settings.performance.slidingWindowRateHelp": "Nombre màxim de missatges per enviar dins de la durada de la finestra.",
- "settings.privacy.allowBlocklist": "Permet la llista de bloqueig",
- "settings.privacy.allowBlocklistHelp": "Vols permetre als subscriptors donar-se de baixa de totes les llistes de correu i marcar-se com a llista bloquejada?",
- "settings.privacy.allowExport": "Permet l'exportació",
- "settings.privacy.allowExportHelp": "Vols permetre als subscriptors exportar les dades recollides sobre ells?",
- "settings.privacy.allowPrefs": "Permet fer canvis de les preferències",
- "settings.privacy.allowPrefsHelp": "Permet als subscriptors fer canvis de les preferències tals com els seus noms o la subscripció a múltiples llistes.",
- "settings.privacy.allowWipe": "Permet l'esborrat permanent",
- "settings.privacy.allowWipeHelp": "Permet als subscriptors esborrar-se, incloses les seves subscripcions i totes les altres dades de la base de dades. Les visualitzacions de campanya i els clics als enllaços també s'eliminen mentre es mantenen les visualitzacions i els recomptes de clics (sense subscriptors associats a ells) de manera que les estadístiques i els indicadors no es veuran afectats.",
- "settings.privacy.domainBlocklist": "Llista de dominis bloquejats",
- "settings.privacy.domainBlocklistHelp": "No es permet la subscripció a les adreces de correu electrònic amb aquests dominis. Introduïu un domini per línia, per exemple: somesite.com",
- "settings.privacy.individualSubTracking": "Seguiment individual de subscriptors",
- "settings.privacy.individualSubTrackingHelp": "Feu un seguiment de les visualitzacions i dels clics de la campanya a nivell de subscriptor. Quan està desactivat, el seguiment de visualitzacions i de clics continua disponible sense estar enllaçat a subscriptors individuals.",
- "settings.privacy.listUnsubHeader": "Inclou la capçalera `List-Unsubscribe`",
- "settings.privacy.listUnsubHeaderHelp": "Inclou capçaleres de cancel·lació de subscripció que permetin als clients de correu electrònic permetre als usuaris donar-se de baixa amb un sol clic.",
- "settings.privacy.name": "Privadesa",
- "settings.privacy.recordOptinIP": "Registra l'adreça IP de l'opt-in",
- "settings.privacy.recordOptinIPHelp": "Registra l'adreça IP dels opt-ins dobles en els atributs del subscrit.",
- "settings.restart": "Reinicia",
- "settings.security.captchaKey": "Clau del lloc hCaptcha.com",
- "settings.security.captchaKeyHelp": "Visiteu www.hcaptcha.com per obtenir la clau i el secret.",
- "settings.security.captchaSecret": "Secret del lloc hCaptcha.com",
- "settings.security.enableCaptcha": "Habilita el CAPTCHA",
- "settings.security.enableCaptchaHelp": "Habilita el CAPTCHA al formulari públic de subscripció.",
- "settings.security.name": "Seguretat",
- "settings.smtp.customHeaders": "Capçaleres personalitzades",
- "settings.smtp.customHeadersHelp": "Matriu opcional de capçaleres de correu electrònic per incloure en tots els missatges enviats des d'aquest servidor. p. ex.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
- "settings.smtp.enabled": "Habilitat",
- "settings.smtp.heloHost": "Nom d'amfitrió HELO",
- "settings.smtp.heloHostHelp": "Opcional. Alguns servidors SMTP requereixen un FQDN al hostname. Per defecte, HELLO va amb `localhost`. Estableix-loo si s'ha d'utilitzar un hostname personalitzat.",
- "settings.smtp.name": "SMTP",
- "settings.smtp.retries": "Reintents",
- "settings.smtp.retriesHelp": "Nombre de vegades que cal tornar a intentar quan un missatge falla.",
- "settings.smtp.sendTest": "Envia el correu electrònic",
- "settings.smtp.setCustomHeaders": "Estableix capçaleres personalitzades",
- "settings.smtp.testConnection": "Prova de connexió",
- "settings.smtp.testEnterEmail": "Introduïu la contrasenya per provar",
- "settings.smtp.toEmail": "Destinatari del correu electrònic",
- "settings.title": "Configuració",
- "settings.updateAvailable": "Hi ha disponible una nova actualització {versió}.",
- "subscribers.advancedQuery": "Avançat",
- "subscribers.advancedQueryHelp": "Expressió SQL parcial per consultar els atributs del subscriptor",
- "subscribers.attribs": "Atributs",
- "subscribers.attribsHelp": "Els atributs es defineixen com un mapa JSON, per exemple:",
- "subscribers.blocklistedHelp": "Els subscriptors bloquejats no rebran mai cap correu electrònic.",
- "subscribers.confirmBlocklist": "Afegir a la llista de bloqueig {nombre} subscriptors?",
- "subscribers.confirmDelete": "Esborrar {num} subscriptors(s)?",
- "subscribers.confirmExport": "Exportar {num} subscriptor(s)?",
- "subscribers.domainBlocklisted": "El domini de correu electrònic està bloquejat.",
- "subscribers.downloadData": "Descarrega les dades",
- "subscribers.email": "Correu electrònic",
- "subscribers.emailExists": "El correu electrònic ja existeix.",
- "subscribers.errorBlocklisting": "Error en afegir a la llista de bloqueig els subscriptors: {error}",
- "subscribers.errorNoIDs": "No s'han facilitat IDs.",
- "subscribers.errorNoListsGiven": "No es troben llistes.",
- "subscribers.errorPreparingQuery": "Error en preparar la consulta de subscriptor: {error}",
- "subscribers.errorSendingOptin": "Error en enviar el correu electrònic d'opt-in.",
- "subscribers.export": "Exportació",
- "subscribers.invalidAction": "Acció no vàlida.",
- "subscribers.invalidEmail": "Correu electroǹic no vàlid.",
- "subscribers.invalidJSON": "JSON no vàlid als atributs.",
- "subscribers.invalidName": "Nom no vàlid.",
- "subscribers.listChangeApplied": "S'ha aplicat el canvi de llista.",
- "subscribers.lists": "Llistes",
- "subscribers.listsHelp": "Les llistes de les quals els subscriptors s'han donat de baixa no es poden eliminar.",
- "subscribers.listsPlaceholder": "Llistes per subscriure's",
- "subscribers.manageLists": "Gestionar llistes",
- "subscribers.markUnsubscribed": "Marca com a no subscrit",
- "subscribers.newSubscriber": "Nou subscriptor",
- "subscribers.numSelected": "{num} subscriptors seleccionats",
- "subscribers.optinSubject": "Confirma la teva subscripció",
- "subscribers.preconfirm": "Preconfirmació de subscripcions",
- "subscribers.preconfirmHelp": "No envieu correus electrònics d'opt-in i marqueu totes les subscripcions a la llista com a \"subscrites\".",
- "subscribers.query": "Consulta",
- "subscribers.queryPlaceholder": "Correu electrònic o nom",
- "subscribers.reset": "Restableix",
- "subscribers.selectAll": "Selecciona'n {num}",
- "subscribers.sendOptinConfirm": "Envia la confirmació d'opt-in",
- "subscribers.sentOptinConfirm": "Confirmació d'opt-in enviada",
- "subscribers.status.blocklisted": "A la llista de bloqueig",
- "subscribers.status.confirmed": "Confirmat",
- "subscribers.status.enabled": "Actiu",
- "subscribers.status.subscribed": "Subscrit",
- "subscribers.status.unconfirmed": "Sense confirmar",
- "subscribers.status.unsubscribed": "Donat de baixa",
- "subscribers.subscribersDeleted": "S'han suprimit {num} subscriptors",
- "templates.cantDeleteDefault": "No es pot suprimir la plantilla inexistent o predeterminada",
- "templates.default": "Per defecte",
- "templates.dummyName": "Campanya simulada",
- "templates.dummySubject": "Assumpte de campanya simulat",
- "templates.errorCompiling": "Error en compilar la plantilla: {error}",
- "templates.errorRendering": "Error en renderitzar el missatge: {error}",
- "templates.fieldInvalidName": "Longitud no vàlida per al nom.",
- "templates.makeDefault": "Estableix per defecte",
- "templates.newTemplate": "Nova plantilla",
- "templates.placeholderHelp": "El marcador {placeholder} hauria d'aparèixer com a mínim una vegada a la plantilla.",
- "templates.preview": "Previsualització",
- "templates.rawHTML": "Codi HTML",
- "templates.subject": "Assumpte",
- "users.login": "Inicia sessió",
- "users.logout": "Tanca sessió"
-}
+{
+ "_.code": "eo",
+ "_.name": "Esperanto (eo)",
+ "admin.errorMarshallingConfig": "Agorderaro de klasado: {error}",
+ "analytics.count": "Kalkulado",
+ "analytics.fromDate": "De",
+ "analytics.invalidDates": "Datoj `de` aŭ `ĝis` nevalidaj.",
+ "analytics.isUnique": "Kalkuladoj estas solaj por ĉiu abonanto.",
+ "analytics.links": "Ligiloj",
+ "analytics.nonUnique": "La kalkuladoj ne estas solaj, ĉar la sekvado de abonantoj ne estas aktiva.",
+ "analytics.title": "Indikiloj",
+ "analytics.toDate": "Ĝis",
+ "bounces.complaint": "Reklamacio",
+ "bounces.hard": "Malmola",
+ "bounces.soft": "Mola",
+ "bounces.source": "Origino",
+ "bounces.unknownService": "Nekonata servo",
+ "bounces.view": "Vidi robotojn",
+ "campaigns.addAltText": "Aldonu alt-tekston",
+ "campaigns.addAttachments": "Aldonu kunsendaĵojn",
+ "campaigns.archive": "Arĥivo",
+ "campaigns.archiveEnable": "Publikigu en la publika arĥivo",
+ "campaigns.archiveHelp": "Publikugu (sendata, haltigita, finita) la mesaĝon de kampajno en la publika arĥivo ",
+ "campaigns.archiveMeta": "Metadatumoj de la kampajno",
+ "campaigns.archiveMetaHelp": "Datumoj de la test-abonanto kiun oni povas uzi en la publika mesaĝo, inkluzive nomo, respoŝtadreso kaj ia ajn atributo uzata en la mesaĝo de kampajno aŭ ŝablono.",
+ "campaigns.archiveSlug": "URL-nomo",
+ "campaigns.archiveSlugHelp": "Mallonga nomo por la paĝo, kiu estos uzita en la publika URL, ekzemple: mia-bulteno-2",
+ "campaigns.attachments": "Kunsendaĵoj",
+ "campaigns.cantUpdate": "Oni ne povas ĝisdatigi kurantan kampajnon aŭ finitan kampajnon.",
+ "campaigns.clicks": "Klakoj",
+ "campaigns.confirmDelete": "Forviŝu {name}",
+ "campaigns.confirmSchedule": "Tiu kampajno ekkomencos aŭtomate je la dato kaj horo progamitaj. Ĉu vi volas programi ĝin nun?",
+ "campaigns.confirmSwitchFormat": "Enhavo povas perdi aranĝon. Ĉu vi volas daŭrigi?",
+ "campaigns.content": "Enhavo",
+ "campaigns.contentHelp": "Enhavo tie",
+ "campaigns.continue": "Daŭrigu",
+ "campaigns.copyOf": "Kopio de {name}",
+ "campaigns.customHeadersHelp": "Matriu de capçaleres personalitzades per adjuntar als missatges de sortida. p. ex.: [{\"X-Custom\": \"valor\"}, {\"X-Custom2\": \"value\"}]",
+ "campaigns.dateAndTime": "Data i hora",
+ "campaigns.ended": "Finalitzada",
+ "campaigns.errorSendTest": "S'ha produit un error en enviar la prova: {error}",
+ "campaigns.fieldInvalidBody": "S'ha produït un error en compilar el cos de la campanya: {error}",
+ "campaigns.fieldInvalidFromEmail": "`from_email` no vàlid.",
+ "campaigns.fieldInvalidListIDs": "Identificadors de llista no vàlids.",
+ "campaigns.fieldInvalidMessenger": "Canal desconegut {name}.",
+ "campaigns.fieldInvalidName": "La longitud del nom no és vàlida.",
+ "campaigns.fieldInvalidSendAt": "La data prevista hauria de ser en el futur.",
+ "campaigns.fieldInvalidSubject": "Longitud no vàlida per a l'assumpte.",
+ "campaigns.formatHTML": "Format HTML",
+ "campaigns.fromAddress": "Adreça remitent",
+ "campaigns.fromAddressPlaceholder": "El teu nom ",
+ "campaigns.invalid": "Campanya invàlida",
+ "campaigns.invalidCustomHeaders": "Capçaleres personalitzades no vàlides: {error}",
+ "campaigns.markdown": "Markdown",
+ "campaigns.needsSendAt": "La campanya necessita una data per ser programada.",
+ "campaigns.newCampaign": "Nova campanya",
+ "campaigns.noKnownSubsToTest": "No hi ha subscriptors coneguts per fer una prova.",
+ "campaigns.noOptinLists": "No s'han trobat llistes opt-in per crear una campanya.",
+ "campaigns.noSubs": "No hi ha subscriptors a les llistes seleccionades per crear la campanya.",
+ "campaigns.noSubsToTest": "No hi ha subscriptors a qui enviar.",
+ "campaigns.notFound": "No s'ha trobat la campanya.",
+ "campaigns.onlyActiveCancel": "Només es poden cancel·lar les campanyes actives.",
+ "campaigns.onlyActivePause": "Només es poden posar en pausa les campanyes actives.",
+ "campaigns.onlyDraftAsScheduled": "Només es poden programar les campanyes en esborrany.",
+ "campaigns.onlyPausedDraft": "Només es poden iniciar campanyes en pausa o en esborrany.",
+ "campaigns.onlyScheduledAsDraft": "Només les campanyes programades es poden desar com a esborranys.",
+ "campaigns.pause": "Pausa",
+ "campaigns.plainText": "Text pla",
+ "campaigns.preview": "Prèvia",
+ "campaigns.progress": "Progrés",
+ "campaigns.queryPlaceholder": "Nom o assumpte",
+ "campaigns.rateMinuteShort": "min",
+ "campaigns.rawHTML": "Codi HTML ",
+ "campaigns.removeAltText": "Elimina el missatge de text pla alternatiu",
+ "campaigns.richText": "Text enriquit",
+ "campaigns.schedule": "Programa campanya",
+ "campaigns.scheduled": "Programada",
+ "campaigns.send": "Envia",
+ "campaigns.sendLater": "Envia més tard",
+ "campaigns.sendTest": "Envia missatge de prova",
+ "campaigns.sendTestHelp": "Premeu Intro després d'escriure una adreça per afegir diversos destinataris. Les adreces han de pertànyer als subscriptors existents.",
+ "campaigns.sendToLists": "Llistes a les quals s'envia",
+ "campaigns.sent": "Enviada",
+ "campaigns.start": "Inicia campanya",
+ "campaigns.started": "\"{name}\" iniciada",
+ "campaigns.startedAt": "Iniciada",
+ "campaigns.stats": "Indicadors",
+ "campaigns.status.cancelled": "Cancel·lada",
+ "campaigns.status.draft": "Esborrany",
+ "campaigns.status.finished": "Finalitzada",
+ "campaigns.status.paused": "Pausada",
+ "campaigns.status.running": "En curs",
+ "campaigns.status.scheduled": "Programada",
+ "campaigns.statusChanged": "\"{name}\" està {status}",
+ "campaigns.subject": "Assumpte",
+ "campaigns.templatingRef": "Referència de plantilles",
+ "campaigns.testEmails": "Adreces de correu electrònic",
+ "campaigns.testSent": "S'ha enviat el missatge de prova",
+ "campaigns.timestamps": "Segells de temps",
+ "campaigns.trackLink": "Enllaç de seguiment",
+ "campaigns.views": "Visualitzacions",
+ "dashboard.campaignViews": "Visualitzacions de la campanya",
+ "dashboard.linkClicks": "Clics a enllaços",
+ "dashboard.messagesSent": "Missatges enviats",
+ "dashboard.orphanSubs": "Orfes",
+ "email.data.info": "S'adjunta una còpia de totes les dades enregistrades sobre la teva persona en un fitxer en format JSON. Es pot veure en un editor de text.",
+ "email.data.title": "Les teves dades ",
+ "email.optin.confirmSub": "Confirma la subscripció",
+ "email.optin.confirmSubHelp": "Confirmeu la terva subscripció fent clic al botó següent.",
+ "email.optin.confirmSubInfo": "Heu estat afegit a les llistes següents:",
+ "email.optin.confirmSubTitle": "Confirmació de la subscrpció",
+ "email.optin.confirmSubWelcome": "Hola",
+ "email.optin.privateList": "Llista privada",
+ "email.status.campaignReason": "Motiu",
+ "email.status.campaignSent": "Enviada",
+ "email.status.campaignUpdateTitle": "Campanya actualitzada",
+ "email.status.importFile": "Fitxer",
+ "email.status.importRecords": "Registres",
+ "email.status.importTitle": "Importació actualitzada",
+ "email.status.status": "Estat",
+ "email.unsub": "Desubscripció",
+ "email.unsubHelp": "No voleu rebre aquests correus electrònics?",
+ "email.viewInBrowser": "Veure al navegador",
+ "forms.formHTML": "Format HTML",
+ "forms.formHTMLHelp": "Utilitzeu l'HTML següent per mostrar un formulari de subscripció en una pàgina web externa. El formulari hauria de tenir el camp de correu electrònic i un o més camps `l` (llista UUID). El camp del nom és opcional.",
+ "forms.noPublicLists": "No hi ha llistes públiques per generar formularis.",
+ "forms.publicLists": "Llistes públiques",
+ "forms.publicSubPage": "Pàgina de subscripció pública",
+ "forms.selectHelp": "Selecciona les llistes que vols afegir al formulari.",
+ "forms.title": "Formularis",
+ "globals.buttons.add": "Afegeix",
+ "globals.buttons.addNew": "Afegeix nou",
+ "globals.buttons.back": "Enrere",
+ "globals.buttons.cancel": "Cancel·la",
+ "globals.buttons.clear": "Esborra",
+ "globals.buttons.clearAll": "Esborra tot",
+ "globals.buttons.clone": "Clona",
+ "globals.buttons.close": "Tanca",
+ "globals.buttons.continue": "Continua",
+ "globals.buttons.copy": "Copiar",
+ "globals.buttons.delete": "Esborra",
+ "globals.buttons.deleteAll": "Esborra tot",
+ "globals.buttons.edit": "Edita",
+ "globals.buttons.enabled": "Habilitat",
+ "globals.buttons.insert": "Inserta",
+ "globals.buttons.learnMore": "Saber-nes més",
+ "globals.buttons.manage": "Manage",
+ "globals.buttons.more": "Més",
+ "globals.buttons.new": "Nou",
+ "globals.buttons.ok": "D'acord",
+ "globals.buttons.remove": "Elimina",
+ "globals.buttons.save": "Desa",
+ "globals.buttons.saveChanges": "Desa els canvis",
+ "globals.buttons.toggleSelect": "Toggle selection",
+ "globals.buttons.view": "Visualitzar",
+ "globals.days.0": "dg.",
+ "globals.days.1": "dg.",
+ "globals.days.2": "dl.",
+ "globals.days.3": "dt.",
+ "globals.days.4": "dc.",
+ "globals.days.5": "dj.",
+ "globals.days.6": "dv.",
+ "globals.days.7": "ds.",
+ "globals.fields.createdAt": "Creat",
+ "globals.fields.description": "Descripció",
+ "globals.fields.id": "ID",
+ "globals.fields.name": "Nom",
+ "globals.fields.status": "Estat",
+ "globals.fields.type": "Tipus",
+ "globals.fields.updatedAt": "Actualitzat",
+ "globals.fields.uuid": "UUID",
+ "globals.messages.confirm": "Estàs segur?",
+ "globals.messages.confirmDiscard": "Vols descartar els canvis?",
+ "globals.messages.copied": "Copiat",
+ "globals.messages.created": "\"{name}\" ha estat creat",
+ "globals.messages.deleted": "\"{name}\" ha estat esborrat",
+ "globals.messages.deletedCount": "{name} ({num}) ha estat esborrat",
+ "globals.messages.done": "Fet",
+ "globals.messages.emptyState": "No hi ha res aquí",
+ "globals.messages.errorCreating": "Error en crear {name}: {error}",
+ "globals.messages.errorDeleting": "Error en esborrar {name}: {error}",
+ "globals.messages.errorFetching": "Error en obtenir {name}: {error}",
+ "globals.messages.errorInvalidIDs": "Un o més identificadors no són vàlids: {error}",
+ "globals.messages.errorUUID": "Error en generar UUID: {error}",
+ "globals.messages.errorUpdating": "Error en actualitzar {name}: {error}",
+ "globals.messages.internalError": "Error del servidor intern",
+ "globals.messages.invalidData": "Dades no vàlides",
+ "globals.messages.invalidFields": "Camps no vàlids: {name}",
+ "globals.messages.invalidID": "ID(s) no vàlid",
+ "globals.messages.invalidUUID": "UUID(s) no vàlid",
+ "globals.messages.missingFields": "Falten camps: {name}",
+ "globals.messages.notFound": "No s'ha trobat {name} ",
+ "globals.messages.passwordChange": "Introduïu un valor per canviar",
+ "globals.messages.passwordChangeFull": "Buida i torna a introduir la contrasenya completa a '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
+ "globals.messages.slowQueriesCached": "Les consultes lentes s'estan emmagatzemant en memòria cau. Algunes xifres en aquesta pàgina no seran actuals.",
+ "globals.messages.updated": "\"{name}\" actualitzat",
+ "globals.months.1": "gen.",
+ "globals.months.10": "oct.",
+ "globals.months.11": "nov.",
+ "globals.months.12": "des.",
+ "globals.months.2": "febr.",
+ "globals.months.3": "març",
+ "globals.months.4": "abr.",
+ "globals.months.5": "maig",
+ "globals.months.6": "juny",
+ "globals.months.7": "jul.",
+ "globals.months.8": "ag.",
+ "globals.months.9": "set.",
+ "globals.states.off": "Apagat",
+ "globals.terms.all": "Tot",
+ "globals.terms.analytics": "Indicadors",
+ "globals.terms.bounce": "Rebot | Rebots",
+ "globals.terms.bounces": "Rebots",
+ "globals.terms.campaign": "Campanya | Campanyes",
+ "globals.terms.campaigns": "Campanyes",
+ "globals.terms.dashboard": "Taulell",
+ "globals.terms.day": "Dia | Dies",
+ "globals.terms.hour": "Hora | Hores",
+ "globals.terms.list": "Llista | Llistes",
+ "globals.terms.lists": "Llistes",
+ "globals.terms.media": "Mèdia | Mèdia",
+ "globals.terms.messenger": "Canal | Canals",
+ "globals.terms.messengers": "Canals",
+ "globals.terms.minute": "Minut | Minuts",
+ "globals.terms.month": "Mes | Mesos",
+ "globals.terms.none": "Cap",
+ "globals.terms.second": "Segon | Segons",
+ "globals.terms.settings": "Configuració",
+ "globals.terms.subscriber": "Subscriptor | Subscriptors",
+ "globals.terms.subscribers": "Subscriptors",
+ "globals.terms.subscriptions": "Subscripció | Subscripcions",
+ "globals.terms.tag": "Etiqueta | Etiquetes",
+ "globals.terms.tags": "Etiquetes",
+ "globals.terms.template": "Plantilla | Plantilles",
+ "globals.terms.templates": "Plantilles",
+ "globals.terms.tx": "Transaccional | Transaccionals",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
+ "globals.terms.year": "Any | Anys",
+ "import.alreadyRunning": "Ja s'està executant una importació. Espereu que acabi o atureu-lo abans de tornar-ho a provar.",
+ "import.blocklist": "Llista de bloqueig",
+ "import.csvDelim": "Delimitador CSV",
+ "import.csvDelimHelp": "El delimitador predeterminat és la coma.",
+ "import.csvExample": "Exemple de CSV en brut",
+ "import.csvFile": "Fitxer CSV o ZIP",
+ "import.csvFileHelp": "Feu clic o arrossegueu un fitxer CSV o ZIP aquí",
+ "import.errorCopyingFile": "Error en copiar el fitxer: {error}",
+ "import.errorProcessingZIP": "Error en processar el fitxer ZIP: {error}",
+ "import.errorStarting": "Error en iniciar la importació: {error}",
+ "import.importDone": "Fet",
+ "import.importStarted": "S'ha iniciat la importació",
+ "import.instructions": "Instruccions",
+ "import.instructionsHelp": "Carrega un fitxer CSV o un fitxer ZIP amb un únic fitxer CSV per importar subscriptors de forma massiva. El fitxer CSV hauria de tenir les capçaleres següents amb els noms exactes de les columnes. els atributs (opcional) han de ser una cadena JSON vàlida amb cometes dobles.",
+ "import.invalidDelim": "El delimitador ha de ser un sol caràcter.",
+ "import.invalidFile": "Fitxer no vàlid: {error}",
+ "import.invalidMode": "Mode no vàlid",
+ "import.invalidParams": "Paràmetres no vàlids: {error}",
+ "import.invalidSubStatus": "Estat de subscripció no vàlid",
+ "import.listSubHelp": "Llistes a les quals subscriure's.",
+ "import.mode": "Mode",
+ "import.overwrite": "Vols sobreescriure?",
+ "import.overwriteHelp": "Vols sobreescriure el nom, els atributs i l'estat de la subscripció dels subscriptors existents?",
+ "import.recordsCount": "{num} / {total} registres",
+ "import.stopImport": "Atura la importació",
+ "import.subscribe": "Subscriu",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
+ "import.title": "Importa subscriptors",
+ "import.upload": "Carrega",
+ "lists.confirmDelete": "Estàs segur? Això no elimina els subscriptors.",
+ "lists.confirmSub": "Confirmeu les subscripcions a {name}",
+ "lists.invalidName": "Nom no vàlid",
+ "lists.newList": "Nova llista",
+ "lists.optin": "Opt-in",
+ "lists.optinHelp": "El doble opt-in envia un correu electrònic al subscriptor demanant confirmació. A les llistes de doble subscripció, les campanyes només s'envien als subscriptors confirmats.",
+ "lists.optinTo": "Fes opt-in a {name}",
+ "lists.optins.double": "Doble opt-in",
+ "lists.optins.single": "Opt-in simple",
+ "lists.sendCampaign": "Envia campanya",
+ "lists.sendOptinCampaign": "Envia campanya opt-in ",
+ "lists.type": "Tipus",
+ "lists.typeHelp": "Les llistes públiques estan obertes a tothom per subscriure's i els seus noms poden aparèixer a pàgines públiques com ara la pàgina de gestió de subscripcions.",
+ "lists.types.private": "Privatt",
+ "lists.types.public": "Públic",
+ "logs.title": "Registres",
+ "maintenance.help": "Algunes accions poden trigar una estona a completar-se en funció de la quantitat de dades.",
+ "maintenance.maintenance.unconfirmedOptins": "Subscripcions opt-in no confirmades",
+ "maintenance.olderThan": "Més antic de",
+ "maintenance.orphanHelp": "Orfes = subscriptors sense llistes",
+ "maintenance.title": "Manteniment",
+ "maintenance.unconfirmedSubs": "Subscripcions no confirmades més antigues de {name} dies.",
+ "media.errorReadingFile": "Error en llegir el fitxer: {error}",
+ "media.errorResizing": "Error en canviar la mida de la imatge: {error}",
+ "media.errorSavingThumbnail": "Error en desar la miniatura: {error}",
+ "media.errorUploading": "Error en carregar el fitxer: {error}",
+ "media.invalidFile": "Fitxer no vàlid: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
+ "media.title": "Mèdia",
+ "media.unsupportedFileType": "El tipus de fitxer ({type}) no és compatible",
+ "media.upload": "Carrega",
+ "media.uploadHelp": "Fes clic o arrossega una o més imatges aquí",
+ "media.uploadImage": "Carrega la imatge",
+ "menu.allCampaigns": "Totes les campanyes",
+ "menu.allLists": "Totes les llistes",
+ "menu.allSubscribers": "Tots els subscriptors",
+ "menu.dashboard": "Taulell",
+ "menu.forms": "Formularis",
+ "menu.import": "Importació",
+ "menu.logs": "Registres",
+ "menu.maintenance": "Manteniment",
+ "menu.media": "Mèdia",
+ "menu.newCampaign": "Crea nova",
+ "menu.settings": "Configuració",
+ "public.archiveEmpty": "Sense missatges arxivats actualment.",
+ "public.archiveTitle": "Arxiu de la llista de correu",
+ "public.blocklisted": "Desubscrit de forma permanent.",
+ "public.campaignNotFound": "No s'ha trobat el missatge de correu electrònic.",
+ "public.confirmOptinSubTitle": "Confirmació de la subscripció",
+ "public.confirmSub": "Confirma la subscripció",
+ "public.confirmSubInfo": "Has estat afegit a les llistes següents:",
+ "public.confirmSubTitle": "Confirmació",
+ "public.dataRemoved": "S'han eliminat les vostres subscripcions i totes les dades associades.",
+ "public.dataRemovedTitle": "Eliminació de dades",
+ "public.dataSent": "Les teves dades t'han estat enviades per correu electrònic com a fitxer adjunt.",
+ "public.dataSentTitle": "Dades enviades per correu electrònic",
+ "public.errorFetchingCampaign": "S'ha produït un error en obtenir el missatge de correu electrònic.",
+ "public.errorFetchingEmail": "No s'ha trobat el missatge de correu electrònic",
+ "public.errorFetchingLists": "S'ha produït un error en obtenir les llistes. Si us plau, torna-ho a provar.",
+ "public.errorProcessingRequest": "S'ha produït un error en processar la sol·licitud. Si us plau, torna-ho a provar.",
+ "public.errorTitle": "Error",
+ "public.invalidCaptcha": "CAPTCHA no vàlid.",
+ "public.invalidFeature": "Aquesta funció no està disponible.",
+ "public.invalidLink": "Enllaç no vàlid",
+ "public.managePrefs": "Gestiona les preferències",
+ "public.managePrefsUnsub": "Desmarca les llistes de les quals vols fer-ne la desubscripció.",
+ "public.noListsAvailable": "No hi ha llistes disponibles per subscriure's.",
+ "public.noListsSelected": "No s'han seleccionat llistes vàlides per subscriure's.",
+ "public.noSubInfo": "No hi ha subscripcions per confirmar.",
+ "public.noSubTitle": "No hi ha subscripcions ",
+ "public.notFoundTitle": "No trobat",
+ "public.poweredBy": "Desenvolupat per",
+ "public.prefsSaved": "Les teves preferències han estat desades.",
+ "public.privacyConfirmWipe": "Estàs segur que vols suprimir totes les dades de la teva subscripció de manera permanent?",
+ "public.privacyExport": "Exporta les teves dades",
+ "public.privacyExportHelp": "Se t'enviarà per correu electrònic una còpia de les teves dades.",
+ "public.privacyTitle": "Privadesa i dades",
+ "public.privacyWipe": "Esborra permanentment les teves dades",
+ "public.privacyWipeHelp": "Suprimeix totes les teves subscripcions i dades relacionades de la base de dades de manera permanent.",
+ "public.sub": "Subscriu",
+ "public.subConfirmed": "T'has subscrit correctament.",
+ "public.subConfirmedTitle": "Confirmat",
+ "public.subName": "Nom (opcional)",
+ "public.subNotFound": "No s'ha trobat la subscripció.",
+ "public.subOptinPending": "S'ha enviat un correu electrònic per confirmar les teves subscripcions.",
+ "public.subPrivateList": "Llista privada",
+ "public.subTitle": "Subscripció",
+ "public.unsub": "Desubscriu",
+ "public.unsubFull": "També dona't de baixa de tots els futurs correus electrònics.",
+ "public.unsubHelp": "Vols donar-te de baixa d'aquesta llista de correu?",
+ "public.unsubTitle": "Desubscriu",
+ "public.unsubbedInfo": "Has cancel·lat la subscripció correctament.",
+ "public.unsubbedTitle": "Desubscrit",
+ "public.unsubscribeTitle": "Cancel·lació de la subscripció a la llista de correu",
+ "settings.appearance.adminHelp": "CSS personalitzat per aplicar a la interfície d'administració.",
+ "settings.appearance.adminName": "Administrador",
+ "settings.appearance.customCSS": "CSS personalitzats",
+ "settings.appearance.customJS": "JavaScript personalitzat",
+ "settings.appearance.name": "Aparença",
+ "settings.appearance.publicHelp": "CSS i JavaScript personalitzats per aplicar-los a les pàgines públiques.",
+ "settings.appearance.publicName": "Públic",
+ "settings.bounces.action": "Acció",
+ "settings.bounces.blocklist": "Llista de bloqueig",
+ "settings.bounces.complaint": "Complaint",
+ "settings.bounces.count": "Recompte de rebots",
+ "settings.bounces.countHelp": "Nombre de rebots per subscriptor",
+ "settings.bounces.delete": "Esborra",
+ "settings.bounces.enable": "Activa el processament de rebots",
+ "settings.bounces.enableMailbox": "Activa la bústia de rebots",
+ "settings.bounces.enablePostmark": "Activa Postmark",
+ "settings.bounces.enableSES": "Activa SES",
+ "settings.bounces.enableSendgrid": "Activa SendGrid",
+ "settings.bounces.enableWebhooks": "Activa els webhooks pels rebots",
+ "settings.bounces.enabled": "Activat",
+ "settings.bounces.folder": "Carpeta",
+ "settings.bounces.folderHelp": "Nom de la carpeta IMAP a escanejar. Ex: Safata d'entrada.",
+ "settings.bounces.hard": "Hard",
+ "settings.bounces.invalidScanInterval": "L'interval d'escaneig ha de ser com a mínim d'1 minut.",
+ "settings.bounces.name": "Rebots",
+ "settings.bounces.none": "Cap",
+ "settings.bounces.postmarkPassword": "Contrasenya de Postmark",
+ "settings.bounces.postmarkUsername": "Nom d'usuari de Postmark",
+ "settings.bounces.postmarkUsernameHelp": "Postmark permet activar l'autorització bàsica per als webhooks. Assegureu-vos d'introduir les mateixes credencials aquí i en la configuració del webhook de Postmark.",
+ "settings.bounces.scanInterval": "Interval d'escaneig",
+ "settings.bounces.scanIntervalHelp": "Interval en què s'hauria d'escanejar la bústia de rebot (s per segon, m per minut).",
+ "settings.bounces.sendgridKey": "Clau SendGrid ",
+ "settings.bounces.soft": "Soft",
+ "settings.bounces.type": "Tipus",
+ "settings.bounces.username": "Usuari",
+ "settings.confirmRestart": "Assegura't que les campanyes en curs estiguin en pausa. Reinicia?",
+ "settings.duplicateMessengerName": "Nom del canal duplicat: {name}",
+ "settings.errorEncoding": "Error en la configuració de codificació: {error}",
+ "settings.errorNoSMTP": "S'ha d'habilitar almenys un bloc SMTP",
+ "settings.general.adminNotifEmails": "Correu electrònic de notificació de l'administrador",
+ "settings.general.adminNotifEmailsHelp": "Llista d'adreces de correu electrònic separades per comes a les quals s'han d'enviar notificacions d'administrador, com ara actualitzacions d'importació, finalització de campanya, errors, etc.",
+ "settings.general.checkUpdates": "Busca actualitzacions",
+ "settings.general.checkUpdatesHelp": "Comprova periòdicament si hi ha noves versions d'aplicacions i notifica-ho.",
+ "settings.general.enablePublicArchive": "Enable public mailing list archive page",
+ "settings.general.enablePublicArchiveHelp": "Publica les campanyes on arxivar està habilitat en el lloc web públic.",
+ "settings.general.enablePublicArchiveRSSContent": "Mostra tot el contingut a l'arxiu RSS públic",
+ "settings.general.enablePublicArchiveRSSContentHelp": "Mostra el contingut complet del correu electrònic a l'aliment RSS. Si està desactivat, només es mostren els elements del títol i l'enllaç.",
+ "settings.general.enablePublicSubPage": "Activa la pàgina de subscripció pública",
+ "settings.general.enablePublicSubPageHelp": "Mostra una pàgina de subscripció pública amb totes les llistes públiques perquè la gent es subscrigui.",
+ "settings.general.faviconURL": "URL del favicon",
+ "settings.general.faviconURLHelp": "(Opcional) URL completa del favicon estàtic que serà visible a l'usuari, com ara la pàgina de cancel·lació de la subscripció.",
+ "settings.general.fromEmail": "Correu electrònic \"Remitent\" per defecte",
+ "settings.general.fromEmailHelp": "El correu electrònic `remitent` es mostra per defecte als correus electrònics de campanya sortints. Això es pot canviar per cada campanya.",
+ "settings.general.language": "Idioma",
+ "settings.general.logoURL": "URL del logotip",
+ "settings.general.logoURLHelp": "(Opcional) URL completa del logotip estàtic que serà visible a l'usuari, com ara la pàgina de cancel·lació de la subscripció.",
+ "settings.general.name": "General",
+ "settings.general.rootURL": "URL arrel",
+ "settings.general.rootURLHelp": "URL públic de la instal·lació (sense barra inclinada).",
+ "settings.general.sendOptinConfirm": "Envia opt-in de confirmació",
+ "settings.general.sendOptinConfirmHelp": "Envia un correu electrònic de confirmació de l'opt-in quan els subscriptors s'inscriguin mitjançant el formulari públic o quan l'administrador els afegeixi.",
+ "settings.general.siteName": "Nom del lloc web",
+ "settings.invalidMessengerName": "Nom de canal no vàlid",
+ "settings.mailserver.authProtocol": "Protocol d'autenticació",
+ "settings.mailserver.host": "Amfitrió",
+ "settings.mailserver.hostHelp": "Adreça host del servidor SMTP.",
+ "settings.mailserver.idleTimeout": "Temps d'espera d'inactivitat",
+ "settings.mailserver.idleTimeoutHelp": "Temps d'inactivitat per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la de la grup (s per segon, m per minut).",
+ "settings.mailserver.maxConns": "Connexions màximes",
+ "settings.mailserver.maxConnsHelp": "Màxim de connexions concurrents al servidor.",
+ "settings.mailserver.password": "Contrasenya",
+ "settings.mailserver.passwordHelp": "Fes intro per canviar",
+ "settings.mailserver.port": "Port",
+ "settings.mailserver.portHelp": "Port del servidor SMTP.",
+ "settings.mailserver.skipTLS": "Omet la verificació TLS",
+ "settings.mailserver.skipTLSHelp": "Omet la comprovació del hostname al certificat TLS.",
+ "settings.mailserver.tls": "TLS",
+ "settings.mailserver.tlsHelp": "Xifratge TLS/SSL. STARTTLS s'utilitza habitualment.",
+ "settings.mailserver.username": "Usuari",
+ "settings.mailserver.waitTimeout": "Espera el timeout",
+ "settings.mailserver.waitTimeoutHelp": "Temps per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la del grup (s per segon, m per minut).",
+ "settings.maintenance.cron": "Interval de cron",
+ "settings.media.provider": "Proveïdor",
+ "settings.media.s3.bucket": "Contenidor",
+ "settings.media.s3.bucketPath": "Ruta del contenidor",
+ "settings.media.s3.bucketPathHelp": "Ruta dins del contenidor per carregar fitxers. El valor per defecte és /",
+ "settings.media.s3.bucketType": "Tipus de contenidor",
+ "settings.media.s3.bucketTypePrivate": "Privat",
+ "settings.media.s3.bucketTypePublic": "Públic",
+ "settings.media.s3.key": "Clau d'accés AWS",
+ "settings.media.s3.publicURL": "URL públic personalitzada (opcional)",
+ "settings.media.s3.publicURLHelp": "Domini S3 personalitzat per ser usat en enllaços a imatges en lloc de l'URL backend S3 predeterminada.",
+ "settings.media.s3.region": "Regió",
+ "settings.media.s3.secret": "Secret d'accés AWS",
+ "settings.media.s3.uploadExpiry": "Caducitat de la càrrega",
+ "settings.media.s3.uploadExpiryHelp": "(Opcional) Especifica TTL per a l'URL presignada generada. Només aplicable a contenidors privats (s, m, h, d per a segons, minuts, hores, dies).",
+ "settings.media.s3.url": "URL del backend S3",
+ "settings.media.s3.urlHelp": "Canvia només si fas servir un backend personalitzat compatible amb S3 com Minio.",
+ "settings.media.title": "Càrrega de mèdia",
+ "settings.media.upload.extensions": "Extensions de fitxers permeses",
+ "settings.media.upload.extensionsHelp": "Afegiu * per permetre totes les extensions",
+ "settings.media.upload.path": "Ruta de càrrega",
+ "settings.media.upload.pathHelp": "Ruta al directori on es carregaran els mèdia.",
+ "settings.media.upload.uri": "Carrega URI",
+ "settings.media.upload.uriHelp": "Carrega un URI visible per al tothom. Els mèdia carregats a upload_path seran accessibles públicament a {root_url}, per exemple, https://listmonk.yoursite.com/upload",
+ "settings.messengers.maxConns": "Connexions màxiomes",
+ "settings.messengers.maxConnsHelp": "Màxim nombre de connexions concurrents al servidor.",
+ "settings.messengers.messageSaved": "S'ha desat la configuració. S'està tornant a carregar l'aplicació...",
+ "settings.messengers.name": "Canals",
+ "settings.messengers.nameHelp": "ex: my-sms. Alfanumèric / guió.",
+ "settings.messengers.password": "Contrasenya",
+ "settings.messengers.retries": "Reintents",
+ "settings.messengers.retriesHelp": "Nombre de vegades que cal tornar a intentar quan un missatge falla.",
+ "settings.messengers.skipTLSHelp": "Omet la comprovació del hostname al certificat TLS.",
+ "settings.messengers.timeout": "Temps d'espera d'inactivitat",
+ "settings.messengers.timeoutHelp": "Temps per esperar una nova activitat en una connexió abans de tancar-la i eliminar-la del grup (s per segon, m per minut).",
+ "settings.messengers.url": "URL",
+ "settings.messengers.urlHelp": "URL arrel del servidor Postback.",
+ "settings.messengers.username": "Usuari",
+ "settings.needsRestart": "La configuració ha canviat. Posa en pausa totes les campanyes en curs i reinicia l'aplicació",
+ "settings.performance.batchSize": "Mida del lot",
+ "settings.performance.batchSizeHelp": "El nombre de subscriptors que cal extreure de la base de dades en una sola iteració. Cada iteració extreu subscriptors de la base de dades, els envia missatges i després passa a la següent iteració per extreure el següent lot. Idealment, hauria de ser superior al rendiment màxim possible (concurrency * message_rate).",
+ "settings.performance.cacheSlowQueries": "Memòria cau de consultes lentes a la base de dades",
+ "settings.performance.cacheSlowQueriesHelp": "Només habiliteu-ho en bases de dades grans que s'hagin tornat significativament més lentes. Emmagatzema en memòria el compte de subscriptors de llista, les estadístiques del tauler de comandament, etc.",
+ "settings.performance.concurrency": "Concurrència",
+ "settings.performance.concurrencyHelp": "Màxim treballador concurrent (fils) que intentarà enviar missatges simultàniament.",
+ "settings.performance.maxErrThreshold": "Llindar d'error màxim",
+ "settings.performance.maxErrThresholdHelp": "El nombre d'errors (p. ex.: temps d'espera SMTP durant l'enviament de correu electrònic) que ha de tolerar una campanya en execució abans d'aturar-la per a una investigació o intervenció manual. Estableix a 0 per no fer mai una pausa.",
+ "settings.performance.messageRate": "Rati de missatges",
+ "settings.performance.messageRateHelp": "Nombre màxim de missatges a enviar per segon per treballador en un segon. Si concurrència = 10 i message_rate = 10, es poden enviar fins a 10x10 = 100 missatges cada segon. Això, juntament amb la concurrència, s'hauria d'ajustar per mantenir els missatges nets sortint per segon sota els límits dels servidors de missatges objectiu, si n'hi ha.",
+ "settings.performance.name": "Rendiment",
+ "settings.performance.slidingWindow": "Activa el límit de la finestra lliscant",
+ "settings.performance.slidingWindowDuration": "Durada",
+ "settings.performance.slidingWindowDurationHelp": "Durada del període de la finestra lliscant (m per minut, h per hora).",
+ "settings.performance.slidingWindowHelp": "Limita el nombre total de missatges que s'envien en un període determinat. Quan s'arriba a aquest límit, els missatges es retenen des de l'enviament fins que s'esborra la finestra de temps.",
+ "settings.performance.slidingWindowRate": "Missatges màxims",
+ "settings.performance.slidingWindowRateHelp": "Nombre màxim de missatges per enviar dins de la durada de la finestra.",
+ "settings.privacy.allowBlocklist": "Permet la llista de bloqueig",
+ "settings.privacy.allowBlocklistHelp": "Vols permetre als subscriptors donar-se de baixa de totes les llistes de correu i marcar-se com a llista bloquejada?",
+ "settings.privacy.allowExport": "Permet l'exportació",
+ "settings.privacy.allowExportHelp": "Vols permetre als subscriptors exportar les dades recollides sobre ells?",
+ "settings.privacy.allowPrefs": "Permet fer canvis de les preferències",
+ "settings.privacy.allowPrefsHelp": "Permet als subscriptors fer canvis de les preferències tals com els seus noms o la subscripció a múltiples llistes.",
+ "settings.privacy.allowWipe": "Permet l'esborrat permanent",
+ "settings.privacy.allowWipeHelp": "Permet als subscriptors esborrar-se, incloses les seves subscripcions i totes les altres dades de la base de dades. Les visualitzacions de campanya i els clics als enllaços també s'eliminen mentre es mantenen les visualitzacions i els recomptes de clics (sense subscriptors associats a ells) de manera que les estadístiques i els indicadors no es veuran afectats.",
+ "settings.privacy.domainBlocklist": "Llista de dominis bloquejats",
+ "settings.privacy.domainBlocklistHelp": "No es permet la subscripció a les adreces de correu electrònic amb aquests dominis. Introduïu un domini per línia, per exemple: somesite.com",
+ "settings.privacy.individualSubTracking": "Seguiment individual de subscriptors",
+ "settings.privacy.individualSubTrackingHelp": "Feu un seguiment de les visualitzacions i dels clics de la campanya a nivell de subscriptor. Quan està desactivat, el seguiment de visualitzacions i de clics continua disponible sense estar enllaçat a subscriptors individuals.",
+ "settings.privacy.listUnsubHeader": "Inclou la capçalera `List-Unsubscribe`",
+ "settings.privacy.listUnsubHeaderHelp": "Inclou capçaleres de cancel·lació de subscripció que permetin als clients de correu electrònic permetre als usuaris donar-se de baixa amb un sol clic.",
+ "settings.privacy.name": "Privadesa",
+ "settings.privacy.recordOptinIP": "Registra l'adreça IP de l'opt-in",
+ "settings.privacy.recordOptinIPHelp": "Registra l'adreça IP dels opt-ins dobles en els atributs del subscrit.",
+ "settings.restart": "Reinicia",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
+ "settings.security.captchaKey": "Clau del lloc hCaptcha.com",
+ "settings.security.captchaKeyHelp": "Visiteu www.hcaptcha.com per obtenir la clau i el secret.",
+ "settings.security.captchaSecret": "Secret del lloc hCaptcha.com",
+ "settings.security.enableCaptcha": "Habilita el CAPTCHA",
+ "settings.security.enableCaptchaHelp": "Habilita el CAPTCHA al formulari públic de subscripció.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
+ "settings.security.name": "Seguretat",
+ "settings.smtp.customHeaders": "Capçaleres personalitzades",
+ "settings.smtp.customHeadersHelp": "Matriu opcional de capçaleres de correu electrònic per incloure en tots els missatges enviats des d'aquest servidor. p. ex.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
+ "settings.smtp.enabled": "Habilitat",
+ "settings.smtp.heloHost": "Nom d'amfitrió HELO",
+ "settings.smtp.heloHostHelp": "Opcional. Alguns servidors SMTP requereixen un FQDN al hostname. Per defecte, HELLO va amb `localhost`. Estableix-loo si s'ha d'utilitzar un hostname personalitzat.",
+ "settings.smtp.name": "SMTP",
+ "settings.smtp.retries": "Reintents",
+ "settings.smtp.retriesHelp": "Nombre de vegades que cal tornar a intentar quan un missatge falla.",
+ "settings.smtp.sendTest": "Envia el correu electrònic",
+ "settings.smtp.setCustomHeaders": "Estableix capçaleres personalitzades",
+ "settings.smtp.testConnection": "Prova de connexió",
+ "settings.smtp.testEnterEmail": "Introduïu la contrasenya per provar",
+ "settings.smtp.toEmail": "Destinatari del correu electrònic",
+ "settings.title": "Configuració",
+ "settings.updateAvailable": "Hi ha disponible una nova actualització {versió}.",
+ "subscribers.advancedQuery": "Avançat",
+ "subscribers.advancedQueryHelp": "Expressió SQL parcial per consultar els atributs del subscriptor",
+ "subscribers.attribs": "Atributs",
+ "subscribers.attribsHelp": "Els atributs es defineixen com un mapa JSON, per exemple:",
+ "subscribers.blocklistedHelp": "Els subscriptors bloquejats no rebran mai cap correu electrònic.",
+ "subscribers.confirmBlocklist": "Afegir a la llista de bloqueig {nombre} subscriptors?",
+ "subscribers.confirmDelete": "Esborrar {num} subscriptors(s)?",
+ "subscribers.confirmExport": "Exportar {num} subscriptor(s)?",
+ "subscribers.domainBlocklisted": "El domini de correu electrònic està bloquejat.",
+ "subscribers.downloadData": "Descarrega les dades",
+ "subscribers.email": "Correu electrònic",
+ "subscribers.emailExists": "El correu electrònic ja existeix.",
+ "subscribers.errorBlocklisting": "Error en afegir a la llista de bloqueig els subscriptors: {error}",
+ "subscribers.errorNoIDs": "No s'han facilitat IDs.",
+ "subscribers.errorNoListsGiven": "No es troben llistes.",
+ "subscribers.errorPreparingQuery": "Error en preparar la consulta de subscriptor: {error}",
+ "subscribers.errorSendingOptin": "Error en enviar el correu electrònic d'opt-in.",
+ "subscribers.export": "Exportació",
+ "subscribers.invalidAction": "Acció no vàlida.",
+ "subscribers.invalidEmail": "Correu electroǹic no vàlid.",
+ "subscribers.invalidJSON": "JSON no vàlid als atributs.",
+ "subscribers.invalidName": "Nom no vàlid.",
+ "subscribers.listChangeApplied": "S'ha aplicat el canvi de llista.",
+ "subscribers.lists": "Llistes",
+ "subscribers.listsHelp": "Les llistes de les quals els subscriptors s'han donat de baixa no es poden eliminar.",
+ "subscribers.listsPlaceholder": "Llistes per subscriure's",
+ "subscribers.manageLists": "Gestionar llistes",
+ "subscribers.markUnsubscribed": "Marca com a no subscrit",
+ "subscribers.newSubscriber": "Nou subscriptor",
+ "subscribers.numSelected": "{num} subscriptors seleccionats",
+ "subscribers.optinSubject": "Confirma la teva subscripció",
+ "subscribers.preconfirm": "Preconfirmació de subscripcions",
+ "subscribers.preconfirmHelp": "No envieu correus electrònics d'opt-in i marqueu totes les subscripcions a la llista com a \"subscrites\".",
+ "subscribers.query": "Consulta",
+ "subscribers.queryPlaceholder": "Correu electrònic o nom",
+ "subscribers.reset": "Restableix",
+ "subscribers.selectAll": "Selecciona'n {num}",
+ "subscribers.sendOptinConfirm": "Envia la confirmació d'opt-in",
+ "subscribers.sentOptinConfirm": "Confirmació d'opt-in enviada",
+ "subscribers.status.blocklisted": "A la llista de bloqueig",
+ "subscribers.status.confirmed": "Confirmat",
+ "subscribers.status.enabled": "Actiu",
+ "subscribers.status.subscribed": "Subscrit",
+ "subscribers.status.unconfirmed": "Sense confirmar",
+ "subscribers.status.unsubscribed": "Donat de baixa",
+ "subscribers.subscribersDeleted": "S'han suprimit {num} subscriptors",
+ "templates.cantDeleteDefault": "No es pot suprimir la plantilla inexistent o predeterminada",
+ "templates.default": "Per defecte",
+ "templates.dummyName": "Campanya simulada",
+ "templates.dummySubject": "Assumpte de campanya simulat",
+ "templates.errorCompiling": "Error en compilar la plantilla: {error}",
+ "templates.errorRendering": "Error en renderitzar el missatge: {error}",
+ "templates.fieldInvalidName": "Longitud no vàlida per al nom.",
+ "templates.makeDefault": "Estableix per defecte",
+ "templates.newTemplate": "Nova plantilla",
+ "templates.placeholderHelp": "El marcador {placeholder} hauria d'aparèixer com a mínim una vegada a la plantilla.",
+ "templates.preview": "Previsualització",
+ "templates.rawHTML": "Codi HTML",
+ "templates.subject": "Assumpte",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
+ "users.login": "Inicia sessió",
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Tanca sessió",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
+}
diff --git a/i18n/es.json b/i18n/es.json
index 7fe720916..b1ac18d4b 100644
--- a/i18n/es.json
+++ b/i18n/es.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "Habilitar",
"globals.buttons.insert": "Insertar",
"globals.buttons.learnMore": "Conocer más",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Más",
"globals.buttons.new": "Nuevo",
"globals.buttons.ok": "Aceptar",
"globals.buttons.remove": "Eliminar",
"globals.buttons.save": "Guardar",
"globals.buttons.saveChanges": "Guardar cambios",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Ver",
"globals.days.0": "Dom",
"globals.days.1": "Dom",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} no encontrado",
"globals.messages.passwordChange": "Ingresar una contraseña para cambiar",
"globals.messages.passwordChangeFull": "Borre y vuelva a ingresar la contraseña completa en '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Las consultas lentas se están almacenando en caché. Algunos números en esta página no estarán actualizados.",
"globals.messages.updated": "\"{name}\" actualizado",
"globals.months.1": "Enero",
@@ -233,6 +236,8 @@
"globals.terms.template": "Plantilla | Plantillas",
"globals.terms.templates": "Plantillas",
"globals.terms.tx": "Transaccional | Transaccional",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Año | Años",
"import.alreadyRunning": "Se está ejecutándo una importación. Espere a que termine o deténgala antes de intentar una nueva.",
"import.blocklist": "Lista de bloqueados",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} de {total} registros",
"import.stopImport": "Detener importación",
"import.subscribe": "Suscribir",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importar suscriptores",
"import.upload": "Cargar",
"lists.confirmDelete": "¿Está seguro? Esto no elimina suscriptores",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "Error guardando miniatura: {error}",
"media.errorUploading": "Error cargando archivo: {error}",
"media.invalidFile": "Archivo inválido: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Medios",
"media.unsupportedFileType": "Tipo de archivo no soportado ({type})",
"media.upload": "Cargar",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "Grabar dirección IP de inscripción",
"settings.privacy.recordOptinIPHelp": "Registrar la dirección IP de doble inscripción en los atributos del suscriptor.",
"settings.restart": "Reiniciar",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Clave de sitio hCaptcha.com",
"settings.security.captchaKeyHelp": "Visite www.hcaptcha.com para conseguir la SiteKey y el secret.",
"settings.security.captchaSecret": "Secreto hCaptcha.com",
"settings.security.enableCaptcha": "Habilitar CAPTCHA",
"settings.security.enableCaptchaHelp": "Habilitar CAPTCHA en el formulario público de suscripción.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Seguridad",
"settings.smtp.customHeaders": "Encabezados personalizados",
"settings.smtp.customHeadersHelp": "Lista de encabezados opcionales a incluir en todos los mensajes enviados desde este servidor. Por ejemplo {{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "Vista previa",
"templates.rawHTML": "HTML de orige",
"templates.subject": "Asunto",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Ingresar",
- "users.logout": "Salir"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Salir",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/fi.json b/i18n/fi.json
index 8ef68e23c..e6be39660 100644
--- a/i18n/fi.json
+++ b/i18n/fi.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "Käytössä",
"globals.buttons.insert": "Lisää",
"globals.buttons.learnMore": "Lue lisää",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Lisää",
"globals.buttons.new": "Uusi",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Poista",
"globals.buttons.save": "Tallenna",
"globals.buttons.saveChanges": "Tallenna muutokset",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Näytä",
"globals.days.0": "Su",
"globals.days.1": "Su",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} ei löytynyt",
"globals.messages.passwordChange": "Syötä arvoa muuttaaksesi",
"globals.messages.passwordChangeFull": "Tyhjennä ja kirjoita uudelleen täysi salasana kohdassa '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Hitaat kyselyt tallennetaan välimuistiin. Jotkin numerot tällä sivulla eivät ole ajan tasalla.",
"globals.messages.updated": "\"{name}\" päivitetty",
"globals.months.1": "Tammi",
@@ -233,6 +236,8 @@
"globals.terms.template": "Pohja | Pohjat",
"globals.terms.templates": "Pohjat",
"globals.terms.tx": "Transaktiivinen | Transaktiiviset",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Vuosi | Vuodet",
"import.alreadyRunning": "Tuo on jo käynnissä. Odota sen valmistumista tai lopeta se ennen uudelleen yrittämistä.",
"import.blocklist": "Estolista",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} / {total} tietuetta",
"import.stopImport": "Pysäytä tuonti",
"import.subscribe": "Tilaa",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Tuo tilaajat",
"import.upload": "Lataa",
"lists.confirmDelete": "Oletko varma? Tilauksia tämä ei poista.",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "Virhe pikkukuvan tallentamisessa: {error}",
"media.errorUploading": "Virhe tiedoston lataamisessa: {error}",
"media.invalidFile": "Virheellinen tiedosto: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Media",
"media.unsupportedFileType": "Tiedostotyyppiä ei tueta ({type})",
"media.upload": "Lataa",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "Kirjaa opt-in IP-osoite",
"settings.privacy.recordOptinIPHelp": "Kirjaa tuplaopt-insien IP-osoitteet tilaajan attribuutteihin.",
"settings.restart": "Käynnistä uudelleen",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com-sivutunnus",
"settings.security.captchaKeyHelp": "Hanki avain ja salaisuus osoitteesta www.hcaptcha.com.",
"settings.security.captchaSecret": "hCaptcha.com-salaisuus",
"settings.security.enableCaptcha": "Ota käyttöön CAPTCHA",
"settings.security.enableCaptchaHelp": "Ota käyttöön CAPTCHA julkaistavalla tilauslomakkeella.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Turvallisuus",
"settings.smtp.customHeaders": "Mukautetut otsakkeet",
"settings.smtp.customHeadersHelp": "Eventuualinen taulukko sähköpostiosoitteita, joka sisältää lähtevien viestien mukautetut otsakkeet. esim: [{\"X-Custom\": \"arvo\"}, {\"X-Custom2\": \"arvo\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "Esikatselu",
"templates.rawHTML": "Raaka HTML",
"templates.subject": "Aihe",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Kirjaudu sisään",
- "users.logout": "Kirjaudu ulos"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Kirjaudu ulos",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/fr-CA.json b/i18n/fr-CA.json
index 5ce397430..936909090 100644
--- a/i18n/fr-CA.json
+++ b/i18n/fr-CA.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "Activé",
"globals.buttons.insert": "Insérer",
"globals.buttons.learnMore": "En savoir plus",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Plus",
"globals.buttons.new": "Ajouter",
"globals.buttons.ok": "Valider",
"globals.buttons.remove": "Supprimer",
"globals.buttons.save": "Enregistrer",
"globals.buttons.saveChanges": "Enregistrer les changements",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Voir",
"globals.days.0": "Dim",
"globals.days.1": "Dim",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} introuvable",
"globals.messages.passwordChange": "Entrez un nouveau mot de passe pour en changer",
"globals.messages.passwordChangeFull": "Effacer et saisir à nouveau le mot de passe complet dans '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Les requêtes lentes sont mises en cache. Certains nombres sur cette page ne seront pas à jour.",
"globals.messages.updated": "Mise à jour de \"{name}\"",
"globals.months.1": "jan.",
@@ -233,6 +236,8 @@
"globals.terms.template": "Modèle | Modèles",
"globals.terms.templates": "Modèles",
"globals.terms.tx": "Transactionnel | Transactionnels",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Année | Années",
"import.alreadyRunning": "Une importation est déjà en cours. Attendez qu'elle se termine ou arrêtez-la avant de réessayer.",
"import.blocklist": "Bloquer les adresses importées",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} / {total} contacts importés",
"import.stopImport": "Arrêter l'importation",
"import.subscribe": "S'abonner",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importer des abonné·es",
"import.upload": "Envoyer",
"lists.confirmDelete": "Êtes-vous sûr·e de supprimer cette liste ? Cela ne supprimera pas les abonné·es.",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "Erreur lors de l'enregistrement de la miniature : {error}",
"media.errorUploading": "Erreur lors de l'envoi du fichier : {error}",
"media.invalidFile": "Fichier non valide : {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Fichiers",
"media.unsupportedFileType": "Type de fichier non pris en charge ({type})",
"media.upload": "Importer",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "Enregistrer l'adresse IP d'inscription",
"settings.privacy.recordOptinIPHelp": "Enregistre l'adresse IP des double opt-ins dans les attributs des abonnés.",
"settings.restart": "Redémarrer",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Clef de site hCaptcha.com",
"settings.security.captchaKeyHelp": "Allez sur www.hcaptcha.com pour obtenir une clef et son secret.",
"settings.security.captchaSecret": "Secret hCaptcha.com",
"settings.security.enableCaptcha": "Activer CAPTCHA",
"settings.security.enableCaptchaHelp": "Activer CAPTCHA sur le formulaire public de souscription.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Sécurité",
"settings.smtp.customHeaders": "En-têtes personnalisées",
"settings.smtp.customHeadersHelp": "Tableau facultatif d'en-têtes à inclure dans tous les courriels envoyés depuis ce serveur. Par exemple : [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "Aperçu",
"templates.rawHTML": "HTML brut",
"templates.subject": "Objet",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Connecter",
- "users.logout": "Déconnecter"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Déconnecter",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/fr.json b/i18n/fr.json
index 13e9ed15a..921396bf1 100644
--- a/i18n/fr.json
+++ b/i18n/fr.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "Activé",
"globals.buttons.insert": "Insérer",
"globals.buttons.learnMore": "En savoir plus",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Plus",
"globals.buttons.new": "Ajouter",
"globals.buttons.ok": "Valider",
"globals.buttons.remove": "Supprimer",
"globals.buttons.save": "Enregistrer",
"globals.buttons.saveChanges": "Enregistrer les changements",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Voir",
"globals.days.0": "Dim",
"globals.days.1": "Dim",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} introuvable",
"globals.messages.passwordChange": "Entrez un nouveau mot de passe pour en changer",
"globals.messages.passwordChangeFull": "Effacer et saisir à nouveau le mot de passe complet dans '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Les requêtes lentes sont mises en cache. Certains nombres sur cette page ne seront pas à jour.",
"globals.messages.updated": "Mise à jour de \"{name}\"",
"globals.months.1": "jan.",
@@ -233,6 +236,8 @@
"globals.terms.template": "Modèle | Modèles",
"globals.terms.templates": "Modèles",
"globals.terms.tx": "Transactionnel | Transactionnels",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Année | Années",
"import.alreadyRunning": "Une importation est déjà en cours. Attendez qu'elle se termine ou arrêtez-la avant de réessayer.",
"import.blocklist": "Bloquer les adresses importées",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} / {total} contacts importés",
"import.stopImport": "Arrêter l'importation",
"import.subscribe": "S'abonner",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importer des abonné·es",
"import.upload": "Envoyer",
"lists.confirmDelete": "Êtes-vous sûr·e de supprimer cette liste ? Cela ne supprimera pas les abonné·es.",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "Erreur lors de l'enregistrement de la miniature : {error}",
"media.errorUploading": "Erreur lors de l'envoi du fichier : {error}",
"media.invalidFile": "Fichier non valide : {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Fichiers",
"media.unsupportedFileType": "Type de fichier non pris en charge ({type})",
"media.upload": "Importer",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "Enregistrer l'adresse IP d'inscription",
"settings.privacy.recordOptinIPHelp": "Enregistre l'adresse IP des double opt-ins dans les attributs des abonnés.",
"settings.restart": "Redémarrer",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Clef de site hCaptcha.com",
"settings.security.captchaKeyHelp": "Allez sur www.hcaptcha.com pour obtenir une clef et son secret.",
"settings.security.captchaSecret": "Secret hCaptcha.com",
"settings.security.enableCaptcha": "Activer CAPTCHA",
"settings.security.enableCaptchaHelp": "Activer CAPTCHA sur le formulaire public de souscription.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Sécurité",
"settings.smtp.customHeaders": "En-têtes personnalisées",
"settings.smtp.customHeadersHelp": "Tableau facultatif d'en-têtes à inclure dans tous les e-mails envoyés depuis ce serveur. Par exemple : [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "Aperçu",
"templates.rawHTML": "HTML brut",
"templates.subject": "Objet",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Connecter",
- "users.logout": "Déconnecter"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Déconnecter",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/he.json b/i18n/he.json
index cd18ae321..cf54977fe 100644
--- a/i18n/he.json
+++ b/i18n/he.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "מופעל",
"globals.buttons.insert": "להכניס",
"globals.buttons.learnMore": "למד עוד",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "עוד",
"globals.buttons.new": "חדש",
"globals.buttons.ok": "אישור",
"globals.buttons.remove": "הסרה",
"globals.buttons.save": "שמירה",
"globals.buttons.saveChanges": "שמור שינויים",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "צפה",
"globals.days.0": "ראשון",
"globals.days.1": "ראשון",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} לא נמצא",
"globals.messages.passwordChange": "הזן ערך לשינוי",
"globals.messages.passwordChangeFull": "נא לנקות ולהזין שוב את הסיסמה המלאה ב־'{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "שאילתות איטיות מוקפאות במטמון. חלק מהמספרים בדף זה לא יהיו מעודכנים.",
"globals.messages.updated": "\"{name}\" עודכן",
"globals.months.1": "ינואר",
@@ -232,6 +235,8 @@
"globals.terms.template": "תבנית | תבניות",
"globals.terms.templates": "תבניות",
"globals.terms.tx": "עסקה | עסקה",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "שנה | שנים",
"import.alreadyRunning": "היבוא כבר פועל. יש להמתין שיסתיים או לעצור אותו לפני שינוי נוסף.",
"import.blocklist": "חסום רשימה",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} רשומות",
"import.stopImport": "עצור ייבוא",
"import.subscribe": "הירשם",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "ייבוא מנויים",
"import.upload": "העלאה",
"lists.confirmDelete": "האם אתה בטוח? זה לא מוחק את המנויים.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "שגיאה בשמירת התמונה הקטנה: {error}",
"media.errorUploading": "שגיאה בהעלאת הקובץ: {error}",
"media.invalidFile": "קובץ לא חוקי: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "מדיה",
"media.unsupportedFileType": "סוג קובץ לא נתמך ({type})",
"media.upload": "העלאה",
@@ -505,11 +512,19 @@
"settings.privacy.recordOptinIP": "תצורת דין רישום IP הפעילה",
"settings.privacy.recordOptinIPHelp": "תיחום כתובת ה־IP של רישום הפעילה החזקה במאפייני המנוי.",
"settings.restart": "הפעלה מחדש",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "מפתח אתר של hCaptcha.com",
"settings.security.captchaKeyHelp": "אין להתרשם הפעלה על מנת לקבל את מפתח המקוד והסוד שלך.",
"settings.security.captchaSecret": "סוד מאיש הגזיון",
"settings.security.enableCaptcha": "הפעל קאפצ׳ה",
"settings.security.enableCaptchaHelp": "הפעלת CAPTCHA על טופס ההרשמה הציבורי.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "אבטחה",
"settings.smtp.customHeaders": "כותרות מותאמות אישית",
"settings.smtp.customHeadersHelp": "מערך אופציונלי של כותרות הדואר האלקטרוני הנרשמות בכל הודעה הנשלחת מתוך השרת הזה. לדוגמה: [{\"X-Custom\": \"ערך\"}, {\"X-Custom2\": \"ערך\"}]",
@@ -585,6 +600,39 @@
"templates.preview": "תצוגה מקדימה",
"templates.rawHTML": "HTML גולמי",
"templates.subject": "נושא",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "התחברות",
- "users.logout": "התנתקות"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "התנתקות",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/hu.json b/i18n/hu.json
index 3397a8c65..3aa5c63f4 100644
--- a/i18n/hu.json
+++ b/i18n/hu.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Be",
"globals.buttons.insert": "Beillesztés",
"globals.buttons.learnMore": "Tudj meg többet",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Tovább",
"globals.buttons.new": "Új",
"globals.buttons.ok": "Rendben",
"globals.buttons.remove": "Eltávolítás",
"globals.buttons.save": "Mentés",
"globals.buttons.saveChanges": "Változtatások mentése",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Megtekintés",
"globals.days.0": "H",
"globals.days.1": "H",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} nem található",
"globals.messages.passwordChange": "Adja meg az új jelszót",
"globals.messages.passwordChangeFull": "Tisztítsa meg és írja be újra a teljes jelszót a(z) '{name}'-ben.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "A lassú lekérdezések gyorsítótárazva vannak. Ennek az oldalnak néhány száma nem lesz naprakész.",
"globals.messages.updated": "\"{name}\" frissítve",
"globals.months.1": "Jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Sablon",
"globals.terms.templates": "Sablonok",
"globals.terms.tx": "Ügymenet",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Év",
"import.alreadyRunning": "Az importálás elkezdődött. Várja meg, amíg befejeződik, vagy állítsa le, mielőtt újra próbálkozna.",
"import.blocklist": "Tiltás",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} rekord",
"import.stopImport": "Importálás leállítása",
"import.subscribe": "Feliratkozás",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Tagok importálása",
"import.upload": "Feltöltés",
"lists.confirmDelete": "Biztos? Ez nem törli a tagokat.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Hiba az indexkép mentésekor: {error}",
"media.errorUploading": "Hiba a fájl feltöltésekor: {error}",
"media.invalidFile": "Hibás fájl: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Média",
"media.unsupportedFileType": "Nem támogatott típus ({type})",
"media.upload": "Feltöltés",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Opt-in IP cím rögzítése",
"settings.privacy.recordOptinIPHelp": "Az előfizető attribútumainak feljegyzésekor rögzítse a dupla opt-in IP címét.",
"settings.restart": "Újraindítás",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com kulcs",
"settings.security.captchaKeyHelp": "Kulcs és jelszó igénylése a hcaptcha.com oldalon.",
"settings.security.captchaSecret": "hCaptcha.com jelszó",
"settings.security.enableCaptcha": "CAPTCHA",
"settings.security.enableCaptchaHelp": "CAPTCHA a nyilvános feliratkozási űrlapon.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Biztonság",
"settings.smtp.customHeaders": "Egyéni fejlécek",
"settings.smtp.customHeadersHelp": "Kimenő üzenetek extra fejlécei. Például: [{\"X-K1\": \"V1\"}, {\"X-K2\": \"V2\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Előnézet",
"templates.rawHTML": "HTML Forrás",
"templates.subject": "Tárgy",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Belépés",
- "users.logout": "Kijelentkezés"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Kijelentkezés",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/it.json b/i18n/it.json
index 2ed978f93..c08d95d9b 100644
--- a/i18n/it.json
+++ b/i18n/it.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Attivata",
"globals.buttons.insert": "Inserire",
"globals.buttons.learnMore": "Per saperne di più",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Di più",
"globals.buttons.new": "Nuovo",
"globals.buttons.ok": "Ok",
"globals.buttons.remove": "Cancellare",
"globals.buttons.save": "Salvare",
"globals.buttons.saveChanges": "Salvare le modifiche",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Vedi",
"globals.days.0": "Dom",
"globals.days.1": "Dom",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} introvabile",
"globals.messages.passwordChange": "Inserisci un valore da modificare",
"globals.messages.passwordChangeFull": "Cancella e reinserisci la password completa in '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Le query lente vengono memorizzate nella cache. Alcuni numeri in questa pagina potrebbero non essere aggiornati.",
"globals.messages.updated": "\"{name}\" aggiornato",
"globals.months.1": "Gen",
@@ -232,6 +235,8 @@
"globals.terms.template": "Modello | Modelli",
"globals.terms.templates": "Modelli",
"globals.terms.tx": "Transazionale | Transazionali",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Anno | Anni",
"import.alreadyRunning": "Un'importazione è già in corso. Aspetta che finisca o interrompila prima di riprovare.",
"import.blocklist": "Lista degli indirizzi bloccati",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} salvataggi",
"import.stopImport": "Interrompere l'importazione",
"import.subscribe": "Iscriversi",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importare iscritti",
"import.upload": "Caricare",
"lists.confirmDelete": "Sei sicuro? Questo non cancella gli iscritti",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Errore durante il salvataggio dell'immagine: {error}",
"media.errorUploading": "Errore durante il caricamento del file: {error}",
"media.invalidFile": "File non valido: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Media",
"media.unsupportedFileType": "Tipo di file non supportato ({type})",
"media.upload": "Caricare",
@@ -505,11 +512,19 @@
"settings.privacy.recordOptinIP": "Registra l'indirizzo IP di consenso",
"settings.privacy.recordOptinIPHelp": "Registra l'indirizzo IP dei double opt-in negli attributi dell'iscritto.",
"settings.restart": "Riavviare",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Chiave sito hCaptcha.com",
"settings.security.captchaKeyHelp": "Visita www.hcaptcha.com per ottenere la SiteKey e il secret.",
"settings.security.captchaSecret": "Segreto hCaptcha.com",
"settings.security.enableCaptcha": "Attiva CAPTCHA",
"settings.security.enableCaptchaHelp": "Attiva CAPTCHA nel modulo di sottoiscrizione publica.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Sicurezza",
"settings.smtp.customHeaders": "Headers personalizzate",
"settings.smtp.customHeadersHelp": "Elenco facoltativo di intestazioni di posta elettronica da includere in tutti i messaggi inviati da questo server. Ad esempio: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -585,6 +600,39 @@
"templates.preview": "Anteprima",
"templates.rawHTML": "HTML semplice",
"templates.subject": "Oggetto",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Accesso",
- "users.logout": "Esci"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Esci",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/jp.json b/i18n/jp.json
index 1d89e48a5..c70099359 100644
--- a/i18n/jp.json
+++ b/i18n/jp.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "有効",
"globals.buttons.insert": "入れる",
"globals.buttons.learnMore": "さらに詳しく",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "もっと",
"globals.buttons.new": "新",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "削除",
"globals.buttons.save": "保存",
"globals.buttons.saveChanges": "変更内容を保存",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "表示",
"globals.days.0": "日",
"globals.days.1": "日",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} が見つかりません。",
"globals.messages.passwordChange": "変更するには値を入力",
"globals.messages.passwordChangeFull": "'{name}’でパスワードをクリアして再入力してください。",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "遅いクエリがキャッシュされています。このページの一部の数値は最新ではありません。",
"globals.messages.updated": "\"{name}\" 更新済み",
"globals.months.1": "1月",
@@ -233,6 +236,8 @@
"globals.terms.template": "テンプレート | テンプレート",
"globals.terms.templates": "テンプレート",
"globals.terms.tx": "トランザクションメール | トランザクションメール",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "都市 | 都市",
"import.alreadyRunning": "インポートはすでに実行されています。終わるまで待つか、停止してから再試行してください。",
"import.blocklist": "ブロックリスト",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} / {total} 記録",
"import.stopImport": "インポートを中止",
"import.subscribe": "加入",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "加入者をインポート",
"import.upload": "アップロード",
"lists.confirmDelete": "本当に良いですか?これは加入者を削除しません。",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "サムネイル保存エラー: {error}",
"media.errorUploading": "ファイルアップロードのエラー: {error}",
"media.invalidFile": "無効なファイル: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "メディア",
"media.unsupportedFileType": "サポートされていないファイルタイプ ({type})",
"media.upload": "アップロード",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "オプトインIPアドレスを記録する",
"settings.privacy.recordOptinIPHelp": "購読者属性にダブルオプトインのIPアドレスを記録します。",
"settings.restart": "再起動",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.comのサイトキー",
"settings.security.captchaKeyHelp": "キーとシークレットを取得するには、www.hcaptcha.comを訪問してください。",
"settings.security.captchaSecret": "hCaptcha.comシークレット",
"settings.security.enableCaptcha": "CAPTCHAを有効にする",
"settings.security.enableCaptchaHelp": "公開購読フォームでCAPTCHAを有効にします。",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "セキュリティ",
"settings.smtp.customHeaders": "カスタムヘッダー",
"settings.smtp.customHeadersHelp": "このサーバーから送信する全てのメッセージに含まれる任意のメールヘッダーの配列。 例: [{\"X-カスタム\": \"バリュー\"}, {\"X-カスタム2\": \"バリュー\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "プレビュー",
"templates.rawHTML": "HTML(生)",
"templates.subject": "件名",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "ログイン",
- "users.logout": "ログアウト"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "ログアウト",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/ml.json b/i18n/ml.json
index bd2075e69..721429d37 100644
--- a/i18n/ml.json
+++ b/i18n/ml.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "പ്രവർത്തനക്ഷമാക്കി",
"globals.buttons.insert": "ഉൾച്ചേർക്കുക",
"globals.buttons.learnMore": "കൂടുതൽ അറിയുക",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "കൂടുതൽ",
"globals.buttons.new": "പുതിയത്",
"globals.buttons.ok": "ശരി",
"globals.buttons.remove": "നീക്കം ചെയ്യുക",
"globals.buttons.save": "സൂക്ഷിക്കുക",
"globals.buttons.saveChanges": "മാറ്റങ്ങൾ സൂക്ഷിക്കുക",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "കാണുക",
"globals.days.0": "ഞായർ",
"globals.days.1": "ഞായർ",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} കണ്ടെത്തിയില്ല",
"globals.messages.passwordChange": "മാറ്റം വരുത്തേണ്ട വില രേഖപ്പെടുത്തുക",
"globals.messages.passwordChangeFull": "'{name}' എന്നില് നിന്ന് പൂര്ണ്ണമായി പാസ്വേഡ് മാറ്റുക.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "എന്നാൽ, മാന്ദഹാരമുള്ള ചോദ്യങ്ങൾ കാഷെചെയ്യുന്നു. ഈ പേജിൽ ചില സംഖ്യകളുടെ പുതുരൂപം അപ്ഡേറ്റ് ആകുമായിരിക്കും.",
"globals.messages.updated": "\"{name}\" പുതുക്കി",
"globals.months.1": "ജനുവരി",
@@ -232,6 +235,8 @@
"globals.terms.template": "ടെംപ്ലേറ്റ് | ടെംപ്ലേറ്റുകൾ",
"globals.terms.templates": "ടെംപ്ലേറ്റുകൾ",
"globals.terms.tx": "ഇടപാട് | ഇടപാട്",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "വർഷം | വർഷങ്ങൾ",
"import.alreadyRunning": "ഒരു ഇമ്പോർട്ട് ഇപ്പോൾ നടന്നുകൊണ്ടിരിക്കുന്നു. വീണ്ടും ശ്രമിക്കുന്നതിന് മുമ്പ് കാത്തിരിക്കുകയോ നടന്നുകൊണ്ടിരിക്കുന്ന ഇമ്പോർട്ട് നിർത്തുകയോ ചെയ്യുക.",
"import.blocklist": "തടയുന്ന പട്ടിക",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} രേഖകള്",
"import.stopImport": "ഇംപോർട്ട് നിർത്തുക",
"import.subscribe": "വരിക്കാരാകുക",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "വരിക്കാരേ ഇംപോർട്ട് ചെയ്യുക",
"import.upload": "അപ്ലോഡ്",
"lists.confirmDelete": "നിങ്ങൾക്ക് തീർച്ചയാണോ? ഇത് ലിസ്റ്റിലെ വരിക്കാരെ ഇല്ലാതാക്കില്ല.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "തമ്പ്നെയിൽ സേവ് ചെയ്യാനായില്ല: {error}",
"media.errorUploading": "ഫയൽ അപ്ലോഡ് ചെയ്യാനായില്ല: {error}",
"media.invalidFile": "ഫയൽ അസാധുവാണ്: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "മീഡിയ",
"media.unsupportedFileType": "പിൻതുണക്കാത്ത തരം ഫയൽ({type})",
"media.upload": "അപ്ലോഡ്",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "ഓപ്റ്റ്-ഇന് IP വിലാസം രേഖപ്പെടുത്തൂ",
"settings.privacy.recordOptinIPHelp": "ഡബിള് ഓപ്റ്റ് ഇന്സ് സബ്സ്ക്രൈബറുടെ വിവരഗണനയിലേക്ക് IP വിലാസം രേഖപ്പെടുത്തൂ.",
"settings.restart": "പുനരാരംഭിയ്ക്കുക",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com സൈറ്റ്കീ",
"settings.security.captchaKeyHelp": "കീ ലഭിക്കാൻ www.hcaptcha.com സന്ദര്ശിക്കുക.",
"settings.security.captchaSecret": "hCaptcha.com രഹസ്യം",
"settings.security.enableCaptcha": "CAPTCHA സജ്ജീകരിക്കുക",
"settings.security.enableCaptchaHelp": "പൊതു ചേര്ക്കല് ഫോംയില് CAPTCHA സജ്ജീകരിക്കുക.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "സുരക്ഷ",
"settings.smtp.customHeaders": "ഇഷ്ടാനുസൃത തലക്കെട്ടുകൾ",
"settings.smtp.customHeadersHelp": "ഈ സേർവറിൽ നിന്നും അയക്കുന്ന എല്ലാ ഈ-മെയിലിലും ഉണ്ടാകേണ്ട ഇഷ്ടാനുസൃത തലക്കെട്ടുകൾ. ഉദാഹരണം: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "പ്രിവ്യൂ",
"templates.rawHTML": "HTML",
"templates.subject": "വിഷയം",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "പ്രവേശിക്കുക",
- "users.logout": "പുറത്തുകടക്കുക"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "പുറത്തുകടക്കുക",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/nl.json b/i18n/nl.json
index 7b0a7f044..e7f2e8aa9 100644
--- a/i18n/nl.json
+++ b/i18n/nl.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Ingeschakeld",
"globals.buttons.insert": "Invoegen",
"globals.buttons.learnMore": "Meer leren",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Meer",
"globals.buttons.new": "Nieuw",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Verwijder",
"globals.buttons.save": "Opslaan",
"globals.buttons.saveChanges": "Veranderingen opslaan",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Bekijken",
"globals.days.0": "Zo",
"globals.days.1": "Zo",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} niet gevonden",
"globals.messages.passwordChange": "Geef een nieuw wachtwoord in",
"globals.messages.passwordChangeFull": "Wis en voer het volledige wachtwoord opnieuw in bij '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Langzame queries worden gecached. Sommige getallen op deze pagina zijn mogelijk niet up-to-date.",
"globals.messages.updated": "\"{name}\" geüpdatet",
"globals.months.1": "Jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Sjabloon | Sjablonen",
"globals.terms.templates": "Sjablonen",
"globals.terms.tx": "Transactioneel | Transactioneel",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Jaar | Jaren",
"import.alreadyRunning": "Er is al een importeeractie bezig. Wacht tot deze gedaan is of annuleer voor het opnieuw te proberen.",
"import.blocklist": "Geblokkeerd",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} records",
"import.stopImport": "Stop importeren",
"import.subscribe": "Inschrijven",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Abonnees importeren",
"import.upload": "Uploaden",
"lists.confirmDelete": "Ben je zeker? Dit verwijdert niet alle abonnees.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Fout bij opslaan thumbnail: {error}",
"media.errorUploading": "Fout bij uploaden bestand: {error}",
"media.invalidFile": "Ongeldig bestand: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Media",
"media.unsupportedFileType": "Bestandstype niet ondersteund ({type})",
"media.upload": "Uploaden",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Opt-in IP-adres registreren",
"settings.privacy.recordOptinIPHelp": "IP-adres van dubbele opt-ins registreren bij abonnee-attributen.",
"settings.restart": "Herstarten",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Ga naar www.hcaptcha.com om de sleutel en het geheim te verkrijgen.",
"settings.security.captchaSecret": "hCaptcha.com-geheim",
"settings.security.enableCaptcha": "Schakel CAPTCHA in",
"settings.security.enableCaptchaHelp": "Schakel CAPTCHA in op het openbare inschrijvingsformulier.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Beveiliging",
"settings.smtp.customHeaders": "Aangepaste headers",
"settings.smtp.customHeadersHelp": "Optionele lijst met e-mail headers om toe te voegen aan alle berichten van deze server. Bv.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Voorbeeld",
"templates.rawHTML": "HTML code",
"templates.subject": "Onderwerp",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Inloggen",
- "users.logout": "Uitloggen"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Uitloggen",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/pl.json b/i18n/pl.json
index b3fb671f7..7459e07f0 100644
--- a/i18n/pl.json
+++ b/i18n/pl.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Włączone",
"globals.buttons.insert": "Wstaw",
"globals.buttons.learnMore": "Dowiedz się więcej",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Więcej",
"globals.buttons.new": "Nowa",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Usuń",
"globals.buttons.save": "Zapisz",
"globals.buttons.saveChanges": "Zapisz zmiany",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Zobacz",
"globals.days.0": "Nd",
"globals.days.1": "Nd",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} nie znaleziono",
"globals.messages.passwordChange": "Podaj wartość do zmiany",
"globals.messages.passwordChangeFull": "Wyczyść i ponownie wprowadź pełne hasło w '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Wolne zapytania są buforowane. Niektóre liczby na tej stronie mogą być nieaktualne.",
"globals.messages.updated": "\"{name}\" zaktualizowano",
"globals.months.1": "Sty",
@@ -232,6 +235,8 @@
"globals.terms.template": "Szablon | Szablony",
"globals.terms.templates": "Szablony",
"globals.terms.tx": "Transakcyjne | Transakcyjne",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Rok | Lat",
"import.alreadyRunning": "Importowanie jest już uruchomione. Poczekaj, aż się zakończy, albo zatrzymaj je przed ponowną próbą.",
"import.blocklist": "Lista zablokowanych",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} rekordów",
"import.stopImport": "Zatrzymaj import",
"import.subscribe": "Subskrypcje",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importuj subskrypcje",
"import.upload": "Wyślij",
"lists.confirmDelete": "Jesteś pewny(a)? To nie usunie subskrybcji.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Błąd zapisywania miniaturki: {error}",
"media.errorUploading": "Błąd wgrywania pliku: {error}",
"media.invalidFile": "Nieprawidłowy plik: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Media",
"media.unsupportedFileType": "Niewspierany typ pliku ({type})",
"media.upload": "Wysyłanie",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Zapisz adres IP zgody na otrzymywanie",
"settings.privacy.recordOptinIPHelp": "Zapisz adres IP podwójnej zgody na otrzymywanie w atrybutach subskrybenta.",
"settings.restart": "Uruchom ponownie",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Klucz witryny hCaptcha.com",
"settings.security.captchaKeyHelp": "Wejdź na www.hcaptcha.com w celu pobrania klucza i sekretu.",
"settings.security.captchaSecret": "Tajny klucz witryny hCaptcha.com",
"settings.security.enableCaptcha": "Włącz CAPTCHA",
"settings.security.enableCaptchaHelp": "Włącz CAPTCHA na publicznym formularzu subskrypcji.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Bezpieczeństwo",
"settings.smtp.customHeaders": "Niestandardowe nagłówki",
"settings.smtp.customHeadersHelp": "Opcjonalna lista nagłówków do zamieszczania w wiadomościach we wszystkich wiadomościach wysłanych z tego serwera. np: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Podgląd",
"templates.rawHTML": "Surowy HTML",
"templates.subject": "Temat",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Zaloguj",
- "users.logout": "Wyloguj"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Wyloguj",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/pt-BR.json b/i18n/pt-BR.json
index e66f30ee2..d6af8d8f6 100644
--- a/i18n/pt-BR.json
+++ b/i18n/pt-BR.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Habilitado",
"globals.buttons.insert": "Inserir",
"globals.buttons.learnMore": "Saiba mais",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Mais",
"globals.buttons.new": "Novo",
"globals.buttons.ok": "Ok",
"globals.buttons.remove": "Excluir",
"globals.buttons.save": "Salvar",
"globals.buttons.saveChanges": "Salvar alterações",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Visualizar",
"globals.days.0": "Dom",
"globals.days.1": "Dom",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} não encontrado",
"globals.messages.passwordChange": "Digite um valor para alterar",
"globals.messages.passwordChangeFull": "Limpe e insira novamente a senha completa em '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "As consultas lentas estão sendo armazenadas em cache. Alguns números nesta página podem não ser atualizados.",
"globals.messages.updated": "\"{name}\"atualizado",
"globals.months.1": "Jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Modelo | Modelos",
"globals.terms.templates": "Modelos",
"globals.terms.tx": "Transacional | Transacionais",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Ano | Anos",
"import.alreadyRunning": "Uma importação já está em execução. Aguarde até que termine ou pare-a antes de tentar novamente.",
"import.blocklist": "Lista de bloqueio",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} registros",
"import.stopImport": "Parar importação",
"import.subscribe": "Inscrever",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importar inscritos",
"import.upload": "Enviar arquivo",
"lists.confirmDelete": "Você tem certeza? Isso não exclui inscritos.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Erro ao salvar miniatura: {error}",
"media.errorUploading": "Erro ao enviar o arquivo: {error}",
"media.invalidFile": "Arquivo inválido: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Mídia",
"media.unsupportedFileType": "Tipo de arquivo não suportado ({type})",
"media.upload": "Enviar arquivo",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Registrar endereço IP de aceitação",
"settings.privacy.recordOptinIPHelp": "Registrar o endereço IP de aceitação dupla nas atributos do assinante.",
"settings.restart": "Reiniciar",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Chave do Site hCaptcha.com",
"settings.security.captchaKeyHelp": "Visite www.hcaptcha.com para obter a chave e o segredo.",
"settings.security.captchaSecret": "Segredo do Site hCaptcha.com",
"settings.security.enableCaptcha": "Habilitar CAPTCHA",
"settings.security.enableCaptchaHelp": "Habilitar CAPTCHA no formulário público de inscrição.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Segurança",
"settings.smtp.customHeaders": "Cabeçalhos personalizados",
"settings.smtp.customHeadersHelp": "Array opcional de cabeçalhos de e-mail para incluir em todas as mensagens enviadas a partir deste servidor. por exemplo: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Pré-visualizar",
"templates.rawHTML": "Código HTML",
"templates.subject": "Assunto",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Entrar",
- "users.logout": "Sair"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Sair",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/pt.json b/i18n/pt.json
index f7e6b319b..7c045bd58 100644
--- a/i18n/pt.json
+++ b/i18n/pt.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Ativo",
"globals.buttons.insert": "Inserir",
"globals.buttons.learnMore": "Saber mais",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Mais",
"globals.buttons.new": "Novo",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Remover",
"globals.buttons.save": "Guardar",
"globals.buttons.saveChanges": "Guardar alterações",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Visualizar",
"globals.days.0": "Dom",
"globals.days.1": "Dom",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} não encontrado",
"globals.messages.passwordChange": "Insere um valor para alterar",
"globals.messages.passwordChangeFull": "Limpe e digite novamente a senha completa em '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "As consultas lentas estão sendo armazenadas em cache. Alguns números nesta página não estarão atualizados.",
"globals.messages.updated": "\"{name}\" atualizado",
"globals.months.1": "Jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Modelo | Modelos",
"globals.terms.templates": "Modelo",
"globals.terms.tx": "Transacional | Transacional",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Ano | Anos",
"import.alreadyRunning": "Uma importação já está em curso. Aguarda que termine ou cancela-a antes de tentares novamente.",
"import.blocklist": "Lista de bloqueio",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} registos",
"import.stopImport": "Parar importação",
"import.subscribe": "Subscrever",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importar subscritores",
"import.upload": "Carregar",
"lists.confirmDelete": "Tens a certeza? Isto não elimina subscritores.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Erro ao guardar miniatura: {error}",
"media.errorUploading": "Erro ao enviar ficheiro: {error}",
"media.invalidFile": "Ficheiro inválido: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Mídia",
"media.unsupportedFileType": "Tipo de ficheiro não suportado ({type})",
"media.upload": "Carregar",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Registrar endereço de IP de opt-in",
"settings.privacy.recordOptinIPHelp": "Registrar o endereço IP de opt-ins duplos nos atributos do assinante.",
"settings.restart": "Reiniciar",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Chave do SiteKey do hCaptcha.com",
"settings.security.captchaKeyHelp": "Visite www.hcaptcha.com para obter a chave e o segredo.",
"settings.security.captchaSecret": "hCaptcha.com segredo",
"settings.security.enableCaptcha": "Ativar o CAPTCHA",
"settings.security.enableCaptchaHelp": "Ativar o CAPTCHA no formulário público de inscrição.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Segurança",
"settings.smtp.customHeaders": "Headers customizados",
"settings.smtp.customHeadersHelp": "Array opcional de headers de email a incluir em todas as mensagens enviadas deste servidor. eg: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Pré-visualização",
"templates.rawHTML": "HTML Simples",
"templates.subject": "Assunto",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Entrar",
- "users.logout": "Sair"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Sair",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/ro.json b/i18n/ro.json
index 72d3b2202..b6070360d 100644
--- a/i18n/ro.json
+++ b/i18n/ro.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "Activat",
"globals.buttons.insert": "Introduceți",
"globals.buttons.learnMore": "Află mai mult",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Mai mult",
"globals.buttons.new": "Nou",
"globals.buttons.ok": "Da",
"globals.buttons.remove": "Elimină",
"globals.buttons.save": "Salvează",
"globals.buttons.saveChanges": "Salvează modificările",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Vezi",
"globals.days.0": "Dum",
"globals.days.1": "Dum",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} nu a fost găsit",
"globals.messages.passwordChange": "Introducerea unei valori de modificat",
"globals.messages.passwordChangeFull": "Ștergeți și reintroduceți parola completă în '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Interogările lente sunt memorate în cache. Unele numere de pe această pagină nu vor fi actualizate.",
"globals.messages.updated": "\"{name}\" actualizat",
"globals.months.1": "Ian",
@@ -233,6 +236,8 @@
"globals.terms.template": "Șabloane WhatsApp",
"globals.terms.templates": "Șabloane",
"globals.terms.tx": "Transactional | Transactional",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Anul",
"import.alreadyRunning": "Un import rulează deja. Așteptă să se termine sau oprește-l înainte de a încerca din nou.",
"import.blocklist": "Lista de blocări",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} / înregistrări {total}",
"import.stopImport": "Importă",
"import.subscribe": "Abonare",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importați abonații",
"import.upload": "Încarcă",
"lists.confirmDelete": "Eşti sigur? Acest lucru nu șterge abonații.",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "Eroare la salvarea miniaturii: {error}",
"media.errorUploading": "Eroare la încărcarea fișierului: {error}",
"media.invalidFile": "Fișier nevalid: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Media",
"media.unsupportedFileType": "Tip de fișier neacceptat ({type})",
"media.upload": "Încarcă",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "Înregistrare adresă IP de opt-in",
"settings.privacy.recordOptinIPHelp": "Înregistrați adresa IP a confirmărilor duble în atributele abonaților.",
"settings.restart": "Repornește",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Cheie SiteKey hCaptcha.com",
"settings.security.captchaKeyHelp": "Vizitați www.hcaptcha.com pentru a obține cheia și secretul.",
"settings.security.captchaSecret": "Secret hCaptcha.com",
"settings.security.enableCaptcha": "Activați CAPTCHA",
"settings.security.enableCaptchaHelp": "Activați CAPTCHA în formularul de abonament public.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Securitate",
"settings.smtp.customHeaders": "Anteturi particularizate",
"settings.smtp.customHeadersHelp": "Matrice opțională de antete de e-mail pentru a include în toate mesajele trimise de pe acest server. de exemplu: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "Previzualizați",
"templates.rawHTML": "HTML brut",
"templates.subject": "Subiect",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Conectează-te",
- "users.logout": "Deconectare"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Deconectare",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/ru.json b/i18n/ru.json
index 3b2d2bd7c..8493e33a5 100644
--- a/i18n/ru.json
+++ b/i18n/ru.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Включено",
"globals.buttons.insert": "Вставить",
"globals.buttons.learnMore": "Подпробней",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Ещё",
"globals.buttons.new": "Новое",
"globals.buttons.ok": "ОК",
"globals.buttons.remove": "Удалить",
"globals.buttons.save": "Сохранить",
"globals.buttons.saveChanges": "Сохранить изменения",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Просмотреть",
"globals.days.0": "Вс",
"globals.days.1": "Вс",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} не найдено",
"globals.messages.passwordChange": "Введите значение для изменения",
"globals.messages.passwordChangeFull": "Очистите и повторно введите полный пароль в '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Медленные запросы кэшируются. Некоторые числа на этой странице могут быть не актуальными.",
"globals.messages.updated": "\"{name}\" обновлено",
"globals.months.1": "Янв",
@@ -232,6 +235,8 @@
"globals.terms.template": "Шаблон | Шаблоны",
"globals.terms.templates": "Шаблоны",
"globals.terms.tx": "Транзакционный | Транзакционный",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Год | Годы",
"import.alreadyRunning": "Импорт уже выполняется. Подождите, пока он закончит, или остановите его, прежде чем пытаться снова. ",
"import.blocklist": "Список блокировки",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} записей",
"import.stopImport": "Остановить импорт",
"import.subscribe": "Подписаться",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Импорт подписчиков",
"import.upload": "Выгрузить",
"lists.confirmDelete": "Уверены? Это не удалит подписчиков.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Ошибка сохранения миниатюры: {error}",
"media.errorUploading": "Ошибка выгрузки файла: {error}",
"media.invalidFile": "Неверный файл: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Медиа",
"media.unsupportedFileType": "Неподдерживаемый тип файла ({type})",
"media.upload": "Выгрузить",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Записывать IP-адрес подписки",
"settings.privacy.recordOptinIPHelp": "Записывать IP-адрес дважды подтверждённых подписок в атрибуты подписчика.",
"settings.restart": "Перезапустить",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com ключ сайта",
"settings.security.captchaKeyHelp": "Посетите www.hcaptcha.com для получения ключа сайта и секретного ключа.",
"settings.security.captchaSecret": "hCaptcha.com секретный ключ",
"settings.security.enableCaptcha": "Включить CAPTCHA",
"settings.security.enableCaptchaHelp": "Включить CAPTCHA на публичной форме подписки.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Безопасность",
"settings.smtp.customHeaders": "Настраиваемые заголовки",
"settings.smtp.customHeadersHelp": "Необязательный массив заголовков e-mail, которые будут включены во все письма, отправляемые с этого сервера. Например: [{\"X-Custom\": \"значение\"}, {\"X-Custom2\": \"значение\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Предпросмотр",
"templates.rawHTML": "Необработанный HTML",
"templates.subject": "Тема",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Вход в систему",
- "users.logout": "Выход из системы"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Выход из системы",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/se.json b/i18n/se.json
index 65b02d840..bbf97914e 100644
--- a/i18n/se.json
+++ b/i18n/se.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Aktiverat",
"globals.buttons.insert": "Infoga",
"globals.buttons.learnMore": "Läs mer",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Mer",
"globals.buttons.new": "Ny",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Ta bort",
"globals.buttons.save": "Spara",
"globals.buttons.saveChanges": "Spara ändringar",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Visa",
"globals.days.0": "sön",
"globals.days.1": "sön",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} hittades inte",
"globals.messages.passwordChange": "Ange ett värde för att ändra",
"globals.messages.passwordChangeFull": "Rensa och ange hela lösenordet i '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Långsamma förfrågningar finns i cacheminnet. En del siffror på den här sidan kommer inte att vara uppdaterade.",
"globals.messages.updated": "\"{name}\" har uppdaterats",
"globals.months.1": "jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Mall | Mallar",
"globals.terms.templates": "Mallar",
"globals.terms.tx": "Transaktion | Transaktioner",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "År | År",
"import.alreadyRunning": "En import körs redan. Vänta tills den är klar eller stoppa den innan du försöker igen.",
"import.blocklist": "Blocklista",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} poster",
"import.stopImport": "Stoppa import",
"import.subscribe": "Prenumerera",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importera prenumeranter",
"import.upload": "Ladda upp",
"lists.confirmDelete": "Är du säker? Detta tar inte bort prenumeranter.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Fel vid spara miniatyrbild: {error}",
"media.errorUploading": "Fel vid uppladdning av fil: {error}",
"media.invalidFile": "Ogiltig fil: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Media",
"media.unsupportedFileType": "Ogiltig filtyp ({type})",
"media.upload": "Ladda upp",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Registrera opt-in-IP-adress",
"settings.privacy.recordOptinIPHelp": "Registrera IP-adress för dubbelopt-in i prenumerationars attribut.",
"settings.restart": "Starta om",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "Besök www.hcaptcha.com för att få nyckeln och hemligheten.",
"settings.security.captchaSecret": "hCaptcha.com hemlighet",
"settings.security.enableCaptcha": "Aktivera CAPTCHA",
"settings.security.enableCaptchaHelp": "Aktivera CAPTCHA på den offentliga prenumerationssidan.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Säkerhet",
"settings.smtp.customHeaders": "Anpassade headers",
"settings.smtp.customHeadersHelp": "Valfri array av e-postheaders att inkludera i alla meddelanden som skickas från den här servern. t.ex: [{\"X-Anpassad\": \"värde\"}, {\"X-Anpassad2\": \"värde\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Förhandsvisa",
"templates.rawHTML": "Rå HTML",
"templates.subject": "Ämne",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Logga in",
- "users.logout": "Logga ut"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Logga ut",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/sk.json b/i18n/sk.json
index 2915a2490..75c2932c5 100644
--- a/i18n/sk.json
+++ b/i18n/sk.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Povolené",
"globals.buttons.insert": "Vložiť",
"globals.buttons.learnMore": "Dalšie informácie",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Viac",
"globals.buttons.new": "Nový",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Odobrať",
"globals.buttons.save": "Uložiť",
"globals.buttons.saveChanges": "Uložiť zmeny",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Zobraziť",
"globals.days.0": "Ne",
"globals.days.1": "Ne",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} sa nenašlo",
"globals.messages.passwordChange": "Zadajte zmenenú hodnotu",
"globals.messages.passwordChangeFull": "Zadajte celé heslo v '{name}' znova.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Pomaly sa vykonávajúce požiadavky sa ukladajú do vyrovnávacej pamäte. Niektoré čísla na tejto stránke môžu byť zastarané.",
"globals.messages.updated": "\"{name}\" upravené",
"globals.months.1": "Jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Šablóna | Šablóny",
"globals.terms.templates": "Šablóny",
"globals.terms.tx": "Transakčné | Transakčné",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Rok | Roky",
"import.alreadyRunning": "Import už beží. Počkajte na jeho dokončenie alebo ho zastavte pred dalším pokusom.",
"import.blocklist": "Zoznam blokovaných",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} záznamov",
"import.stopImport": "Zastaviť import ",
"import.subscribe": "Odoberať",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Importodberateľov",
"import.upload": "Nahrať",
"lists.confirmDelete": "Ste si isti? Týmto sa neodstránia odberatelia.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Chyba pri ukladaní miniatúry: {error}",
"media.errorUploading": "Chyba pri odosielaní súboru: {error}",
"media.invalidFile": "Neplatný súbor: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Médium",
"media.unsupportedFileType": "Nepodporovaný typ súboru ({type})",
"media.upload": "Odoslať",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "Zaznamenávať IP adresu opt-in",
"settings.privacy.recordOptinIPHelp": "Zaznamenávať IP adresu pri dvojitej opt-in v atribútoch odberateľov.",
"settings.restart": "Restarť",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com kľúč webovej stránky",
"settings.security.captchaKeyHelp": "Navštívte www.hcaptcha.com, aby ste získali kľúč a tajomstvo.",
"settings.security.captchaSecret": "hCaptcha.com tajomstvo",
"settings.security.enableCaptcha": "Povoliť CAPTCHA",
"settings.security.enableCaptchaHelp": "Povoliť CAPTCHA vo verejnom formulári na zápis.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Bezpečnostné opatrenia",
"settings.smtp.customHeaders": "Vlastné hlavičky",
"settings.smtp.customHeadersHelp": "Voliteľné polia e-mailových hlavičiek, ktorá sa majú nastaviť do všetkých správ odoslaných z tohoto servera. Napr.: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "Náhľad",
"templates.rawHTML": "Kód HTML",
"templates.subject": "Predmet",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Prihlásiť",
- "users.logout": "Odhlásiť"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Odhlásiť",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/sl.json b/i18n/sl.json
index d23dc8f3f..7e5d6b441 100644
--- a/i18n/sl.json
+++ b/i18n/sl.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Omogočeno",
"globals.buttons.insert": "Vstavi",
"globals.buttons.learnMore": "Več o tem",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Več",
"globals.buttons.new": "Novo",
"globals.buttons.ok": "V redu",
"globals.buttons.remove": "Odstrani",
"globals.buttons.save": "Shrani",
"globals.buttons.saveChanges": "Shrani spremembe",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Pogled",
"globals.days.0": "ned",
"globals.days.1": "ned",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} ni bilo mogoče najti",
"globals.messages.passwordChange": "Vnesite vrednost za spremembo",
"globals.messages.passwordChangeFull": "Počisti in znova vnesi celotno geslo v '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Počasne poizvedbe se predpomnijo. Nekatere številke na tej strani ne bodo posodobljene.",
"globals.messages.updated": "\"{name}\" posodobljeno",
"globals.months.1": "jan",
@@ -232,6 +235,8 @@
"globals.terms.template": "Predloga | Predloge",
"globals.terms.templates": "Predloge",
"globals.terms.tx": "Transakcijsko | Transakcijsko",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Leto | Leta",
"import.alreadyRunning": "Uvoz se že izvaja. Počakajte, da se konča ali ga ustavite, preden poskusite znova.",
"import.blocklist": "Seznam blokiranih",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} zapisov",
"import.stopImport": "Ustavi uvoz",
"import.subscribe": "Naročite se",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Uvozi naročnike",
"import.upload": "Naloži",
"lists.confirmDelete": "Ste prepričani? To ne izbriše naročnikov.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Napaka pri shranjevanju sličice: {error}",
"media.errorUploading": "Napaka pri nalaganju datoteke: {error}",
"media.invalidFile": "Neveljavna datoteka: {napaka}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Mediji",
"media.unsupportedFileType": "Nepodprta vrsta datoteke ({type})",
"media.upload": "Naloži",
@@ -507,11 +514,19 @@
"settings.privacy.recordOptinIP": "Zabeleži IP naslov za privolitev",
"settings.privacy.recordOptinIPHelp": "Zabeleži naslov IP dvojne privolitve v atribute naročnika.",
"settings.restart": "Ponovni zagon",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Ključ mestu hCaptcha.com",
"settings.security.captchaKeyHelp": "Obiščite www.hcaptcha.com za pridobitev ključa in skrivnosti.",
"settings.security.captchaSecret": "skrivnost hCaptcha.com",
"settings.security.enableCaptcha": "Omogoči CAPTCHA",
"settings.security.enableCaptchaHelp": "Omogoči CAPTCHA na javnem obrazcu za naročnino.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Varnost",
"settings.smtp.customHeaders": "Glave po meri",
"settings.smtp.customHeadersHelp": "Izbirno polje e-poštnih glav, ki jih je treba vključiti v vsa sporočila, poslana s tega strežnika. Npr.: [{\"X-Custom\": \"value\"}, {\"X- Custom2\": \"vrednost\"}]",
@@ -587,6 +602,39 @@
"templates.preview": "Predogled",
"templates.rawHTML": "Neobdelani HTML",
"templates.subject": "Zadeva",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Prijava",
- "users.logout": "Odjava"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Odjava",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/tr.json b/i18n/tr.json
index 7ef5dc25d..262c2f197 100644
--- a/i18n/tr.json
+++ b/i18n/tr.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "Etkinleştirildi",
"globals.buttons.insert": "Ekle",
"globals.buttons.learnMore": "Daha fazla öğren",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Daha fazla",
"globals.buttons.new": "Yeni",
"globals.buttons.ok": "Tamam",
"globals.buttons.remove": "Çıkart",
"globals.buttons.save": "Kaydet",
"globals.buttons.saveChanges": "Kaydet değişiklik",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Görüntüle",
"globals.days.0": "Paz",
"globals.days.1": "Paz",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} bulunamadı",
"globals.messages.passwordChange": "Değiştirmek için değer gir",
"globals.messages.passwordChangeFull": "'{name}' içinde parolayı temizleyin ve yeniden girin.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Yavaş sorgular önbelleğe alınıyor. Bu sayfadaki bazı sayılar güncel olmayabilir.",
"globals.messages.updated": "\"{name}\" güncellendi",
"globals.months.1": "Oca",
@@ -233,6 +236,8 @@
"globals.terms.template": "Taslak | Taslaklar",
"globals.terms.templates": "Taslaklar",
"globals.terms.tx": "İşlem | İşlem",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Yıl | Yıllar",
"import.alreadyRunning": "Bir içe aktarım halen sürüyor. Yeniden denemek için durdurun veya yeniden denemek için bekleyin.",
"import.blocklist": "Engelli listesi",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} / {total} kayıt",
"import.stopImport": "İçeri aktarmayı durdur",
"import.subscribe": "Üye ol",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Üyeleri içeri aktar",
"import.upload": "Yükle",
"lists.confirmDelete": "Emin misiniz? Bu işlem üyeleri silmeyecek.",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "Küçük resmi kaydederken hata oluştu: {error}",
"media.errorUploading": "Dosya yüklerken hata oluştu: {error}",
"media.invalidFile": "Hatalı dosya: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Medya",
"media.unsupportedFileType": "Desteklenmeyen dosya tipi ({type})",
"media.upload": "Yükleme",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "Opt-in IP adresini kaydet",
"settings.privacy.recordOptinIPHelp": "Çift onay aboneliklerinin IP adreslerini abone özelliklerinde kaydedin.",
"settings.restart": "Yeniden başlat",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com Site Anahtarı",
"settings.security.captchaKeyHelp": "Anahtarı ve gizli bilgiyi almak için www.hcaptcha.com adresini ziyaret edin.",
"settings.security.captchaSecret": "hCaptcha.com gizli bilgi",
"settings.security.enableCaptcha": "CAPTCHA'yı etkinleştir",
"settings.security.enableCaptchaHelp": "Genel abonelik formunda CAPTCHA'yı etkinleştirin.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Güvenlik",
"settings.smtp.customHeaders": "Özel başlık bilgisi",
"settings.smtp.customHeadersHelp": "Bu sunucudan gönderilen tüm iletilere eklenecek isteğe bağlı e-posta başlıkları dizisi. Örnek: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "Önizleme",
"templates.rawHTML": "Ham HTML",
"templates.subject": "Konu",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Giriş",
- "users.logout": "Çıkış"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Çıkış",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/uk.json b/i18n/uk.json
index f3a6dd188..d793ec689 100644
--- a/i18n/uk.json
+++ b/i18n/uk.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "Увімкнено",
"globals.buttons.insert": "Вставити",
"globals.buttons.learnMore": "Дізнатися більше",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Більше",
"globals.buttons.new": "Створити",
"globals.buttons.ok": "Гаразд",
"globals.buttons.remove": "Вилучити",
"globals.buttons.save": "Зберегти",
"globals.buttons.saveChanges": "Зберегти зміни",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Переглянути",
"globals.days.0": "нд",
"globals.days.1": "нд",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} не знайдено",
"globals.messages.passwordChange": "Щоб змінити, введіть нове значення",
"globals.messages.passwordChangeFull": "Зітріть і введіть заново повний пароль у '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Повільні запити кешуються. Деякі числа на цій сторінці можуть бути неактуальними.",
"globals.messages.updated": "«{name}» оновлено",
"globals.months.1": "січ",
@@ -232,6 +235,8 @@
"globals.terms.template": "Шаблон | Шаблони",
"globals.terms.templates": "Шаблони",
"globals.terms.tx": "Транзакція | Транзакції",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Рік | Роки",
"import.alreadyRunning": "Імпорт уже запущено. Дочекайтеся завершення чи перервіть його, перш ніж повторити спробу.",
"import.blocklist": "Блокування",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} записів",
"import.stopImport": "Перервати імпорт",
"import.subscribe": "Підписка",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Імпортувати підписни_ць",
"import.upload": "Вивантажити",
"lists.confirmDelete": "Точно? Це не видалить підписни_ць.",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "Помилка збереження мініатюри: {error}",
"media.errorUploading": "Помилка вивантаження файлу: {error}",
"media.invalidFile": "Хибний файл: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Картинка",
"media.unsupportedFileType": "Непідтримуваний тип файлу ({type})",
"media.upload": "Вивантажити",
@@ -505,11 +512,19 @@
"settings.privacy.recordOptinIP": "Записувати IP-адресу згоди",
"settings.privacy.recordOptinIPHelp": "Додавати в атрибути підписни_ці IP-адресу подвійної згоди.",
"settings.restart": "Перезапустити",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "SiteKey-значення hCaptcha.com",
"settings.security.captchaKeyHelp": "Щоб отримати ключ і секрет, перейдіть до www.hcaptcha.com.",
"settings.security.captchaSecret": "Секрет hCaptcha.com",
"settings.security.enableCaptcha": "CAPTCHA-підтвердження",
"settings.security.enableCaptchaHelp": "Увімкнути CAPTCHA-підтвердження в загальнодоступній формі підписки.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Захист",
"settings.smtp.customHeaders": "Власні заголовки",
"settings.smtp.customHeadersHelp": "Необов'язковий масив заголовків е-пошти, який слід додавати в усі листи, надіслані цим сервером. Наприклад: [{\"X-Custom\": \"значення\"}, {\"X-Custom2\": \"тощо\"}]",
@@ -585,6 +600,39 @@
"templates.preview": "Переглянути",
"templates.rawHTML": "HTML-код",
"templates.subject": "Тема",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Увійти",
- "users.logout": "Вийти"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Вийти",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/vi.json b/i18n/vi.json
index 1c200cd35..1e4727bf9 100644
--- a/i18n/vi.json
+++ b/i18n/vi.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "Đã bật",
"globals.buttons.insert": "Chèn",
"globals.buttons.learnMore": "Tìm hiểu thêm",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "Thêm",
"globals.buttons.new": "Mới",
"globals.buttons.ok": "OK",
"globals.buttons.remove": "Di chuyển",
"globals.buttons.save": "Lưu",
"globals.buttons.saveChanges": "Lưu thay dổi",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "Xem",
"globals.days.0": "Chủ nhật",
"globals.days.1": "Thứ 2",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} không tìm thấy",
"globals.messages.passwordChange": "Nhập một giá trị để thay đổi",
"globals.messages.passwordChangeFull": "Xóa và nhập lại mật khẩu đầy đủ trong '{name}'.",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "Các truy vấn chậm đang được lưu vào bộ nhớ cache. Một số con số trên trang này có thể không được cập nhật.",
"globals.messages.updated": "\"{name}\" đã cập nhật",
"globals.months.1": "Tháng 1",
@@ -233,6 +236,8 @@
"globals.terms.template": "Mẫu | Mẫu",
"globals.terms.templates": "Mẫu",
"globals.terms.tx": "Giao dịch | Giao dịch",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "Năm | Năm",
"import.alreadyRunning": "Quá trình nhập đang chạy. Chờ quá trình hoàn tất hoặc dừng trước khi thử lại.",
"import.blocklist": "Danh sách chặn",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} / {total} Hồ sơ",
"import.stopImport": "Dừng nhập",
"import.subscribe": "Đặt mua",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "Nhập người đăng ký",
"import.upload": "Tải lên",
"lists.confirmDelete": "Bạn có chắc không? Điều này không xóa người đăng ký.",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "Lỗi khi lưu hình thu nhỏ: {error}",
"media.errorUploading": "Lỗi khi tải tệp lên: {error}",
"media.invalidFile": "Tập tin không hợp lệ: {error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "Phương tiện truyền thông",
"media.unsupportedFileType": "Loại tập tin không được hỗ trợ ({type})",
"media.upload": "Tải lên",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "Ghi lại IP đăng ký",
"settings.privacy.recordOptinIPHelp": "Ghi lại địa chỉ IP của đăng ký kép vào thuộc tính của người đăng ký.",
"settings.restart": "Khởi động lại",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "Khóa trang hCaptcha.com",
"settings.security.captchaKeyHelp": "Truy cập www.hcaptcha.com để lấy khóa và bí mật.",
"settings.security.captchaSecret": "Bí mật trang hCaptcha.com",
"settings.security.enableCaptcha": "Bật CAPTCHA",
"settings.security.enableCaptchaHelp": "Bật CAPTCHA trên biểu mẫu đăng ký công khai.",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "Bảo mật",
"settings.smtp.customHeaders": "Tiêu đề tùy chỉnh",
"settings.smtp.customHeadersHelp": "Mảng tiêu đề e-mail tùy chọn để bao gồm trong tất cả các thư được gửi từ máy chủ này. ví dụ: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "Xem trước",
"templates.rawHTML": "HTML thô",
"templates.subject": "Chủ đề",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "Đăng nhập",
- "users.logout": "Đăng xuất"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "Đăng xuất",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/zh-CN.json b/i18n/zh-CN.json
index cd00009e4..c7d31eb56 100644
--- a/i18n/zh-CN.json
+++ b/i18n/zh-CN.json
@@ -144,12 +144,14 @@
"globals.buttons.enabled": "启用",
"globals.buttons.insert": "添加",
"globals.buttons.learnMore": "更多",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "更多",
"globals.buttons.new": "新建",
"globals.buttons.ok": "好的",
"globals.buttons.remove": "移除",
"globals.buttons.save": "保存",
"globals.buttons.saveChanges": "保存更改",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "查看",
"globals.days.0": "星期日",
"globals.days.1": "星期日",
@@ -190,6 +192,7 @@
"globals.messages.notFound": "{name} 未找到",
"globals.messages.passwordChange": "输入要更改的值",
"globals.messages.passwordChangeFull": "在“{name}”中清除并重新输入完整密码。",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "慢查询正在缓存中。此页面上的某些数字可能不是最新的。",
"globals.messages.updated": "“{name}”已更新",
"globals.months.1": "一月",
@@ -232,6 +235,8 @@
"globals.terms.template": "模板 | 多个模板",
"globals.terms.templates": "模板",
"globals.terms.tx": "交易 | 交易",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "年 | 多年",
"import.alreadyRunning": "导入已在运行。等待它完成或停止它,然后再试一次。",
"import.blocklist": "黑名单",
@@ -259,6 +264,7 @@
"import.recordsCount": "{num} / {total} 条记录",
"import.stopImport": "停止导入",
"import.subscribe": "订阅",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "导入订阅者",
"import.upload": "上传",
"lists.confirmDelete": "你确定吗?这不会删除订阅者。",
@@ -288,6 +294,7 @@
"media.errorSavingThumbnail": "保存缩略图时出错:{error}",
"media.errorUploading": "上传文件时出错:{error}",
"media.invalidFile": "无效文件:{error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "媒体",
"media.unsupportedFileType": "不支持的文件类型 ({type})",
"media.upload": "上传",
@@ -509,11 +516,19 @@
"settings.privacy.recordOptinIP": "记录开通IP地址",
"settings.privacy.recordOptinIPHelp": "在订阅者属性中记录双选订阅的IP地址。",
"settings.restart": "重新开始",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com SiteKey",
"settings.security.captchaKeyHelp": "访问www.hcaptcha.com获取密钥和秘密。",
"settings.security.captchaSecret": "hCaptcha.com秘密",
"settings.security.enableCaptcha": "启用验证码",
"settings.security.enableCaptchaHelp": "在公共订阅表单上启用验证码。",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "安全性",
"settings.smtp.customHeaders": "自定义标头",
"settings.smtp.customHeadersHelp": "要包含在从此服务器发送的所有消息中的可选电子邮件标头数组。例如: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -589,6 +604,39 @@
"templates.preview": "预览",
"templates.rawHTML": "原始HTML",
"templates.subject": "主题",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "登录",
- "users.logout": "登出"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "登出",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/i18n/zh-TW.json b/i18n/zh-TW.json
index 61ee75b64..dd709f13f 100644
--- a/i18n/zh-TW.json
+++ b/i18n/zh-TW.json
@@ -145,12 +145,14 @@
"globals.buttons.enabled": "啟用",
"globals.buttons.insert": "插入",
"globals.buttons.learnMore": "更多",
+ "globals.buttons.manage": "Manage",
"globals.buttons.more": "更多",
"globals.buttons.new": "新增",
"globals.buttons.ok": "好的",
"globals.buttons.remove": "移除",
"globals.buttons.save": "儲存",
"globals.buttons.saveChanges": "儲存變更",
+ "globals.buttons.toggleSelect": "Toggle selection",
"globals.buttons.view": "檢視",
"globals.days.0": "星期日",
"globals.days.1": "星期日",
@@ -191,6 +193,7 @@
"globals.messages.notFound": "{name} 未找到",
"globals.messages.passwordChange": "輸入要變更的密碼",
"globals.messages.passwordChangeFull": "在 '{name}' 中清除並重新輸入完整密碼。",
+ "globals.messages.permissionDenied": "Permission denied: {name}",
"globals.messages.slowQueriesCached": "正在進行慢速查詢。此頁面上的部分數字可能不是最新的。",
"globals.messages.updated": "“{name}”已更新",
"globals.months.1": "一月",
@@ -233,6 +236,8 @@
"globals.terms.template": "版型| 多個版型",
"globals.terms.templates": "版型",
"globals.terms.tx": "交易 | 交易",
+ "globals.terms.user": "User | Users",
+ "globals.terms.users": "Users",
"globals.terms.year": "年| 多年",
"import.alreadyRunning": "匯入正在進行中。等待它完成或停止它,然後再試一次。",
"import.blocklist": "黑名單",
@@ -260,6 +265,7 @@
"import.recordsCount": "{num} / {total} 條記錄",
"import.stopImport": "停止匯入",
"import.subscribe": "訂閱",
+ "import.subscribeWarning": "Overwriting will re-subscribe unusbscribed e-mails. Continue?",
"import.title": "匯入訂閱者",
"import.upload": "上傳",
"lists.confirmDelete": "你確定嗎?這不會刪除訂閱者。",
@@ -289,6 +295,7 @@
"media.errorSavingThumbnail": "儲存縮圖時出錯:{error}",
"media.errorUploading": "上傳文件時出錯:{error}",
"media.invalidFile": "無效文件:{error}",
+ "media.invalidFileName": "Invalid filename {name}. Use only ASCII characters",
"media.title": "媒體",
"media.unsupportedFileType": "不支援的檔案類型({type})",
"media.upload": "上傳",
@@ -510,11 +517,19 @@
"settings.privacy.recordOptinIP": "記錄訂閱同意的 IP 位址",
"settings.privacy.recordOptinIPHelp": "在訂閱者屬性中記錄 double opt-ins 的 IP 位址。",
"settings.restart": "重新開始",
+ "settings.security.OIDCClientID": "Client ID",
+ "settings.security.OIDCClientSecret": "Client secret",
+ "settings.security.OIDCHelp": "Enable OpenID Connect OAuth2 login via an OAuth provider.",
+ "settings.security.OIDCRedirectURL": "Redirect URL for oAuth provider",
+ "settings.security.OIDCRedirectWarning": "This does not seem to be a production URL. Change the Root URL in 'General' settings.",
+ "settings.security.OIDCURL": "Provider URL",
+ "settings.security.OIDCWarning": "When OIDC is enabled, default password login is disabled. Invalid config can lock you out.",
"settings.security.captchaKey": "hCaptcha.com 網站金鑰",
"settings.security.captchaKeyHelp": "開啟 www.hcaptcha.com 獲取金鑰和密鑰。",
"settings.security.captchaSecret": "hCaptcha.com 密鑰",
"settings.security.enableCaptcha": "啟用 CAPTCHA 驗證",
"settings.security.enableCaptchaHelp": "在公開訂閱表單上啟用 CAPTCHA 驗證。",
+ "settings.security.enableOIDC": "Enable OIDC SSO",
"settings.security.name": "安全性",
"settings.smtp.customHeaders": "自定義 header",
"settings.smtp.customHeadersHelp": "可選擇性的排列此伺服器寄送的所有電子郵件 headers。例如: [{\"X-Custom\": \"value\"}, {\"X-Custom2\": \"value\"}]",
@@ -590,6 +605,39 @@
"templates.preview": "預覽",
"templates.rawHTML": "原始 HTML",
"templates.subject": "主題",
+ "users.apiOneTimeToken": "Copy the API access token now. It will not be shown again.",
+ "users.cantDeleteRole": "Cannot delete role that is in use.",
+ "users.invalidLogin": "Invalid login or password",
+ "users.invalidRequest": "Invalid auth request",
+ "users.lastLogin": "Last login",
+ "users.listPerms": "List permissions",
+ "users.listPermsWarning": "lists:get_all or lists:manage_all are enabled which overrides per-list permissions",
+ "users.listRole": "List roles | List role",
+ "users.listRoles": "List roles",
"users.login": "登入",
- "users.logout": "登出"
+ "users.loginOIDC": "Login with {name}",
+ "users.logout": "登出",
+ "users.needSuper": "User(s) couldn't updated. There has to be at least one active Super Admin user.",
+ "users.newListRole": "New list role",
+ "users.newUser": "New user",
+ "users.newUserRole": "New user role",
+ "users.password": "Password",
+ "users.passwordEnable": "Enable password login",
+ "users.passwordMismatch": "Passwords don't match",
+ "users.passwordRepeat": "Repeat password",
+ "users.perms": "Permissions",
+ "users.profile": "Profile",
+ "users.role": "Role | Roles",
+ "users.roleGroup": "Group",
+ "users.roles": "Roles",
+ "users.status.disabled": "Disabled",
+ "users.status.enabled": "Enabled",
+ "users.type": "Type",
+ "users.type.api": "API",
+ "users.type.super": "Super Admin",
+ "users.type.user": "User",
+ "users.userRole": "User role | User roles",
+ "users.userRoles": "User roles",
+ "users.username": "Username",
+ "users.usernameHelp": "Used with password login"
}
diff --git a/internal/auth/auth.go b/internal/auth/auth.go
new file mode 100644
index 000000000..735725940
--- /dev/null
+++ b/internal/auth/auth.go
@@ -0,0 +1,372 @@
+package auth
+
+import (
+ "context"
+ "crypto/subtle"
+ "database/sql"
+ "encoding/base64"
+ "errors"
+ "fmt"
+ "log"
+ "net/http"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/coreos/go-oidc/v3/oidc"
+ "github.com/knadh/listmonk/models"
+ "github.com/labstack/echo/v4"
+ "github.com/zerodha/simplesessions/stores/postgres/v3"
+ "github.com/zerodha/simplesessions/v3"
+ "golang.org/x/oauth2"
+)
+
+const (
+ // UserKey is the key on which the User profile is set on echo handlers.
+ UserKey = "auth_user"
+ SessionKey = "auth_session"
+ SuperAdminRoleID = 1
+)
+
+const (
+ sessTypeNative = "native"
+ sessTypeOIDC = "oidc"
+)
+
+type OIDCclaim struct {
+ Email string `json:"email"`
+ EmailVerified bool `json:"email_verified"`
+ Sub string `json:"sub"`
+ Picture string `json:"picture"`
+}
+
+type OIDCConfig struct {
+ Enabled bool `json:"enabled"`
+ ProviderURL string `json:"provider_url"`
+ RedirectURL string `json:"redirect_url"`
+ ClientID string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+}
+
+type BasicAuthConfig struct {
+ Enabled bool `json:"enabled"`
+ Username string `json:"username"`
+ Password string `json:"password"`
+}
+
+type Config struct {
+ OIDC OIDCConfig
+ BasicAuth BasicAuthConfig
+}
+
+// Callbacks takes two callback functions required by simplesessions.
+type Callbacks struct {
+ SetCookie func(cookie *http.Cookie, w interface{}) error
+ GetCookie func(name string, r interface{}) (*http.Cookie, error)
+ GetUser func(id int) (models.User, error)
+}
+
+type Auth struct {
+ apiUsers map[string]models.User
+ sync.RWMutex
+
+ cfg Config
+ oauthCfg oauth2.Config
+ verifier *oidc.IDTokenVerifier
+ sess *simplesessions.Manager
+ sessStore *postgres.Store
+ cb *Callbacks
+ log *log.Logger
+}
+
+func New(cfg Config, db *sql.DB, cb *Callbacks, lo *log.Logger) (*Auth, error) {
+ a := &Auth{
+ cfg: cfg,
+ cb: cb,
+ log: lo,
+
+ apiUsers: map[string]models.User{},
+ }
+
+ // Initialize OIDC.
+ if cfg.OIDC.Enabled {
+ provider, err := oidc.NewProvider(context.Background(), cfg.OIDC.ProviderURL)
+ if err != nil {
+ lo.Printf("error initializing OIDC OAuth provider: %v", err)
+ } else {
+
+ a.verifier = provider.Verifier(&oidc.Config{
+ ClientID: cfg.OIDC.ClientID,
+ })
+
+ a.oauthCfg = oauth2.Config{
+ ClientID: cfg.OIDC.ClientID,
+ ClientSecret: cfg.OIDC.ClientSecret,
+ Endpoint: provider.Endpoint(),
+ RedirectURL: cfg.OIDC.RedirectURL,
+ Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
+ }
+ }
+ }
+
+ // Initialize session manager.
+ a.sess = simplesessions.New(simplesessions.Options{
+ EnableAutoCreate: false,
+ SessionIDLength: 64,
+ Cookie: simplesessions.CookieOptions{
+ IsHTTPOnly: true,
+ MaxAge: time.Hour * 24 * 7,
+ },
+ })
+ st, err := postgres.New(postgres.Opt{}, db)
+ if err != nil {
+ return nil, err
+ }
+ a.sessStore = st
+ a.sess.UseStore(st)
+ a.sess.SetCookieHooks(cb.GetCookie, cb.SetCookie)
+
+ // Prune dead sessions from the DB periodically.
+ go func() {
+ if err := st.Prune(); err != nil {
+ lo.Printf("error pruning login sessions: %v", err)
+ }
+ time.Sleep(time.Hour * 12)
+ }()
+
+ return a, nil
+}
+
+// CacheAPIUsers caches API users for authenticating requests. It wipes
+// the existing cache every time and is meant for syncing all API users
+// in the database in one shot.
+func (o *Auth) CacheAPIUsers(users []models.User) {
+ o.Lock()
+ o.apiUsers = map[string]models.User{}
+
+ for _, u := range users {
+ o.apiUsers[u.Username] = u
+ }
+ o.Unlock()
+}
+
+// CacheAPIUser caches an API user for authenticating requests.
+func (o *Auth) CacheAPIUser(u models.User) {
+ o.Lock()
+ o.apiUsers[u.Username] = u
+ o.Unlock()
+}
+
+// GetAPIToken validates an API user+token.
+func (o *Auth) GetAPIToken(user string, token string) (models.User, bool) {
+ o.RLock()
+ t, ok := o.apiUsers[user]
+ o.RUnlock()
+
+ if !ok || subtle.ConstantTimeCompare([]byte(t.Password.String), []byte(token)) != 1 {
+ return models.User{}, false
+ }
+
+ return t, true
+}
+
+// GetOIDCAuthURL returns the OIDC provider's auth URL to redirect to.
+func (o *Auth) GetOIDCAuthURL(state, nonce string) string {
+ return o.oauthCfg.AuthCodeURL(state, oidc.Nonce(nonce))
+}
+
+// ExchangeOIDCToken takes an OIDC authorization code (recieved via redirect from the OIDC provider),
+// validates it, and returns an OIDC token for subsequent auth.
+func (o *Auth) ExchangeOIDCToken(code, nonce string) (string, OIDCclaim, error) {
+ tk, err := o.oauthCfg.Exchange(context.TODO(), code)
+ if err != nil {
+ return "", OIDCclaim{}, echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("error exchanging token: %v", err))
+ }
+
+ rawIDTk, ok := tk.Extra("id_token").(string)
+ if !ok {
+ return "", OIDCclaim{}, echo.NewHTTPError(http.StatusUnauthorized, "`id_token` missing.")
+ }
+
+ idTk, err := o.verifier.Verify(context.TODO(), rawIDTk)
+ if err != nil {
+ return "", OIDCclaim{}, echo.NewHTTPError(http.StatusUnauthorized, fmt.Sprintf("error verifying ID token: %v", err))
+ }
+
+ if idTk.Nonce != nonce {
+ return "", OIDCclaim{}, echo.NewHTTPError(http.StatusUnauthorized, "nonce did not match")
+ }
+
+ var claims OIDCclaim
+ if err := idTk.Claims(&claims); err != nil {
+ return "", OIDCclaim{}, errors.New("error getting user from OIDC")
+ }
+
+ return rawIDTk, claims, nil
+}
+
+// Middleware is the HTTP middleware used for wrapping HTTP handlers registered on the echo router.
+// It authorizes token (BasicAuth/token) based and cookie based sessions and on successful auth,
+// sets the authenticated User{} on the echo context on the key UserKey. On failure, it sets an Error{}
+// instead on the same key.
+func (o *Auth) Middleware(next echo.HandlerFunc) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ // It's an `Authorization` header request.
+ hdr := strings.TrimSpace(c.Request().Header.Get("Authorization"))
+
+ // If cookie is set, ignore BasicAuth. This is to preserve backwards compatibility
+ // in v3 -> v4 upgrade where the user browser sessions would still have old
+ // BasicAuth credentials, which no longer work in the new system which expects
+ // session cookies instead, which causes a redirect loop despite loggin in and session
+ // cookies being set.
+ //
+ // TODO: This should be removed in a future version.
+ if c := strings.TrimSpace(c.Request().Header.Get("Cookie")); strings.Contains(c, "session=") {
+ hdr = ""
+ }
+
+ if len(hdr) > 0 {
+ key, token, err := parseAuthHeader(hdr)
+ if err != nil {
+ c.Set(UserKey, echo.NewHTTPError(http.StatusForbidden, err.Error()))
+ return next(c)
+ }
+
+ // Validate the token.
+ user, ok := o.GetAPIToken(key, token)
+ if !ok {
+ c.Set(UserKey, echo.NewHTTPError(http.StatusForbidden, "invalid API credentials"))
+ return next(c)
+ }
+
+ // Set the user details on the handler context.
+ c.Set(UserKey, user)
+ return next(c)
+ }
+
+ // Is it a cookie based session?
+ sess, user, err := o.validateSession(c)
+ if err != nil {
+ c.Set(UserKey, echo.NewHTTPError(http.StatusForbidden, "invalid session"))
+ return next(c)
+ }
+
+ // Set the user details on the handler context.
+ c.Set(UserKey, user)
+ c.Set(SessionKey, sess)
+ return next(c)
+ }
+}
+
+func (o *Auth) Perm(next echo.HandlerFunc, perms ...string) echo.HandlerFunc {
+ return func(c echo.Context) error {
+ u, ok := c.Get(UserKey).(models.User)
+ if !ok {
+ c.Set(UserKey, echo.NewHTTPError(http.StatusForbidden, "invalid session"))
+ return next(c)
+ }
+
+ // If the current user is a Super Admin user, do no checks.
+ if u.UserRole.ID == SuperAdminRoleID {
+ return next(c)
+ }
+
+ // Check if the current handler's permission is in the user's permission map.
+ var (
+ has = false
+ perm = ""
+ )
+ for _, perm = range perms {
+ if _, ok := u.PermissionsMap[perm]; ok {
+ has = true
+ break
+ }
+ }
+
+ if !has {
+ return echo.NewHTTPError(http.StatusForbidden, fmt.Sprintf("permission denied: %s", perm))
+ }
+
+ return next(c)
+ }
+}
+
+// SaveSession creates and sets a session (post successful login/auth).
+func (o *Auth) SaveSession(u models.User, oidcToken string, c echo.Context) error {
+ sess, err := o.sess.NewSession(c, c)
+ if err != nil {
+ o.log.Printf("error creating login session: %v", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "error creating session")
+ }
+
+ if err := sess.SetMulti(map[string]interface{}{"user_id": u.ID, "oidc_token": oidcToken}); err != nil {
+ o.log.Printf("error setting login session: %v", err)
+ return echo.NewHTTPError(http.StatusInternalServerError, "error creating session")
+ }
+
+ return nil
+}
+
+func (o *Auth) validateSession(c echo.Context) (*simplesessions.Session, models.User, error) {
+ // Cookie session.
+ sess, err := o.sess.Acquire(nil, c, c)
+ if err != nil {
+ return nil, models.User{}, echo.NewHTTPError(http.StatusForbidden, err.Error())
+ }
+
+ // Get the session variables.
+ vars, err := sess.GetMulti("user_id", "oidc_token")
+ if err != nil {
+ return nil, models.User{}, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ // Validate the user ID in the session.
+ userID, err := o.sessStore.Int(vars["user_id"], nil)
+ if err != nil || userID < 1 {
+ o.log.Printf("error fetching session user ID: %v", err)
+ return nil, models.User{}, echo.NewHTTPError(http.StatusInternalServerError, err.Error())
+ }
+
+ // Fetch user details from the database.
+ user, err := o.cb.GetUser(userID)
+ if err != nil {
+ o.log.Printf("error fetching session user: %v", err)
+ }
+
+ return sess, user, err
+}
+
+// parseAuthHeader parses the Authorization header and returns the api_key and access_token.
+func parseAuthHeader(h string) (string, string, error) {
+ const authBasic = "Basic"
+ const authToken = "token"
+
+ var (
+ pair []string
+ delim = ":"
+ )
+
+ if strings.HasPrefix(h, authToken) {
+ // token api_key:access_token.
+ pair = strings.SplitN(strings.Trim(h[len(authToken):], " "), delim, 2)
+ } else if strings.HasPrefix(h, authBasic) {
+ // HTTP BasicAuth. This is supported for backwards compatibility.
+ payload, err := base64.StdEncoding.DecodeString(string(strings.Trim(h[len(authBasic):], " ")))
+ if err != nil {
+ return "", "", echo.NewHTTPError(http.StatusBadRequest, "invalid Base64 value in Basic Authorization header")
+ }
+ pair = strings.SplitN(string(payload), delim, 2)
+ } else {
+ return "", "", echo.NewHTTPError(http.StatusBadRequest, "unknown Authorization scheme")
+ }
+
+ if len(pair) < 2 {
+ return "", "", echo.NewHTTPError(http.StatusBadRequest, "api_key:token missing")
+ }
+
+ if len(pair[0]) == 0 || len(pair[1]) == 0 {
+ return "", "", echo.NewHTTPError(http.StatusBadRequest, "empty `api_key` or `token`")
+ }
+
+ return pair[0], pair[1], nil
+}
diff --git a/internal/core/lists.go b/internal/core/lists.go
index 1db7f1112..0b8c7285b 100644
--- a/internal/core/lists.go
+++ b/internal/core/lists.go
@@ -10,10 +10,10 @@ import (
)
// GetLists gets all lists optionally filtered by type.
-func (c *Core) GetLists(typ string) ([]models.List, error) {
+func (c *Core) GetLists(typ string, getAll bool, permittedIDs []int) ([]models.List, error) {
out := []models.List{}
- if err := c.q.GetLists.Select(&out, typ, "id"); err != nil {
+ if err := c.q.GetLists.Select(&out, typ, "id", getAll, pq.Array(permittedIDs)); err != nil {
c.log.Printf("error fetching lists: %v", err)
return nil, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
@@ -36,7 +36,7 @@ func (c *Core) GetLists(typ string) ([]models.List, error) {
// QueryLists gets multiple lists based on multiple query params. Along with the paginated and sliced
// results, the total number of lists in the DB is returned.
-func (c *Core) QueryLists(searchStr, typ, optin string, tags []string, orderBy, order string, offset, limit int) ([]models.List, int, error) {
+func (c *Core) QueryLists(searchStr, typ, optin string, tags []string, orderBy, order string, getAll bool, permittedIDs []int, offset, limit int) ([]models.List, int, error) {
_ = c.refreshCache(matListSubStats, false)
if tags == nil {
@@ -47,7 +47,7 @@ func (c *Core) QueryLists(searchStr, typ, optin string, tags []string, orderBy,
out = []models.List{}
queryStr, stmt = makeSearchQuery(searchStr, orderBy, order, c.q.QueryLists, listQuerySortFields)
)
- if err := c.db.Select(&out, stmt, 0, "", queryStr, typ, optin, pq.StringArray(tags), offset, limit); err != nil {
+ if err := c.db.Select(&out, stmt, 0, "", queryStr, typ, optin, pq.StringArray(tags), getAll, pq.Array(permittedIDs), offset, limit); err != nil {
c.log.Printf("error fetching lists: %v", err)
return nil, 0, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
@@ -82,7 +82,7 @@ func (c *Core) GetList(id int, uuid string) (models.List, error) {
var res []models.List
queryStr, stmt := makeSearchQuery("", "", "", c.q.QueryLists, nil)
- if err := c.db.Select(&res, stmt, id, uu, queryStr, "", "", pq.StringArray{}, 0, 1); err != nil {
+ if err := c.db.Select(&res, stmt, id, uu, queryStr, "", "", pq.StringArray{}, true, nil, 0, 1); err != nil {
c.log.Printf("error fetching lists: %v", err)
return models.List{}, echo.NewHTTPError(http.StatusInternalServerError,
c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.lists}", "error", pqErrMsg(err)))
diff --git a/internal/core/roles.go b/internal/core/roles.go
new file mode 100644
index 000000000..346c91f83
--- /dev/null
+++ b/internal/core/roles.go
@@ -0,0 +1,163 @@
+package core
+
+import (
+ "encoding/json"
+ "net/http"
+
+ "github.com/knadh/listmonk/models"
+ "github.com/labstack/echo/v4"
+ "github.com/lib/pq"
+)
+
+// GetRoles retrieves all roles.
+func (c *Core) GetRoles() ([]models.Role, error) {
+ out := []models.Role{}
+ if err := c.q.GetUserRoles.Select(&out); err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorFetching", "name", "role", "error", pqErrMsg(err)))
+ }
+
+ return out, nil
+}
+
+// GetListRoles retrieves all list roles.
+func (c *Core) GetListRoles() ([]models.ListRole, error) {
+ out := []models.ListRole{}
+ if err := c.q.GetListRoles.Select(&out); err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorFetching", "name", "role", "error", pqErrMsg(err)))
+ }
+
+ // Unmarshall the nested list permissions, if any.
+ for n, r := range out {
+ if r.ListsRaw == nil {
+ continue
+ }
+
+ if err := json.Unmarshal(r.ListsRaw, &out[n].Lists); err != nil {
+ c.log.Printf("error unmarshalling list permissions for role %d: %v", r.ID, err)
+ }
+ }
+
+ return out, nil
+}
+
+// CreateRole creates a new role.
+func (c *Core) CreateRole(r models.Role) (models.Role, error) {
+ var out models.Role
+
+ if err := c.q.CreateRole.Get(&out, r.Name, models.RoleTypeUser, pq.Array(r.Permissions)); err != nil {
+ return out, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorCreating", "name", "{users.role}", "error", pqErrMsg(err)))
+ }
+
+ return out, nil
+}
+
+// CreateListRole creates a new list role.
+func (c *Core) CreateListRole(r models.ListRole) (models.ListRole, error) {
+ var out models.ListRole
+
+ if err := c.q.CreateRole.Get(&out, r.Name, models.RoleTypeList, pq.Array([]string{})); err != nil {
+ return out, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorCreating", "name", "{users.role}", "error", pqErrMsg(err)))
+ }
+
+ if err := c.UpsertListPermissions(out.ID, r.Lists); err != nil {
+ return out, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorCreating", "name", "{users.role}", "error", pqErrMsg(err)))
+ }
+
+ return out, nil
+}
+
+// UpsertListPermissions upserts permission for a role.
+func (c *Core) UpsertListPermissions(roleID int, lp []models.ListPermission) error {
+ var (
+ listIDs = make([]int, 0, len(lp))
+ listPerms = make([][]string, 0, len(lp))
+ )
+ for _, p := range lp {
+ if len(p.Permissions) == 0 {
+ continue
+ }
+
+ listIDs = append(listIDs, p.ID)
+
+ // For the Postgres array unnesting query to work, all permissions arrays should
+ // have equal number of entries. Add "" in case there's only one of either list:get or list:manage
+ perms := make([]string, 2)
+ copy(perms[:], p.Permissions[:])
+ listPerms = append(listPerms, perms)
+ }
+
+ if _, err := c.q.UpsertListPermissions.Exec(roleID, pq.Array(listIDs), pq.Array(listPerms)); err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorCreating", "name", "{users.role}", "error", pqErrMsg(err)))
+ }
+
+ return nil
+}
+
+// DeleteListPermission deletes a list permission entry from a role.
+func (c *Core) DeleteListPermission(roleID, listID int) error {
+ if _, err := c.q.DeleteListPermission.Exec(roleID, listID); err != nil {
+ if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "users_role_id_fkey" {
+ return echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("users.cantDeleteRole"))
+ }
+ return echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorDeleting", "name", "{users.role}", "error", pqErrMsg(err)))
+ }
+
+ return nil
+}
+
+// UpdateUserRole updates a given role.
+func (c *Core) UpdateUserRole(id int, r models.Role) (models.Role, error) {
+ var out models.Role
+
+ if err := c.q.UpdateRole.Get(&out, id, r.Name, pq.Array(r.Permissions)); err != nil {
+ return out, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorUpdating", "name", "{users.userRole}", "error", pqErrMsg(err)))
+ }
+
+ if out.ID == 0 {
+ return out, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("globals.messages.notFound", "name", "{users.userRole}"))
+ }
+
+ return out, nil
+}
+
+// UpdateListRole updates a given role.
+func (c *Core) UpdateListRole(id int, r models.ListRole) (models.ListRole, error) {
+ var out models.ListRole
+
+ if err := c.q.UpdateRole.Get(&out, id, r.Name, pq.Array([]string{})); err != nil {
+ return out, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorUpdating", "name", "{users.listRole}", "error", pqErrMsg(err)))
+ }
+
+ if out.ID == 0 {
+ return out, echo.NewHTTPError(http.StatusBadRequest, c.i18n.Ts("globals.messages.notFound", "name", "{users.listRole}"))
+ }
+
+ if err := c.UpsertListPermissions(out.ID, r.Lists); err != nil {
+ return out, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorCreating", "name", "{users.listRole}", "error", pqErrMsg(err)))
+ }
+
+ return out, nil
+}
+
+// DeleteRole deletes a given role.
+func (c *Core) DeleteRole(id int) error {
+ if _, err := c.q.DeleteRole.Exec(id); err != nil {
+ if pqErr, ok := err.(*pq.Error); ok && pqErr.Constraint == "users_role_id_fkey" {
+ return echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("users.cantDeleteRole"))
+ }
+ return echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorDeleting", "name", "{users.role}", "error", pqErrMsg(err)))
+ }
+
+ return nil
+}
diff --git a/internal/core/subscribers.go b/internal/core/subscribers.go
index 64c0b5509..759c11196 100644
--- a/internal/core/subscribers.go
+++ b/internal/core/subscribers.go
@@ -43,6 +43,27 @@ func (c *Core) GetSubscriber(id int, uuid, email string) (models.Subscriber, err
return out[0], nil
}
+// HasSubscriberLists checks if the given subscribers have at least one of the given lists.
+func (c *Core) HasSubscriberLists(subIDs []int, listIDs []int) (map[int]bool, error) {
+ res := []struct {
+ SubID int `db:"subscriber_id"`
+ Has bool `db:"has"`
+ }{}
+
+ if err := c.q.HasSubscriberLists.Select(&res, pq.Array(subIDs), pq.Array(listIDs)); err != nil {
+ c.log.Printf("error fetching subscriber: %v", err)
+ return nil, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.subscriber}", "error", pqErrMsg(err)))
+ }
+
+ out := make(map[int]bool, len(res))
+ for _, r := range res {
+ out[r.SubID] = r.Has
+ }
+
+ return out, nil
+}
+
// GetSubscribersByEmail fetches a subscriber by one of the given params.
func (c *Core) GetSubscribersByEmail(emails []string) (models.Subscribers, error) {
var out models.Subscribers
diff --git a/internal/core/users.go b/internal/core/users.go
new file mode 100644
index 000000000..eae8e912a
--- /dev/null
+++ b/internal/core/users.go
@@ -0,0 +1,214 @@
+package core
+
+import (
+ "database/sql"
+ "encoding/json"
+ "errors"
+ "net/http"
+ "strings"
+
+ "github.com/knadh/listmonk/internal/utils"
+ "github.com/knadh/listmonk/models"
+ "github.com/labstack/echo/v4"
+ "github.com/lib/pq"
+ "gopkg.in/volatiletech/null.v6"
+)
+
+// GetUsers retrieves all users.
+func (c *Core) GetUsers() ([]models.User, error) {
+ out, err := c.getUsers(0, "", "")
+ return out, err
+}
+
+// GetUser retrieves a specific user based on any one given identifier.
+func (c *Core) GetUser(id int, username, email string) (models.User, error) {
+ out, err := c.getUsers(id, username, strings.ToLower(email))
+ if err != nil {
+ return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
+ }
+
+ return out[0], nil
+}
+
+// CreateUser creates a new user.
+func (c *Core) CreateUser(u models.User) (models.User, error) {
+ var id int
+
+ // If it's an API user, generate a random token for password
+ // and set the e-mail to default.
+ if u.Type == models.UserTypeAPI {
+ // Generate a random admin password.
+ tk, err := utils.GenerateRandomString(32)
+ if err != nil {
+ return models.User{}, err
+ }
+
+ u.Email = null.String{String: u.Username + "@api", Valid: true}
+ u.PasswordLogin = false
+ u.Password = null.String{String: tk, Valid: true}
+ }
+
+ if err := c.q.CreateUser.Get(&id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.UserRoleID, u.ListRoleID, u.Status); err != nil {
+ return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorCreating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
+ }
+
+ // Hide the password field in the response except for when the user type is an API token,
+ // where the frontend shows the token on the UI just once.
+ if u.Type != models.UserTypeAPI {
+ u.Password = null.String{Valid: false}
+ }
+
+ out, err := c.GetUser(id, "", "")
+ return out, err
+}
+
+// UpdateUser updates a given user.
+func (c *Core) UpdateUser(id int, u models.User) (models.User, error) {
+ listRoleID := 0
+ if u.ListRoleID == nil {
+ listRoleID = -1
+ } else {
+ listRoleID = *u.ListRoleID
+ }
+
+ res, err := c.q.UpdateUser.Exec(id, u.Username, u.PasswordLogin, u.Password, u.Email, u.Name, u.Type, u.UserRoleID, listRoleID, u.Status)
+ if err != nil {
+ return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
+ }
+
+ if n, _ := res.RowsAffected(); n == 0 {
+ return models.User{}, echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("users.needSuper"))
+ }
+
+ out, err := c.GetUser(id, "", "")
+
+ return out, err
+}
+
+// UpdateUserProfile updates the basic fields of a given uesr (name, email, password).
+func (c *Core) UpdateUserProfile(id int, u models.User) (models.User, error) {
+ res, err := c.q.UpdateUserProfile.Exec(id, u.Name, u.Email, u.PasswordLogin, u.Password)
+ if err != nil {
+ return models.User{}, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
+ }
+
+ if n, _ := res.RowsAffected(); n == 0 {
+ return models.User{}, echo.NewHTTPError(http.StatusBadRequest,
+ c.i18n.Ts("globals.messages.notFound", "name", "{globals.terms.user}"))
+ }
+
+ return c.GetUser(id, "", "")
+}
+
+// UpdateUserLogin updates a user's record post-login.
+func (c *Core) UpdateUserLogin(id int, avatar string) error {
+ if _, err := c.q.UpdateUserLogin.Exec(id, avatar); err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorUpdating", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
+ }
+
+ return nil
+}
+
+// DeleteUsers deletes a given user.
+func (c *Core) DeleteUsers(ids []int) error {
+ res, err := c.q.DeleteUsers.Exec(pq.Array(ids))
+ if err != nil {
+ return echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorDeleting", "name", "{globals.terms.user}", "error", pqErrMsg(err)))
+ }
+ if num, err := res.RowsAffected(); err != nil || num == 0 {
+ return echo.NewHTTPError(http.StatusBadRequest, c.i18n.T("users.needSuper"))
+ }
+
+ return nil
+}
+
+// LoginUser attempts to log the given user_id in by matching the password.
+func (c *Core) LoginUser(username, password string) (models.User, error) {
+ var out models.User
+ if err := c.q.LoginUser.Get(&out, username, password); err != nil {
+ if err == sql.ErrNoRows {
+ return out, echo.NewHTTPError(http.StatusForbidden,
+ c.i18n.T("users.invalidLogin"))
+ }
+
+ return out, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
+ }
+
+ return out, nil
+}
+
+func (c *Core) getUsers(id int, username, email string) ([]models.User, error) {
+ out := []models.User{}
+ if err := c.q.GetUsers.Select(&out, id, username, email); err != nil {
+ return nil, echo.NewHTTPError(http.StatusInternalServerError,
+ c.i18n.Ts("globals.messages.errorFetching", "name", "{globals.terms.users}", "error", pqErrMsg(err)))
+ }
+
+ if len(out) == 0 {
+ return nil, errors.New("user not found")
+ }
+
+ for n, u := range out {
+ u := u
+
+ if u.Password.String != "" {
+ u.HasPassword = true
+ u.PasswordLogin = true
+ }
+
+ if u.Type == models.UserTypeAPI {
+ u.Email = null.String{}
+ }
+
+ u.UserRole.ID = u.UserRoleID
+ u.UserRole.Name = u.UserRoleName
+ u.UserRole.Permissions = u.UserRolePerms
+ u.UserRoleID = 0
+
+ // Prepare lookup maps.
+ u.ListPermissionsMap = make(map[int]map[string]struct{})
+ u.PermissionsMap = make(map[string]struct{})
+ for _, p := range u.UserRolePerms {
+ u.PermissionsMap[p] = struct{}{}
+ }
+
+ if u.ListRoleID != nil {
+ // Unmarshall the raw list perms map.
+ var listPerms []models.ListPermission
+ if u.ListsPermsRaw != nil {
+ if err := json.Unmarshal(*u.ListsPermsRaw, &listPerms); err != nil {
+ c.log.Printf("error unmarshalling list permissions for role %d: %v", u.ID, err)
+ }
+ }
+
+ u.ListRole = &models.ListRolePermissions{ID: *u.ListRoleID, Name: u.ListRoleName.String, Lists: listPerms}
+
+ for _, p := range listPerms {
+ u.ListPermissionsMap[p.ID] = make(map[string]struct{})
+
+ for _, perm := range p.Permissions {
+ u.ListPermissionsMap[p.ID][perm] = struct{}{}
+
+ // List IDs with get / manage permissions.
+ if perm == "list:get" {
+ u.GetListIDs = append(u.GetListIDs, p.ID)
+ }
+ if perm == "list:manage" {
+ u.ManageListIDs = append(u.ManageListIDs, p.ID)
+ }
+ }
+ }
+ }
+
+ out[n] = u
+ }
+
+ return out, nil
+}
diff --git a/internal/migrations/v4.0.0.go b/internal/migrations/v4.0.0.go
new file mode 100644
index 000000000..800e692a2
--- /dev/null
+++ b/internal/migrations/v4.0.0.go
@@ -0,0 +1,157 @@
+package migrations
+
+import (
+ "encoding/json"
+ "fmt"
+ "log"
+ "os"
+
+ "github.com/jmoiron/sqlx"
+ "github.com/knadh/koanf/v2"
+ "github.com/knadh/listmonk/internal/utils"
+ "github.com/knadh/stuffbin"
+ "github.com/lib/pq"
+)
+
+// V4_0_0 performs the DB migrations.
+func V4_0_0(db *sqlx.DB, fs stuffbin.FileSystem, ko *koanf.Koanf, lo *log.Logger) error {
+ lo.Println("IMPORTANT: this upgrade might take a while if you have a large database. Please be patient ...")
+
+ if _, err := db.Exec(`CREATE INDEX IF NOT EXISTS idx_subs_id_status ON subscribers(id, status);`); err != nil {
+ return err
+ }
+
+ if _, err := db.Exec(`
+ CREATE EXTENSION IF NOT EXISTS pgcrypto;
+
+ DO $$
+ BEGIN
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_type') THEN
+ CREATE TYPE user_type AS ENUM ('user', 'api');
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'user_status') THEN
+ CREATE TYPE user_status AS ENUM ('enabled', 'disabled');
+ END IF;
+
+ IF NOT EXISTS (SELECT 1 FROM pg_type WHERE typname = 'role_type') THEN
+ CREATE TYPE role_type AS ENUM ('user', 'list');
+ END IF;
+ END$$;
+
+ CREATE TABLE IF NOT EXISTS roles (
+ id SERIAL PRIMARY KEY,
+ type role_type NOT NULL DEFAULT 'user',
+ parent_id INTEGER NULL REFERENCES roles(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ list_id INTEGER NULL REFERENCES lists(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ permissions TEXT[] NOT NULL DEFAULT '{}',
+ name TEXT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+ );
+ CREATE UNIQUE INDEX IF NOT EXISTS roles_idx ON roles (parent_id, list_id);
+ CREATE UNIQUE INDEX IF NOT EXISTS roles_name_idx ON roles (type, name) WHERE name IS NOT NULL;
+
+ CREATE TABLE IF NOT EXISTS users (
+ id SERIAL PRIMARY KEY,
+ username TEXT NOT NULL UNIQUE,
+ password_login BOOLEAN NOT NULL DEFAULT false,
+ password TEXT NULL,
+ email TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
+ avatar TEXT NULL,
+ type user_type NOT NULL DEFAULT 'user',
+ user_role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
+ list_role_id INTEGER NULL REFERENCES roles(id) ON DELETE CASCADE,
+ status user_status NOT NULL DEFAULT 'disabled',
+ loggedin_at TIMESTAMP WITH TIME ZONE NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+ );
+
+ CREATE TABLE IF NOT EXISTS sessions (
+ id TEXT NOT NULL PRIMARY KEY,
+ data JSONB DEFAULT '{}'::jsonb NOT NULL,
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL
+ );
+ CREATE INDEX IF NOT EXISTS idx_sessions ON sessions (id, created_at);
+ `); err != nil {
+ return err
+ }
+
+ // Insert new preference settings.
+ if _, err := db.Exec(`
+ INSERT INTO settings (key, value) VALUES('security.oidc', '{"enabled": false, "provider_url": "", "client_id": "", "client_secret": ""}') ON CONFLICT DO NOTHING;
+ `); err != nil {
+ return err
+ }
+
+ // Insert superuser role.
+ pmRaw, err := fs.Read("/permissions.json")
+ if err != nil {
+ lo.Fatalf("error reading permissions file: %v", err)
+ }
+ permGroups := []struct {
+ Group string `json:"group"`
+ Permissions []string `json:"permissions"`
+ }{}
+ if err := json.Unmarshal(pmRaw, &permGroups); err != nil {
+ lo.Fatalf("error loading permissions file: %v", err)
+ }
+
+ perms := []string{}
+ for _, group := range permGroups {
+ for _, p := range group.Permissions {
+ perms = append(perms, p)
+ }
+ }
+ if _, err := db.Exec(`INSERT INTO roles (type, name, permissions) VALUES('user', 'Super Admin', $1) ON CONFLICT DO NOTHING`, pq.Array(perms)); err != nil {
+ return err
+ }
+
+ // Create super admin.
+ var (
+ user = os.Getenv("LISTMONK_ADMIN_USER")
+ password = os.Getenv("LISTMONK_ADMIN_PASSWORD")
+ typ = "env"
+ )
+
+ if user != "" {
+ // If the env vars are set, use those values
+ if len(user) < 2 || len(password) < 8 {
+ lo.Fatal("LISTMONK_ADMIN_USER should be min 3 chars and LISTMONK_ADMIN_PASSWORD should be min 8 chars")
+ }
+ } else if ko.Exists("app.admin_username") {
+ // Legacy admin/password are set in the config or env var. Use those.
+ user = ko.String("app.admin_username")
+ password = ko.String("app.admin_password")
+
+ if len(user) < 2 || len(password) < 8 {
+ lo.Fatal("admin_username should be min 3 chars and admin_password should be min 8 chars")
+ }
+ typ = "legacy config"
+ } else {
+ // None are set. Auto-generate.
+ user = "admin"
+ if p, err := utils.GenerateRandomString(12); err != nil {
+ lo.Fatal("error generating admin password")
+ } else {
+ password = p
+ }
+ typ = "auto-generated"
+ }
+
+ lo.Printf("creating admin user '%s'. Credential source is '%s'", user, typ)
+
+ if _, err := db.Exec(`
+ INSERT INTO users (username, password_login, password, email, name, type, user_role_id, status) VALUES($1, true, CRYPT($2, GEN_SALT('bf')), $3, $4, 'user', 1, 'enabled') ON CONFLICT DO NOTHING;
+ `, user, password, user+"@listmonk", user); err != nil {
+ return err
+ }
+
+ if typ == "auto-generated" {
+ fmt.Printf("\n\033[31mIMPORTANT! CHANGE PASSWORD AFTER LOGGING IN\033[0m\nusername: \033[32m%s\033[0m and password: \033[32m%s\033[0m\n\n", user, password)
+ }
+
+ return nil
+}
diff --git a/internal/utils/utils.go b/internal/utils/utils.go
new file mode 100644
index 000000000..60c1f9ee9
--- /dev/null
+++ b/internal/utils/utils.go
@@ -0,0 +1,53 @@
+package utils
+
+import (
+ "crypto/rand"
+ "net/mail"
+ "net/url"
+ "path"
+ "strings"
+)
+
+// ValidateEmail validates whether the given string is a correctly formed e-mail address.
+func ValidateEmail(email string) bool {
+ // Since `mail.ParseAddress` parses an email address which can also contain an optional name component,
+ // here we check if incoming email string is same as the parsed email.Address. So this eliminates
+ // any valid email address with name and also valid address with empty name like ``.
+ em, err := mail.ParseAddress(email)
+ if err != nil || em.Address != email {
+ return false
+ }
+
+ return true
+}
+
+// GenerateRandomString generates a cryptographically random, alphanumeric string of length n.
+func GenerateRandomString(n int) (string, error) {
+ const dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
+ var bytes = make([]byte, n)
+
+ if _, err := rand.Read(bytes); err != nil {
+ return "", err
+ }
+ for k, v := range bytes {
+ bytes[k] = dictionary[v%byte(len(dictionary))]
+ }
+
+ return string(bytes), nil
+}
+
+// SanitizeURI takes a URL or URI, removes the domain from it, returns only the URI.
+// This is used for cleaning "next" redirect URLs/URIs to prevent open redirects.
+func SanitizeURI(u string) string {
+ u = strings.TrimSpace(u)
+ if u == "" {
+ return "/"
+ }
+
+ p, err := url.Parse(u)
+ if err != nil || strings.Contains(p.Path, "..") {
+ return "/"
+ }
+
+ return path.Clean(p.Path)
+}
diff --git a/models/models.go b/models/models.go
index 745de24de..51d18e3f0 100644
--- a/models/models.go
+++ b/models/models.go
@@ -56,11 +56,15 @@ const (
ListOptinDouble = "double"
// User.
- UserTypeSuperadmin = "superadmin"
UserTypeUser = "user"
+ UserTypeAPI = "api"
UserStatusEnabled = "enabled"
UserStatusDisabled = "disabled"
+ // Role.
+ RoleTypeUser = "user"
+ RoleTypeList = "list"
+
// BaseTpl is the name of the base template.
BaseTpl = "base"
@@ -148,11 +152,82 @@ type Base struct {
type User struct {
Base
- Email string `json:"email"`
- Name string `json:"name"`
- Password string `json:"-"`
- Type string `json:"type"`
- Status string `json:"status"`
+ Username string `db:"username" json:"username"`
+
+ // For API users, this is the plaintext API token.
+ Password null.String `db:"password" json:"password,omitempty"`
+ PasswordLogin bool `db:"password_login" json:"password_login"`
+ Email null.String `db:"email" json:"email"`
+ Name string `db:"name" json:"name"`
+ Type string `db:"type" json:"type"`
+ Status string `db:"status" json:"status"`
+ Avatar null.String `db:"avatar" json:"avatar"`
+ LoggedInAt null.Time `db:"loggedin_at" json:"loggedin_at"`
+
+ // Role struct {
+ // ID int `db:"-" json:"id"`
+ // Name string `db:"-" json:"name"`
+ // Permissions []string `db:"-" json:"permissions"`
+ // Lists []ListPermission `db:"-" json:"lists"`
+ // } `db:"-" json:"role"`
+
+ // Filled post-retrieval.
+ UserRole struct {
+ ID int `db:"-" json:"id"`
+ Name string `db:"-" json:"name"`
+ Permissions []string `db:"-" json:"permissions"`
+ } `db:"-" json:"user_role"`
+
+ ListRole *ListRolePermissions `db:"-" json:"list_role"`
+
+ UserRoleID int `db:"user_role_id" json:"user_role_id,omitempty"`
+ UserRoleName string `db:"user_role_name" json:"-"`
+ ListRoleID *int `db:"list_role_id" json:"list_role_id,omitempty"`
+ ListRoleName null.String `db:"list_role_name" json:"-"`
+ UserRolePerms pq.StringArray `db:"user_role_permissions" json:"-"`
+ ListsPermsRaw *json.RawMessage `db:"list_role_perms" json:"-"`
+
+ PermissionsMap map[string]struct{} `db:"-" json:"-"`
+ ListPermissionsMap map[int]map[string]struct{} `db:"-" json:"-"`
+ GetListIDs []int `db:"-" json:"-"`
+ ManageListIDs []int `db:"-" json:"-"`
+ HasPassword bool `db:"-" json:"-"`
+}
+
+type ListPermission struct {
+ ID int `json:"id"`
+ Name string `json:"name"`
+ Permissions pq.StringArray `json:"permissions"`
+}
+
+type ListRolePermissions struct {
+ ID int `db:"-" json:"id"`
+ Name string `db:"-" json:"name"`
+ Lists []ListPermission `db:"-" json:"lists"`
+}
+
+type Role struct {
+ Base
+
+ Type string `db:"type" json:"type"`
+ Name null.String `db:"name" json:"name"`
+ Permissions pq.StringArray `db:"permissions" json:"permissions"`
+
+ ListID null.Int `db:"list_id" json:"-"`
+ ParentID null.Int `db:"parent_id" json:"-"`
+ ListsRaw json.RawMessage `db:"list_permissions" json:"-"`
+ Lists []ListPermission `db:"-" json:"lists"`
+}
+
+type ListRole struct {
+ Base
+
+ Name null.String `db:"name" json:"name"`
+
+ ListID null.Int `db:"list_id" json:"-"`
+ ParentID null.Int `db:"parent_id" json:"-"`
+ ListsRaw json.RawMessage `db:"list_permissions" json:"-"`
+ Lists []ListPermission `db:"-" json:"lists"`
}
// Subscriber represents an e-mail subscriber.
@@ -266,6 +341,7 @@ type Campaign struct {
// List of media (attachment) IDs obtained from the next-campaign query
// while sending a campaign.
MediaIDs pq.Int64Array `json:"-" db:"media_id"`
+
// Fetched bodies of the attachments.
Attachments []Attachment `json:"-" db:"-"`
@@ -738,3 +814,45 @@ func (h Headers) Value() (driver.Value, error) {
return "[]", nil
}
+
+func (u *User) HasPerm(perm string) bool {
+ _, ok := u.PermissionsMap[perm]
+ return ok
+}
+
+// FilterListsByPerm returns list IDs filtered by either of the given perms.
+func (u *User) FilterListsByPerm(listIDs []int, get, manage bool) []int {
+ // If the user has full list management permission,
+ // no further checks are required.
+ if get {
+ if _, ok := u.PermissionsMap[PermListGetAll]; ok {
+ return listIDs
+ }
+ }
+ if manage {
+ if _, ok := u.PermissionsMap[PermListManageAll]; ok {
+ return listIDs
+ }
+ }
+
+ out := make([]int, 0, len(listIDs))
+
+ // Go through every list ID.
+ for _, id := range listIDs {
+ // Check if it exists in the map.
+ if l, ok := u.ListPermissionsMap[id]; ok {
+ // Check if any of the given permission exists for it.
+ if get {
+ if _, ok := l[PermListGet]; ok {
+ out = append(out, id)
+ }
+ } else if manage {
+ if _, ok := l[PermListManage]; ok {
+ out = append(out, id)
+ }
+ }
+ }
+ }
+
+ return out
+}
diff --git a/models/permissions.go b/models/permissions.go
new file mode 100644
index 000000000..8184e1434
--- /dev/null
+++ b/models/permissions.go
@@ -0,0 +1,31 @@
+package models
+
+const (
+ PermListGetAll = "lists:get_all"
+ PermListManageAll = "lists:manage_all"
+ PermListManage = "list:manage"
+ PermListGet = "list:get"
+ PermSubscribersGet = "subscribers:get"
+ PermSubscribersGetAll = "subscribers:get_all"
+ PermSubscribersManage = "subscribers:manage"
+ PermSubscribersImport = "subscribers:import"
+ PermSubscribersSqlQuery = "subscribers:sql_query"
+ PermTxSend = "tx:send"
+ PermCampaignsGet = "campaigns:get"
+ PermCampaignsGetAnalytics = "campaigns:get_analytics"
+ PermCampaignsManage = "campaigns:manage"
+ PermBouncesGet = "bounces:get"
+ PermBouncesManage = "bounces:manage"
+ PermWebhooksPostBounce = "webhooks:post_bounce"
+ PermMediaGet = "media:get"
+ PermMediaManage = "media:manage"
+ PermTemplatesGet = "templates:get"
+ PermTemplatesManage = "templates:manage"
+ PermUsersGet = "users:get"
+ PermUsersManage = "users:manage"
+ PermRolesGet = "roles:get"
+ PermRolesManage = "roles:manage"
+ PermSettingsGet = "settings:get"
+ PermSettingsManage = "settings:manage"
+ PermSettingsMaintain = "settings:maintain"
+)
diff --git a/models/queries.go b/models/queries.go
index 9e978e4dd..0bce69741 100644
--- a/models/queries.go
+++ b/models/queries.go
@@ -18,6 +18,7 @@ type Queries struct {
UpsertSubscriber *sqlx.Stmt `query:"upsert-subscriber"`
UpsertBlocklistSubscriber *sqlx.Stmt `query:"upsert-blocklist-subscriber"`
GetSubscriber *sqlx.Stmt `query:"get-subscriber"`
+ HasSubscriberLists *sqlx.Stmt `query:"has-subscriber-list"`
GetSubscribersByEmails *sqlx.Stmt `query:"get-subscribers-by-emails"`
GetSubscriberLists *sqlx.Stmt `query:"get-subscriber-lists"`
GetSubscriptions *sqlx.Stmt `query:"get-subscriptions"`
@@ -75,6 +76,7 @@ type Queries struct {
DeleteCampaignLinkClicks *sqlx.Stmt `query:"delete-campaign-link-clicks"`
NextCampaigns *sqlx.Stmt `query:"next-campaigns"`
+ GetRunningCampaign *sqlx.Stmt `query:"get-running-campaign"`
NextCampaignSubscribers *sqlx.Stmt `query:"next-campaign-subscribers"`
GetOneCampaignSubscriber *sqlx.Stmt `query:"get-one-campaign-subscriber"`
UpdateCampaign *sqlx.Stmt `query:"update-campaign"`
@@ -107,6 +109,23 @@ type Queries struct {
DeleteBounces *sqlx.Stmt `query:"delete-bounces"`
DeleteBouncesBySubscriber *sqlx.Stmt `query:"delete-bounces-by-subscriber"`
GetDBInfo string `query:"get-db-info"`
+
+ CreateUser *sqlx.Stmt `query:"create-user"`
+ UpdateUser *sqlx.Stmt `query:"update-user"`
+ UpdateUserProfile *sqlx.Stmt `query:"update-user-profile"`
+ UpdateUserLogin *sqlx.Stmt `query:"update-user-login"`
+ DeleteUsers *sqlx.Stmt `query:"delete-users"`
+ GetUsers *sqlx.Stmt `query:"get-users"`
+ GetAPITokens *sqlx.Stmt `query:"get-api-tokens"`
+ LoginUser *sqlx.Stmt `query:"login-user"`
+
+ CreateRole *sqlx.Stmt `query:"create-role"`
+ GetUserRoles *sqlx.Stmt `query:"get-user-roles"`
+ GetListRoles *sqlx.Stmt `query:"get-list-roles"`
+ UpdateRole *sqlx.Stmt `query:"update-role"`
+ DeleteRole *sqlx.Stmt `query:"delete-role"`
+ UpsertListPermissions *sqlx.Stmt `query:"upsert-list-permissions"`
+ DeleteListPermission *sqlx.Stmt `query:"delete-list-permission"`
}
// CompileSubscriberQueryTpl takes an arbitrary WHERE expressions
diff --git a/models/settings.go b/models/settings.go
index 577ea48ee..ef129aab3 100644
--- a/models/settings.go
+++ b/models/settings.go
@@ -40,6 +40,13 @@ type Settings struct {
SecurityCaptchaKey string `json:"security.captcha_key"`
SecurityCaptchaSecret string `json:"security.captcha_secret"`
+ OIDC struct {
+ Enabled bool `json:"enabled"`
+ ProviderURL string `json:"provider_url"`
+ ClientID string `json:"client_id"`
+ ClientSecret string `json:"client_secret"`
+ } `json:"security.oidc"`
+
UploadProvider string `json:"upload.provider"`
UploadExtensions []string `json:"upload.extensions"`
UploadFilesystemUploadPath string `json:"upload.filesystem.upload_path"`
diff --git a/permissions.json b/permissions.json
new file mode 100644
index 000000000..2f86958c6
--- /dev/null
+++ b/permissions.json
@@ -0,0 +1,75 @@
+[
+ {
+ "group": "lists",
+ "permissions":
+ [
+ "lists:get_all",
+ "lists:manage_all"
+ ]
+ },
+ {
+ "group": "subscribers",
+ "permissions":
+ [
+ "subscribers:get",
+ "subscribers:get_all",
+ "subscribers:manage",
+ "subscribers:import",
+ "subscribers:sql_query",
+ "tx:send"
+ ]
+ },
+ {
+ "group": "campaigns",
+ "permissions":
+ [
+ "campaigns:get",
+ "campaigns:get_analytics",
+ "campaigns:manage"
+ ]
+ },
+ {
+ "group": "bounces",
+ "permissions":
+ [
+ "bounces:get",
+ "bounces:manage",
+ "webhooks:post_bounce"
+ ]
+ },
+ {
+ "group": "media",
+ "permissions":
+ [
+ "media:get",
+ "media:manage"
+ ]
+ },
+ {
+ "group": "templates",
+ "permissions":
+ [
+ "templates:get",
+ "templates:manage"
+ ]
+ },
+ {
+ "group": "users",
+ "permissions":
+ [
+ "users:get",
+ "users:manage",
+ "roles:get",
+ "roles:manage"
+ ]
+ },
+ {
+ "group": "settings",
+ "permissions":
+ [
+ "settings:get",
+ "settings:manage",
+ "settings:maintain"
+ ]
+ }
+]
diff --git a/queries.sql b/queries.sql
index 7489afea3..a0d91bb32 100644
--- a/queries.sql
+++ b/queries.sql
@@ -1,4 +1,3 @@
-
-- subscribers
-- name: get-subscriber
-- Get a single subscriber by id or UUID or email.
@@ -9,6 +8,16 @@ SELECT * FROM subscribers WHERE
WHEN $3 != '' THEN email = $3
END;
+-- name: has-subscriber-list
+-- Used for checking access permission by list.
+SELECT s.id AS subscriber_id,
+ CASE
+ WHEN EXISTS (SELECT 1 FROM subscriber_lists sl WHERE sl.subscriber_id = s.id AND sl.list_id = ANY($2))
+ THEN TRUE
+ ELSE FALSE
+ END AS has
+FROM subscribers s WHERE s.id = ANY($1);
+
-- name: get-subscribers-by-emails
-- Get subscribers by emails.
SELECT * FROM subscribers WHERE email=ANY($1);
@@ -113,13 +122,15 @@ WITH sub AS (
name=(CASE WHEN $7 THEN $3 ELSE s.name END),
attribs=(CASE WHEN $7 THEN $4 ELSE s.attribs END),
updated_at=NOW()
- RETURNING uuid, id
+ RETURNING uuid, id, status
),
subs AS (
INSERT INTO subscriber_lists (subscriber_id, list_id, status)
- VALUES((SELECT id FROM sub), UNNEST($5::INT[]), $6)
+ SELECT sub.id, listID, CASE WHEN sub.status = 'blocklisted' THEN 'unsubscribed' ELSE $6::subscription_status END
+ FROM sub, UNNEST($5::INT[]) AS listID
ON CONFLICT (subscriber_id, list_id) DO UPDATE
- SET updated_at=NOW(), status=(CASE WHEN $7 THEN $6 ELSE subscriber_lists.status END)
+ SET updated_at = NOW(),
+ status = CASE WHEN $7 THEN EXCLUDED.status ELSE subscriber_lists.status END
)
SELECT uuid, id from sub;
@@ -413,6 +424,10 @@ UPDATE subscriber_lists SET status='unsubscribed', updated_at=NOW()
-- lists
-- name: get-lists
SELECT * FROM lists WHERE (CASE WHEN $1 = '' THEN 1=1 ELSE type=$1::list_type END)
+ AND CASE
+ -- Optional list IDs based on user permission.
+ WHEN $3 = TRUE THEN TRUE ELSE id = ANY($4::INT[])
+ END
ORDER BY CASE WHEN $2 = 'id' THEN id END, CASE WHEN $2 = 'name' THEN name END;
-- name: query-lists
@@ -427,7 +442,11 @@ WITH ls AS (
AND ($4 = '' OR type = $4::list_type)
AND ($5 = '' OR optin = $5::list_optin)
AND (CARDINALITY($6::VARCHAR(100)[]) = 0 OR $6 <@ tags)
- OFFSET $7 LIMIT (CASE WHEN $8 < 1 THEN NULL ELSE $8 END)
+ AND CASE
+ -- Optional list IDs based on user permission.
+ WHEN $7 = TRUE THEN TRUE ELSE id = ANY($8::INT[])
+ END
+ OFFSET $9 LIMIT (CASE WHEN $10 < 1 THEN NULL ELSE $10 END)
),
statuses AS (
SELECT
@@ -469,31 +488,23 @@ DELETE FROM lists WHERE id = ALL($1);
-- campaigns
-- name: create-campaign
-- This creates the campaign and inserts campaign_lists relationships.
-WITH campLists AS (
- -- Get the list_ids and their optin statuses for the campaigns found in the previous step.
- SELECT lists.id AS list_id, campaign_id, optin FROM lists
- INNER JOIN campaign_lists ON (campaign_lists.list_id = lists.id)
- WHERE lists.id = ANY($14::INT[])
-),
-tpl AS (
+WITH tpl AS (
-- If there's no template_id given, use the default template.
SELECT (CASE WHEN $13 = 0 THEN id ELSE $13 END) AS id FROM templates WHERE is_default IS TRUE
),
counts AS (
- SELECT COALESCE(COUNT(id), 0) as to_send, COALESCE(MAX(id), 0) as max_sub_id
- FROM subscribers
- LEFT JOIN campLists ON (campLists.campaign_id = ANY($14::INT[]))
- LEFT JOIN subscriber_lists ON (
- subscriber_lists.status != 'unsubscribed' AND
- subscribers.id = subscriber_lists.subscriber_id AND
- subscriber_lists.list_id = campLists.list_id AND
-
- -- For double opt-in lists, consider only 'confirmed' subscriptions. For single opt-ins,
- -- any status except for 'unsubscribed' (already excluded above) works.
- (CASE WHEN campLists.optin = 'double' THEN subscriber_lists.status = 'confirmed' ELSE true END)
- )
- WHERE subscriber_lists.list_id=ANY($14::INT[])
- AND subscribers.status='enabled'
+ -- This is going to be slow on large databases.
+ SELECT
+ COALESCE(COUNT(DISTINCT sl.subscriber_id), 0) AS to_send, COALESCE(MAX(s.id), 0) AS max_sub_id
+ FROM subscriber_lists sl
+ JOIN lists l ON sl.list_id = l.id
+ JOIN subscribers s ON sl.subscriber_id = s.id
+ WHERE sl.list_id = ANY($14::INT[])
+ AND s.status != 'blocklisted'
+ AND (
+ (l.optin = 'double' AND sl.status = 'confirmed') OR
+ (l.optin != 'double' AND sl.status != 'unsubscribed')
+ )
),
camp AS (
INSERT INTO campaigns (uuid, type, name, subject, from_email, body, altbody, content_type, send_at, headers, tags, messenger, template_id, to_send, max_subscriber_id, archive, archive_slug, archive_template_id, archive_meta)
@@ -653,27 +664,18 @@ campMedia AS (
GROUP BY campaign_id
),
counts AS (
- -- For each campaign above, get the total number of subscribers and the max_subscriber_id
- -- across all its lists.
- SELECT id AS campaign_id,
- COUNT(DISTINCT(subscriber_lists.subscriber_id)) AS to_send,
- COALESCE(MAX(subscriber_lists.subscriber_id), 0) AS max_subscriber_id
+ SELECT camps.id AS campaign_id, COUNT(DISTINCT sl.subscriber_id) AS to_send, COALESCE(MAX(sl.subscriber_id), 0) AS max_subscriber_id
FROM camps
- LEFT JOIN campLists ON (campLists.campaign_id = camps.id)
- LEFT JOIN subscriber_lists ON (
- subscriber_lists.list_id = campLists.list_id AND
- (CASE
- -- For optin campaigns, only e-mail 'unconfirmed' subscribers belonging to 'double' optin lists.
- WHEN camps.type = 'optin' THEN subscriber_lists.status = 'unconfirmed' AND campLists.optin = 'double'
-
- -- For regular campaigns with double optin lists, only e-mail 'confirmed' subscribers.
- WHEN campLists.optin = 'double' THEN subscriber_lists.status = 'confirmed'
-
- -- For regular campaigns with non-double optin lists, e-mail everyone
- -- except unsubscribed subscribers.
- ELSE subscriber_lists.status != 'unsubscribed'
- END)
- )
+ JOIN campLists cl ON cl.campaign_id = camps.id
+ JOIN subscriber_lists sl ON sl.list_id = cl.list_id
+ AND (
+ CASE
+ WHEN camps.type = 'optin' THEN sl.status = 'unconfirmed' AND cl.optin = 'double'
+ WHEN cl.optin = 'double' THEN sl.status = 'confirmed'
+ ELSE sl.status != 'unsubscribed'
+ END
+ )
+ JOIN subscribers s ON (s.id = sl.subscriber_id AND s.status != 'blocklisted')
GROUP BY camps.id
),
updateCounts AS (
@@ -738,48 +740,63 @@ SELECT COUNT(%s) AS "count", url
WHERE campaign_id=ANY($1) AND link_clicks.created_at >= $2 AND link_clicks.created_at <= $3
GROUP BY links.url ORDER BY "count" DESC LIMIT 50;
+-- name: get-running-campaign
+-- Returns the metadata for a running campaign that is required by next-campaign-subscribers to retrieve
+-- a batch of campaign subscribers for processing.
+SELECT campaigns.id AS campaign_id, campaigns.type as campaign_type, last_subscriber_id, max_subscriber_id, lists.id AS list_id
+ FROM campaigns
+ LEFT JOIN campaign_lists ON (campaign_lists.campaign_id = campaigns.id)
+ LEFT JOIN lists ON (lists.id = campaign_lists.list_id)
+ WHERE campaigns.id = $1 AND status='running';
+
-- name: next-campaign-subscribers
-- Returns a batch of subscribers in a given campaign starting from the last checkpoint
-- (last_subscriber_id). Every fetch updates the checkpoint and the sent count, which means
-- every fetch returns a new batch of subscribers until all rows are exhausted.
-WITH camps AS (
- SELECT last_subscriber_id, max_subscriber_id, type FROM campaigns WHERE id = $1 AND status='running'
-),
-campLists AS (
+--
+-- In previous versions, get-running-campaign + this was a single query spread across multiple
+-- CTEs, but despite numerous permutations and combinations, Postgres query planner simply would not use
+-- the right indexes on subscriber_lists when the JOIN or ids were referenced dynamically from campLists
+-- (be it a CTE or various kinds of joins). However, statically providing the list IDs to JOIN on ($5::INT[])
+-- the query planner works as expected. The difference is staggering. ~15 seconds on a subscribers table with 15m
+-- rows and a subscriber_lists table with 70 million rows when fetching subscribers for a campaign with a single list,
+-- vs. a few million seconds using this current approach.
+WITH campLists AS (
SELECT lists.id AS list_id, optin FROM lists
- LEFT JOIN campaign_lists ON (campaign_lists.list_id = lists.id)
+ LEFT JOIN campaign_lists ON campaign_lists.list_id = lists.id
WHERE campaign_lists.campaign_id = $1
),
-subIDs AS (
- SELECT DISTINCT ON (subscriber_lists.subscriber_id) subscriber_id, list_id, status FROM subscriber_lists
- WHERE
- -- ARRAY_AGG is 20x faster instead of a simple SELECT because the query planner
- -- understands the CTE's cardinality after the scalar array conversion. Huh.
- list_id = ANY((SELECT ARRAY_AGG(list_id) FROM campLists)::INT[]) AND
- status != 'unsubscribed' AND
- subscriber_id > (SELECT last_subscriber_id FROM camps) AND
- subscriber_id <= (SELECT max_subscriber_id FROM camps)
- ORDER BY subscriber_id LIMIT $2
-),
subs AS (
- SELECT subscribers.* FROM subIDs
- LEFT JOIN campLists ON (campLists.list_id = subIDs.list_id)
- INNER JOIN subscribers ON (
- subscribers.status != 'blocklisted' AND
- subscribers.id = subIDs.subscriber_id AND
-
- (CASE
- -- For optin campaigns, only e-mail 'unconfirmed' subscribers.
- WHEN (SELECT type FROM camps) = 'optin' THEN subIDs.status = 'unconfirmed' AND campLists.optin = 'double'
-
- -- For regular campaigns with double optin lists, only e-mail 'confirmed' subscribers.
- WHEN campLists.optin = 'double' THEN subIDs.status = 'confirmed'
-
- -- For regular campaigns with non-double optin lists, e-mail everyone
- -- except unsubscribed subscribers.
- ELSE subIDs.status != 'unsubscribed'
- END)
- )
+ SELECT s.*
+ FROM (
+ SELECT DISTINCT s.id
+ FROM subscriber_lists sl
+ JOIN campLists ON sl.list_id = campLists.list_id
+ JOIN subscribers s ON s.id = sl.subscriber_id
+ WHERE
+ sl.list_id = ANY($5::INT[])
+ -- last_subscriber_id
+ AND s.id > $3
+ -- max_subscriber_id
+ AND s.id <= $4
+ -- Subscriber should not be blacklisted.
+ AND s.status != 'blocklisted'
+ AND (
+ -- If it's an optin campaign and the list is double-optin, only pick unconfirmed subscribers.
+ ($2 = 'optin' AND sl.status = 'unconfirmed' AND campLists.optin = 'double')
+ OR (
+ -- It is a regular campaign.
+ $2 != 'optin' AND (
+ -- It is a double optin list. Only pick confirmed subscribers.
+ (campLists.optin = 'double' AND sl.status = 'confirmed') OR
+
+ -- It is a single optin list. Pick all non-unsubscribed subscribers.
+ (campLists.optin != 'double' AND sl.status != 'unsubscribed')
+ )
+ )
+ )
+ ORDER BY s.id LIMIT $6
+ ) subIDs JOIN subscribers s ON (s.id = subIDs.id) ORDER BY s.id
),
u AS (
UPDATE campaigns
@@ -873,28 +890,6 @@ WITH view AS (
INSERT INTO campaign_views (campaign_id, subscriber_id)
VALUES((SELECT campaign_id FROM view), (SELECT subscriber_id FROM view));
--- users
--- name: get-users
-SELECT * FROM users WHERE $1 = 0 OR id = $1 OFFSET $2 LIMIT $3;
-
--- name: create-user
-INSERT INTO users (email, name, password, type, status) VALUES($1, $2, $3, $4, $5) RETURNING id;
-
--- name: update-user
-UPDATE users SET
- email=(CASE WHEN $2 != '' THEN $2 ELSE email END),
- name=(CASE WHEN $3 != '' THEN $3 ELSE name END),
- password=(CASE WHEN $4 != '' THEN $4 ELSE password END),
- type=(CASE WHEN $5 != '' THEN $5::user_type ELSE type END),
- status=(CASE WHEN $6 != '' THEN $6::user_status ELSE status END),
- updated_at=NOW()
-WHERE id = $1;
-
--- name: delete-user
--- Delete a user, except for the primordial super admin.
-DELETE FROM users WHERE $1 != 1 AND id=$1;
-
-
-- templates
-- name: get-templates
-- Only if the second param ($2) is true, body is returned.
@@ -1048,3 +1043,171 @@ DELETE FROM bounces WHERE subscriber_id = (SELECT id FROM sub);
-- name: get-db-info
SELECT JSON_BUILD_OBJECT('version', (SELECT VERSION()),
'size_mb', (SELECT ROUND(pg_database_size((SELECT CURRENT_DATABASE()))/(1024^2)))) AS info;
+
+-- name: create-user
+INSERT INTO users (username, password_login, password, email, name, type, user_role_id, list_role_id, status)
+ VALUES($1, $2, (
+ CASE
+ -- For user types with password_login enabled, bcrypt and store the hash of the password.
+ WHEN $6::user_type != 'api' AND $2 AND $3 != ''
+ THEN CRYPT($3, GEN_SALT('bf'))
+ WHEN $6 = 'api'
+ -- For APIs, store the password (token) as-is.
+ THEN $3
+ ELSE NULL
+ END
+ ), $4, $5, $6, (SELECT id FROM roles WHERE id = $7 AND type = 'user'), (SELECT id FROM roles WHERE id = $8 AND type = 'list'), $9) RETURNING id;
+
+-- name: update-user
+WITH u AS (
+ -- Edit is only allowed if there are more than 1 active super users or
+ -- if the only superadmin user's status/role isn't being changed.
+ SELECT
+ CASE
+ WHEN (SELECT COUNT(*) FROM users WHERE id != $1 AND status = 'enabled' AND type = 'user' AND user_role_id = 1) = 0 AND ($8 != 1 OR $10 != 'enabled')
+ THEN FALSE
+ ELSE TRUE
+ END AS canEdit
+)
+UPDATE users SET
+ username=(CASE WHEN $2 != '' THEN $2 ELSE username END),
+ password_login=$3,
+ password=(CASE WHEN $3 = TRUE THEN (CASE WHEN $4 != '' THEN CRYPT($4, GEN_SALT('bf')) ELSE password END) ELSE NULL END),
+ email=(CASE WHEN $5 != '' THEN $5 ELSE email END),
+ name=(CASE WHEN $6 != '' THEN $6 ELSE name END),
+ type=(CASE WHEN $7 != '' THEN $7::user_type ELSE type END),
+ user_role_id=(CASE WHEN $8 != 0 THEN (SELECT id FROM roles WHERE id = $8 AND type = 'user') ELSE user_role_id END),
+ list_role_id=(
+ CASE
+ WHEN $9 < 0 THEN NULL
+ WHEN $9 > 0 THEN (SELECT id FROM roles WHERE id = $9 AND type = 'list')
+ ELSE list_role_id END
+ ),
+ status=(CASE WHEN $10 != '' THEN $10::user_status ELSE status END),
+ updated_at=NOW()
+ WHERE id=$1 AND (SELECT canEdit FROM u) = TRUE;
+
+-- name: delete-users
+WITH u AS (
+ SELECT COUNT(*) AS num FROM users WHERE NOT(id = ANY($1)) AND user_role_id=1 AND status='enabled'
+)
+DELETE FROM users WHERE id = ALL($1) AND (SELECT num FROM u) > 0;
+
+-- name: get-users
+WITH ur AS (
+ SELECT id, name, permissions FROM roles WHERE type = 'user' AND parent_id IS NULL
+),
+lr AS (
+ SELECT r.id, r.name, r.permissions, r.list_id, l.name AS list_name
+ FROM roles r
+ LEFT JOIN lists l ON r.list_id = l.id
+ WHERE r.type = 'list' AND r.parent_id IS NULL
+),
+lp AS (
+ SELECT lr.id AS list_role_id,
+ JSONB_AGG(
+ JSONB_BUILD_OBJECT(
+ 'id', COALESCE(cr.list_id, lr.list_id),
+ 'name', COALESCE(cl.name, lr.list_name),
+ 'permissions', COALESCE(cr.permissions, lr.permissions)
+ )
+ ) AS list_role_perms
+ FROM lr
+ LEFT JOIN roles cr ON cr.parent_id = lr.id AND cr.type = 'list'
+ LEFT JOIN lists cl ON cr.list_id = cl.id
+ GROUP BY lr.id
+)
+SELECT
+ users.*,
+ ur.id AS user_role_id,
+ ur.name AS user_role_name,
+ ur.permissions AS user_role_permissions,
+ lp.list_role_id,
+ lr.name AS list_role_name,
+ lp.list_role_perms
+FROM users
+ LEFT JOIN ur ON users.user_role_id = ur.id
+ LEFT JOIN lp ON users.list_role_id = lp.list_role_id
+ LEFT JOIN lr ON lp.list_role_id = lr.id
+ WHERE
+ (
+ CASE
+ WHEN $1::INT != 0 THEN users.id = $1
+ WHEN $2::TEXT != '' THEN username = $2
+ WHEN $3::TEXT != '' THEN email = $3
+ ELSE TRUE
+ END
+ )
+ ORDER BY users.created_at;
+
+
+-- name: get-api-tokens
+SELECT username, password FROM users WHERE status='enabled' AND type='api';
+
+-- name: login-user
+WITH u AS (
+ SELECT users.*, r.name as role_name, r.permissions FROM users
+ LEFT JOIN roles r ON (r.id = users.user_role_id)
+ WHERE username=$1 AND status != 'disabled' AND password_login = TRUE
+)
+SELECT * FROM u WHERE CRYPT($2, password) = password;
+
+-- name: update-user-profile
+UPDATE users SET name=$2, email=(CASE WHEN password_login THEN $3 ELSE email END),
+ password=(CASE WHEN $4 = TRUE THEN (CASE WHEN $5 != '' THEN CRYPT($5, GEN_SALT('bf')) ELSE password END) ELSE NULL END)
+ WHERE id=$1;
+
+-- name: update-user-login
+UPDATE users SET loggedin_at=NOW(), avatar=(CASE WHEN $2 != '' THEN $2 ELSE avatar END) WHERE id=$1;
+
+-- name: get-user-roles
+WITH mainroles AS (
+ SELECT ur.* FROM roles ur WHERE type = 'user' AND ur.parent_id IS NULL
+),
+listPerms AS (
+ SELECT ur.parent_id, JSONB_AGG(JSONB_BUILD_OBJECT('id', ur.list_id, 'name', lists.name, 'permissions', ur.permissions)) AS listPerms
+ FROM roles ur
+ LEFT JOIN lists ON(lists.id = ur.list_id)
+ WHERE ur.parent_id IS NOT NULL GROUP BY ur.parent_id
+)
+SELECT p.*, COALESCE(l.listPerms, '[]'::JSONB) AS "list_permissions" FROM mainroles p
+ LEFT JOIN listPerms l ON p.id = l.parent_id ORDER BY p.created_at;
+
+-- name: get-list-roles
+WITH mainroles AS (
+ SELECT ur.* FROM roles ur WHERE type = 'list' AND ur.parent_id IS NULL
+),
+listPerms AS (
+ SELECT ur.parent_id, JSONB_AGG(JSONB_BUILD_OBJECT('id', ur.list_id, 'name', lists.name, 'permissions', ur.permissions)) AS listPerms
+ FROM roles ur
+ LEFT JOIN lists ON(lists.id = ur.list_id)
+ WHERE ur.parent_id IS NOT NULL GROUP BY ur.parent_id
+)
+SELECT p.*, COALESCE(l.listPerms, '[]'::JSONB) AS "list_permissions" FROM mainroles p
+ LEFT JOIN listPerms l ON p.id = l.parent_id ORDER BY p.created_at;
+
+
+-- name: create-role
+INSERT INTO roles (name, type, permissions, created_at, updated_at) VALUES($1, $2, $3, NOW(), NOW()) RETURNING *;
+
+-- name: upsert-list-permissions
+WITH d AS (
+ -- Delete lists that aren't included.
+ DELETE FROM roles WHERE parent_id = $1 AND list_id != ALL($2::INT[])
+),
+p AS (
+ -- Get (list_id, perms[]), (list_id, perms[])
+ SELECT UNNEST($2) AS list_id, JSONB_ARRAY_ELEMENTS(TO_JSONB($3::TEXT[][])) AS perms
+)
+INSERT INTO roles (parent_id, list_id, permissions, type)
+ SELECT $1, list_id, ARRAY_REMOVE(ARRAY(SELECT JSONB_ARRAY_ELEMENTS_TEXT(perms)), ''), 'list' FROM p
+ ON CONFLICT (parent_id, list_id) DO UPDATE SET permissions = EXCLUDED.permissions;
+
+-- name: delete-list-permission
+DELETE FROM roles WHERE parent_id=$1 AND list_id=$2;
+
+-- name: update-role
+UPDATE roles SET name=$2, permissions=$3 WHERE id=$1 and parent_id IS NULL RETURNING *;
+
+-- name: delete-role
+DELETE FROM roles WHERE id=$1;
diff --git a/schema.sql b/schema.sql
index 12276e4dd..1f354e413 100644
--- a/schema.sql
+++ b/schema.sql
@@ -7,6 +7,11 @@ DROP TYPE IF EXISTS campaign_type CASCADE; CREATE TYPE campaign_type AS ENUM ('r
DROP TYPE IF EXISTS content_type CASCADE; CREATE TYPE content_type AS ENUM ('richtext', 'html', 'plain', 'markdown');
DROP TYPE IF EXISTS bounce_type CASCADE; CREATE TYPE bounce_type AS ENUM ('soft', 'hard', 'complaint');
DROP TYPE IF EXISTS template_type CASCADE; CREATE TYPE template_type AS ENUM ('campaign', 'tx');
+DROP TYPE IF EXISTS user_type CASCADE; CREATE TYPE user_type AS ENUM ('user', 'api');
+DROP TYPE IF EXISTS user_status CASCADE; CREATE TYPE user_status AS ENUM ('enabled', 'disabled');
+DROP TYPE IF EXISTS role_type CASCADE; CREATE TYPE role_type AS ENUM ('user', 'list');
+
+CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- subscribers
DROP TABLE IF EXISTS subscribers CASCADE;
@@ -23,6 +28,7 @@ CREATE TABLE subscribers (
);
DROP INDEX IF EXISTS idx_subs_email; CREATE UNIQUE INDEX idx_subs_email ON subscribers(LOWER(email));
DROP INDEX IF EXISTS idx_subs_status; CREATE INDEX idx_subs_status ON subscribers(status);
+DROP INDEX IF EXISTS idx_subs_id_status; CREATE INDEX idx_subs_id_status ON subscribers(id, status);
DROP INDEX IF EXISTS idx_subs_created_at; CREATE INDEX idx_subs_created_at ON subscribers(created_at);
DROP INDEX IF EXISTS idx_subs_updated_at; CREATE INDEX idx_subs_updated_at ON subscribers(updated_at);
@@ -246,6 +252,7 @@ INSERT INTO settings (key, value) VALUES
('security.enable_captcha', 'false'),
('security.captcha_key', '""'),
('security.captcha_secret', '""'),
+ ('security.oidc', '{"enabled": false, "provider_url": "", "client_id": "", "client_secret": ""}'),
('upload.provider', '"filesystem"'),
('upload.max_file_size', '5000'),
('upload.extensions', '["jpg","jpeg","png","gif","svg","*"]'),
@@ -295,7 +302,48 @@ DROP INDEX IF EXISTS idx_bounces_camp_id; CREATE INDEX idx_bounces_camp_id ON bo
DROP INDEX IF EXISTS idx_bounces_source; CREATE INDEX idx_bounces_source ON bounces(source);
DROP INDEX IF EXISTS idx_bounces_date; CREATE INDEX idx_bounces_date ON bounces((TIMEZONE('UTC', created_at)::DATE));
+-- roles
+DROP TABLE IF EXISTS roles CASCADE;
+CREATE TABLE roles (
+ id SERIAL PRIMARY KEY,
+ type role_type NOT NULL DEFAULT 'user',
+ parent_id INTEGER NULL REFERENCES roles(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ list_id INTEGER NULL REFERENCES lists(id) ON DELETE CASCADE ON UPDATE CASCADE,
+ permissions TEXT[] NOT NULL DEFAULT '{}',
+ name TEXT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+CREATE UNIQUE INDEX roles_idx ON roles (parent_id, list_id);
+CREATE UNIQUE INDEX roles_name_idx ON roles (type, name) WHERE name IS NOT NULL;
+
+-- users
+DROP TABLE IF EXISTS users CASCADE;
+CREATE TABLE users (
+ id SERIAL PRIMARY KEY,
+ username TEXT NOT NULL UNIQUE,
+ password_login BOOLEAN NOT NULL DEFAULT false,
+ password TEXT NULL,
+ email TEXT NOT NULL UNIQUE,
+ name TEXT NOT NULL,
+ avatar TEXT NULL,
+ type user_type NOT NULL DEFAULT 'user',
+ user_role_id INTEGER NOT NULL REFERENCES roles(id) ON DELETE RESTRICT,
+ list_role_id INTEGER NULL REFERENCES roles(id) ON DELETE CASCADE,
+ status user_status NOT NULL DEFAULT 'disabled',
+ loggedin_at TIMESTAMP WITH TIME ZONE NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+-- user sessions
+DROP TABLE IF EXISTS sessions CASCADE;
+CREATE TABLE sessions (
+ id TEXT NOT NULL PRIMARY KEY,
+ data JSONB DEFAULT '{}'::jsonb NOT NULL,
+ created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT now() NOT NULL
+);
+DROP INDEX IF EXISTS idx_sessions; CREATE INDEX idx_sessions ON sessions (id, created_at);
-- materialized views
diff --git a/static/public/static/auth/auth0.com.png b/static/public/static/auth/auth0.com.png
new file mode 100644
index 000000000..3b2d8562d
Binary files /dev/null and b/static/public/static/auth/auth0.com.png differ
diff --git a/static/public/static/auth/github.com.png b/static/public/static/auth/github.com.png
new file mode 100644
index 000000000..1fa19c55d
Binary files /dev/null and b/static/public/static/auth/github.com.png differ
diff --git a/static/public/static/auth/google.com.png b/static/public/static/auth/google.com.png
new file mode 100644
index 000000000..389c1cd54
Binary files /dev/null and b/static/public/static/auth/google.com.png differ
diff --git a/static/public/static/auth/microsoftonline.com.png b/static/public/static/auth/microsoftonline.com.png
new file mode 100644
index 000000000..168a9c813
Binary files /dev/null and b/static/public/static/auth/microsoftonline.com.png differ
diff --git a/static/public/static/auth/oidc.png b/static/public/static/auth/oidc.png
new file mode 100644
index 000000000..7b53d129f
Binary files /dev/null and b/static/public/static/auth/oidc.png differ
diff --git a/static/public/static/style.css b/static/public/static/style.css
index 905b79d04..a47267c3c 100644
--- a/static/public/static/style.css
+++ b/static/public/static/style.css
@@ -34,7 +34,7 @@ h4 {
margin-bottom: 45px;
}
-input[type="text"], input[type="email"], select {
+input[type="text"], input[type="email"], input[type="password"], select {
padding: 10px 15px;
border: 1px solid #888;
border-radius: 3px;
@@ -61,6 +61,9 @@ input[disabled] {
.right {
text-align: right;
}
+.error {
+ color: #FF5722;
+}
.button {
background: #0055d4;
padding: 15px 30px;
@@ -84,7 +87,8 @@ input[disabled] {
color: #0055d4;
}
.button.button-outline:hover {
- background-color: #0055d4;
+ border-color: #333;
+ background-color: #333;
color: #fff;
}
@@ -178,6 +182,21 @@ input[disabled] {
font-weight: bold;
}
+.login .submit {
+ margin-top: 20px;
+}
+ .login button {
+ width: 100%;
+ vertical-align: middle;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ }
+ .login button img {
+ max-width: 24px;
+ margin-right: 10px;
+ }
+
#btn-back {
display: none;
}
diff --git a/static/public/templates/login.html b/static/public/templates/login.html
new file mode 100644
index 000000000..a96a988cc
--- /dev/null
+++ b/static/public/templates/login.html
@@ -0,0 +1,43 @@
+{{ define "admin-login" }}
+{{ template "header" .}}
+
+
+ {{ .L.T "users.login"}}
+ {{ if .Data.PasswordEnabled }}
+
+ {{ end }}
+
+ {{ if .Data.OIDCProvider }}
+
+ {{ end }}
+
+
+
+{{ template "footer" .}}
+{{ end }}
\ No newline at end of file