web/handlers: revamp error localization

fixes #66
This commit is contained in:
Henry 2021-03-30 12:48:42 +02:00
parent f9652c6423
commit 81bd94344e
11 changed files with 280 additions and 136 deletions

2
go.mod
View File

@ -33,7 +33,7 @@ require (
go.cryptoscope.co/muxrpc/v2 v2.0.0-beta.1.0.20210308090127-5f1f5f9cbb59
go.cryptoscope.co/netwrap v0.1.1
go.cryptoscope.co/secretstream v1.2.2
go.mindeco.de v1.9.0
go.mindeco.de v1.10.0
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9

2
go.sum
View File

@ -497,6 +497,8 @@ go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg=
go.mindeco.de v1.9.0 h1:/xli02DkzpIUZxp/rp1nj8z/OZ9MHvkMIr9TfDVcmBg=
go.mindeco.de v1.9.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de v1.10.0 h1:H/bhL+dIgZZnUgBEDlKUJBisTszNiHDONeGZtGdiJJ0=
go.mindeco.de v1.10.0/go.mod h1:ePOcyktbpqzhMPRBDv2gUaDd3h8QtT+DUU1DK+VbQZE=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870 h1:TCI3AefMAaOYECvppn30+CfEB0Fn8IES1SKvvacc3/c=
go.mindeco.de/ssb-refs v0.1.1-0.20210108133850-cf1f44fea870/go.mod h1:OnBnV02ux4lLsZ39LID6yYLqSDp+dqTHb/3miYPkQFs=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=

View File

@ -8,9 +8,12 @@ import (
"fmt"
)
type ErrNotFound struct {
What string
}
var (
ErrNotAuthorized = errors.New("rooms/web: not authorized")
ErrDenied = errors.New("rooms: this key has been banned")
)
type ErrNotFound struct{ What string }
func (nf ErrNotFound) Error() string {
return fmt.Sprintf("rooms/web: item not found: %s", nf.What)
@ -25,14 +28,20 @@ func (br ErrBadRequest) Error() string {
return fmt.Sprintf("rooms/web: bad request error: %s", br.Details)
}
type ErrForbidden struct {
Details error
}
type ErrForbidden struct{ Details error }
func (f ErrForbidden) Error() string {
return fmt.Sprintf("rooms/web: access denied: %s", f.Details)
}
var ErrNotAuthorized = errors.New("rooms/web: not authorized")
type PageNotFound struct{ Path string }
var ErrDenied = errors.New("rooms: this key has been banned")
func (e PageNotFound) Error() string {
return fmt.Sprintf("rooms/web: page not found: %s", e.Path)
}
type DatabaseError struct{ Reason error }
func (e DatabaseError) Error() string {
return fmt.Sprintf("rooms/web: database failed to complete query: %s", e.Reason.Error())
}

107
web/errors/errhandler.go Normal file
View File

@ -0,0 +1,107 @@
package errors
import (
"errors"
"html/template"
"net/http"
"github.com/go-kit/kit/log/level"
"go.mindeco.de/http/auth"
"go.mindeco.de/http/render"
"go.mindeco.de/logging"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
)
type ErrorHandler struct {
locHelper *i18n.Helper
render *render.Renderer
}
func NewErrorHandler(locHelper *i18n.Helper) *ErrorHandler {
return &ErrorHandler{
locHelper: locHelper,
}
}
// SetRenderer needs to update the rendere later since we need to pass ErrorHandler into render.New (ie. befor we get the pointer for r)
func (eh *ErrorHandler) SetRenderer(r *render.Renderer) {
eh.render = r
}
func (eh *ErrorHandler) Handle(rw http.ResponseWriter, req *http.Request, code int, err error) {
var ih = i18n.LocalizerFromRequest(eh.locHelper, req)
// default, unlocalized message
msg := err.Error()
// localize some specific error messages
var (
aa roomdb.ErrAlreadyAdded
pnf PageNotFound
br ErrBadRequest
f ErrForbidden
)
switch {
case err == ErrNotAuthorized:
code = http.StatusForbidden
msg = ih.LocalizeSimple("ErrorAuthBadLogin")
case err == auth.ErrBadLogin:
msg = ih.LocalizeSimple("ErrorAuthBadLogin")
case errors.Is(err, roomdb.ErrNotFound):
code = http.StatusNotFound
msg = ih.LocalizeSimple("ErrorNotFound")
case errors.As(err, &aa):
msg = ih.LocalizeWithData("ErrorAlreadyAdded", map[string]string{
"Feed": aa.Ref.Ref(),
})
case errors.As(err, &pnf):
code = http.StatusNotFound
msg = ih.LocalizeWithData("ErrorPageNotFound", map[string]string{
"Path": pnf.Path,
})
case errors.As(err, &br):
code = http.StatusBadRequest
// TODO: we could localize all the "Where:" as labels, too
// buttt it feels like overkill right now
msg = ih.LocalizeWithData("ErrorBadRequest", map[string]string{
"Where": br.Where,
"Details": br.Details.Error(),
})
case errors.As(err, &f):
code = http.StatusForbidden
msg = ih.LocalizeWithData("ErrorForbidden", map[string]string{
"Details": f.Details.Error(),
})
}
data := errorTemplateData{
Err: template.HTML(msg),
// TODO: localize status codes? might be fine with a few
Status: http.StatusText(code),
StatusCode: code,
}
renderErr := eh.render.Render(rw, req, "error.tmpl", code, data)
if renderErr != nil {
logger := logging.FromContext(req.Context())
level.Error(logger).Log("event", "error template renderfailed",
"orig-err", err,
"render-err", renderErr,
)
}
}
type errorTemplateData struct {
StatusCode int
Status string
Err template.HTML
}

View File

@ -69,11 +69,13 @@ func (h aliasesHandler) revoke(rw http.ResponseWriter, req *http.Request) {
err = h.db.Revoke(req.Context(), req.FormValue("name"))
if err != nil {
if !errors.Is(err, roomdb.ErrNotFound) {
// TODO: flash error
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
status = http.StatusNotFound
h.r.Error(rw, req, http.StatusInternalServerError, err)
return
}
http.Redirect(rw, req, redirectToAliases, status)

View File

@ -8,12 +8,12 @@ import (
"net/http"
"strconv"
"go.mindeco.de/http/render"
refs "go.mindeco.de/ssb-refs"
"github.com/gorilla/csrf"
"go.mindeco.de/http/render"
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
refs "go.mindeco.de/ssb-refs"
)
type deniedKeysHandler struct {
@ -26,21 +26,21 @@ const redirectToDeniedKeys = "/admin/denied"
func (h deniedKeysHandler) add(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
if err := req.ParseForm(); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
newEntry := req.Form.Get("pub_key")
newEntryParsed, err := refs.ParseFeedRef(newEntry)
if err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
err = weberrors.ErrBadRequest{Where: "Public Key", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
@ -53,11 +53,7 @@ func (h deniedKeysHandler) add(w http.ResponseWriter, req *http.Request) {
var aa roomdb.ErrAlreadyAdded
if errors.As(err, &aa) {
code = http.StatusBadRequest
// TODO: localized error pages
// h.r.Error(w, req, http.StatusBadRequest, weberrors.Localize())
// return
}
h.r.Error(w, req, code, err)
return
}
@ -98,6 +94,7 @@ func (h deniedKeysHandler) removeConfirm(rw http.ResponseWriter, req *http.Reque
entry, err := h.db.GetByID(req.Context(), id)
if err != nil {
if errors.Is(err, roomdb.ErrNotFound) {
// TODO "flash" errors
http.Redirect(rw, req, redirectToDeniedKeys, http.StatusFound)
return nil, ErrRedirected
}

View File

@ -47,17 +47,15 @@ func (h invitesHandler) overview(rw http.ResponseWriter, req *http.Request) (int
func (h invitesHandler) create(w http.ResponseWriter, req *http.Request) (interface{}, error) {
if req.Method != "POST" {
// TODO: proper error type
return nil, fmt.Errorf("bad request")
return nil, weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
}
if err := req.ParseForm(); err != nil {
// TODO: proper error type
return nil, fmt.Errorf("bad request: %w", err)
return nil, weberrors.ErrBadRequest{Where: "Form data", Details: err}
}
member := members.FromContext(req.Context())
if member == nil {
return nil, fmt.Errorf("warning: no user session for elevated access request")
return nil, weberrors.ErrNotAuthorized
}
pm, err := h.config.GetPrivacyMode(req.Context())
if err != nil {

View File

@ -29,22 +29,22 @@ const redirectToMembers = "/admin/members"
func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
if err := req.ParseForm(); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
newEntry := req.Form.Get("pub_key")
newEntryParsed, err := refs.ParseFeedRef(newEntry)
if err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad public key: %w", err))
err = weberrors.ErrBadRequest{Where: "Public Key", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
@ -54,11 +54,7 @@ func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {
var aa roomdb.ErrAlreadyAdded
if errors.As(err, &aa) {
code = http.StatusBadRequest
// TODO: localized error pages
// h.r.Error(w, req, http.StatusBadRequest, weberrors.Localize())
// return
}
h.r.Error(w, req, code, err)
return
}
@ -68,41 +64,42 @@ func (h membersHandler) add(w http.ResponseWriter, req *http.Request) {
func (h membersHandler) changeRole(w http.ResponseWriter, req *http.Request) {
if req.Method != "POST" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
if err := req.ParseForm(); err != nil {
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
currentMember := members.FromContext(req.Context())
if currentMember == nil || currentMember.Role != roomdb.RoleAdmin {
// TODO: proper error type
h.r.Error(w, req, http.StatusForbidden, fmt.Errorf("not an admin"))
err := weberrors.ErrForbidden{Details: fmt.Errorf("not an admin")}
h.r.Error(w, req, http.StatusForbidden, err)
return
}
memberID, err := strconv.ParseInt(req.URL.Query().Get("id"), 10, 64)
if err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad member id: %w", err))
return
}
if err := req.ParseForm(); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
err = weberrors.ErrBadRequest{Where: "id", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
var role roomdb.Role
if err := role.UnmarshalText([]byte(req.Form.Get("role"))); err != nil {
// TODO: proper error type
err = weberrors.ErrBadRequest{Where: "role", Details: err}
h.r.Error(w, req, http.StatusBadRequest, err)
return
}
if err := h.db.SetRole(req.Context(), memberID, role); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusInternalServerError, fmt.Errorf("failed to change member role: %w", err))
err = weberrors.DatabaseError{Reason: err}
// TODO: not found error
h.r.Error(w, req, http.StatusInternalServerError, err)
return
}

View File

@ -3,7 +3,6 @@
package handlers
import (
"errors"
"fmt"
"html/template"
"net/http"
@ -25,6 +24,7 @@ import (
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
"github.com/ssb-ngi-pointer/go-ssb-room/web"
weberrs "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
"github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/admin"
roomsAuth "github.com/ssb-ngi-pointer/go-ssb-room/web/handlers/auth"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
@ -78,16 +78,23 @@ func New(
return nil, err
}
eh := weberrs.NewErrorHandler(locHelper)
allTheTemplates := concatTemplates(
HTMLTemplates,
roomsAuth.HTMLTemplates,
admin.HTMLTemplates,
)
allTheTemplates = append(allTheTemplates, "error.tmpl")
r, err := render.New(web.Templates,
render.SetLogger(logger),
render.BaseTemplates("base.tmpl", "menu.tmpl"),
render.AddTemplates(concatTemplates(
HTMLTemplates,
roomsAuth.HTMLTemplates,
admin.HTMLTemplates,
)...),
render.ErrorTemplate("error.tmpl"),
render.AddTemplates(allTheTemplates...),
// render.ErrorTemplate(),
render.SetErrorHandler(eh.Handle),
render.FuncMap(web.TemplateFuncs(m)),
// TODO: move these to the i18n helper pkg
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
loc := i18n.LocalizerFromRequest(locHelper, r)
@ -97,6 +104,7 @@ func New(
loc := i18n.LocalizerFromRequest(locHelper, r)
return loc.LocalizeSimple
}),
render.InjectTemplateFunc("current_page_is", func(r *http.Request) interface{} {
return func(routeName string) bool {
route := m.Get(routeName)
@ -110,6 +118,7 @@ func New(
return r.RequestURI == url.Path
}
}),
render.InjectTemplateFunc("urlToNotice", func(r *http.Request) interface{} {
return func(name string) *url.URL {
noticeName := roomdb.PinnedNoticeName(name)
@ -135,11 +144,13 @@ func New(
return u
}
}),
render.InjectTemplateFunc("is_logged_in", members.TemplateHelper()),
)
if err != nil {
return nil, fmt.Errorf("web Handler: failed to create renderer: %w", err)
}
eh.SetRenderer(r)
cookieCodec, err := web.LoadOrCreateCookieSecrets(repo)
if err != nil {
@ -154,52 +165,14 @@ func New(
},
}
// TODO: this is just the error handler for http/auth, not render
authErrH := func(rw http.ResponseWriter, req *http.Request, err error, code int) {
var ih = i18n.LocalizerFromRequest(locHelper, req)
// default, unlocalized message
msg := err.Error()
// localize some specific error messages
var (
aa roomdb.ErrAlreadyAdded
)
switch {
case err == auth.ErrBadLogin:
msg = ih.LocalizeSimple("AuthErrorBadLogin")
case errors.Is(err, roomdb.ErrNotFound):
msg = ih.LocalizeSimple("ErrorNotFound")
case errors.As(err, &aa):
msg = ih.LocalizeSimple("ErrorAlreadyAdded")
}
r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
return errorTemplateData{
Err: msg,
// TODO: localize?
Status: http.StatusText(code),
StatusCode: code,
}, nil
}).ServeHTTP(rw, req)
}
notAuthorizedH := r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
statusCode := http.StatusUnauthorized
rw.WriteHeader(statusCode)
return errorTemplateData{
statusCode,
"Unauthorized",
"you are not authorized to access the requested site",
}, nil
})
authWithPassword, err := auth.NewHandler(dbs.AuthFallback,
auth.SetStore(cookieStore),
auth.SetErrorHandler(authErrH),
auth.SetNotAuthorizedHandler(notAuthorizedH),
auth.SetErrorHandler(func(rw http.ResponseWriter, req *http.Request, err error, code int) {
eh.Handle(rw, req, code, err)
}),
auth.SetNotAuthorizedHandler(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
eh.Handle(rw, req, http.StatusForbidden, weberrs.ErrNotAuthorized)
})),
auth.SetLifetime(2*time.Hour), // TODO: configure
)
if err != nil {
@ -237,6 +210,7 @@ func New(
bridge,
)
// auth routes
m.Get(router.AuthLogin).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
if label := req.URL.Query().Get("ssb-http-auth"); label != "" {
authWithSSB.DecideMethod(w, req)
@ -246,13 +220,11 @@ func New(
})
m.Get(router.AuthFallbackFinalize).HandlerFunc(authWithPassword.Authorize)
m.Get(router.AuthFallbackLogin).Handler(r.HTML("auth/fallback_sign_in.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
return map[string]interface{}{
csrf.TemplateTag: csrf.TemplateField(req),
}, nil
}))
m.Get(router.AuthLogout).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
err = authWithSSB.Logout(w, req)
if err != nil {
@ -261,6 +233,7 @@ func New(
authWithPassword.Logout(w, req)
})
// all the admin routes
adminHandler := admin.Handler(
netInfo.Domain,
r,
@ -277,7 +250,10 @@ func New(
)
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
// landing page
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
// TODO: try websocket upgrade (issue #)
notice, err := dbs.PinnedNotices.Get(req.Context(), roomdb.NoticeDescription, "en-GB")
if err != nil {
return nil, fmt.Errorf("failed to find description: %w", err)
@ -292,6 +268,7 @@ func New(
}))
m.Get(router.CompleteAbout).Handler(r.StaticHTML("landing/about.tmpl"))
// notices (the mini-CMS)
var nh = noticeHandler{
notices: dbs.Notices,
pinned: dbs.PinnedNotices,
@ -299,6 +276,7 @@ func New(
m.Get(router.CompleteNoticeList).Handler(r.HTML("notice/list.tmpl", nh.list))
m.Get(router.CompleteNoticeShow).Handler(r.HTML("notice/show.tmpl", nh.show))
// public aliases
var ah = aliasHandler{
r: r,
@ -309,6 +287,7 @@ func New(
}
m.Get(router.CompleteAliasResolve).HandlerFunc(ah.resolve)
//public invites
var ih = inviteHandler{
render: r,
@ -324,14 +303,14 @@ func New(
m.Get(router.CompleteInviteInsertID).Handler(r.HTML("invite/insert-id.tmpl", ih.presentInsert))
m.Get(router.CompleteInviteConsume).HandlerFunc(ih.consume)
// statuc assets
m.PathPrefix("/assets/").Handler(http.StripPrefix("/assets/", http.FileServer(web.Assets)))
m.NotFoundHandler = r.HTML("error.tmpl", func(rw http.ResponseWriter, req *http.Request) (interface{}, error) {
rw.WriteHeader(http.StatusNotFound)
msg := i18n.LocalizerFromRequest(locHelper, req).LocalizeSimple("PageNotFound")
return errorTemplateData{http.StatusNotFound, "Not Found", msg}, nil
m.NotFoundHandler = http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
eh.Handle(rw, req, http.StatusNotFound, weberrs.PageNotFound{Path: req.URL.Path})
})
// hook up main stdlib mux to the gorrilla/mux with named routes
mainMux.Handle("/", m)
urlTo := web.NewURLTo(m)
@ -370,13 +349,6 @@ func New(
}
// utils
type errorTemplateData struct {
StatusCode int
Status string
Err string
}
func concatTemplates(lst ...[]string) []string {
var catted []string

View File

@ -1,3 +1,6 @@
# default localiztion file for english
# generic terms
GenericConfirm = "Yes"
GenericGoBack = "Back"
GenericSave = "Save"
@ -11,32 +14,77 @@ PageNotFound = "The requested page was not found."
PubKeyRefPlaceholder = "@ .ed25519"
# roles
RoleMember = "Member"
RoleModerator = "Moderator"
RoleAdmin = "Admin"
# navigation labels (should be single words or as short as possible)
NavAdminLanding = "Home"
NavAdminDashboard = "Dashboard"
NavAdminInvites = "Invites"
NavAdminNotices = "Notices"
# Error messages
ErrorAuthBadLogin = "The supplied authentication credentials seem wrong."
ErrorNotFound = "The database couldn't find the item in question."
ErrorAlreadyAdded = "The public key <strong>{{.Key}}</strong> already is on the list"
ErrorPageNotFound = "The requested page <strong>({{.Path}})</strong> is not there."
ErrorNotAuthorized = "You are not authorized to access this page."
ErrorForbidden = "The request could not be executed because of lacking privileges ({{.Details}})"
ErrorBadRequest = "There was a problem with your Request: {{.Where}} ({{.Details}}"
# TODO: might be obsolete with notices
LandingTitle = "ohai my room"
LandingWelcome = "Landing welcome here"
AuthSignIn = "Sign in"
AuthSignOut = "Sign out"
# authentication
################
AuthTitle = "Member Authentication"
AuthWelcome = "If you are a member of this room, you can access the internal dashboard. Click on your preferred sign-in method below:"
AuthSignIn = "Sign in"
AuthSignOut = "Sign out"
# auth with ssb
AuthWithSSBTitle = "Sign in with SSB"
AuthWithSSBInstruct = "Easy and secure method, if your SSB app supports it."
AuthWithSSBWelcome = "To sign-in with your SSB identity stored on this device, press the button below which will open a compatible SSB app, if it's installed."
AuthWithSSBInstructQR = "If your SSB app is on another device, you can scan the following QR code to sign-in with that device's SSB identity."
AuthWithSSBError = "Sign-in failed. Please make sure you use an SSB app that supports this method of login, and click the button above within a minute after this page was opened."
# auth with password
AuthFallbackTitle = "Password sign-in"
AuthFallbackWelcome = "Signing in with username and password is only possible if the administrator has given you one, because we do not support user registration."
AuthFallbackInstruct = "This method is an acceptable fallback, if you have a username and password."
# general dashboard stuff
#########################
AdminDashboardTitle = "Dashboard"
AdminDashboardWelcome = "Welcome to your dashboard"
# privacy modes
###############
ModeOpen = "Open"
ModeCommunity = "Community"
ModeRestricted = "Restricted"
SetPrivacyModeTitle = "Set Privacy Mode"
PrivacyModesTitle = "Privacy Modes"
RoomsSpecification = "rooms 2 specification"
ExplanationPrivacyModes = "The privacy mode of this room determines who can create invites and who can connect to the room. For more information, see the"
ExplanationOpen = "Open invite codes, anyone may connect"
ExplanationCommunity = "Members can create invites, anyone may connect"
ExplanationRestricted = "Only admins/mods can create invites, only members may connect"
Settings = "Settings"
# banned dashboard
##################
AdminDeniedKeysTitle = "Banned"
AdminDeniedKeysWelcome = "This page can be used to ban SSB IDs so that they can't access the room any more."
AdminDeniedKeysAdd = "Add"
@ -46,6 +94,9 @@ AdminDeniedKeysCommentDescription = "The person who added this ban, added the fo
AdminDeniedKeysRemoveConfirmWelcome = "Are you sure you want to remove this ban? They will will be able to access the room again."
AdminDeniedKeysRemoveConfirmTitle = "Confirm member removal"
# members dashboard
###################
AdminMembersTitle = "Members"
AdminMembersWelcome = "Here you can see all the members of the room and ways to add new ones (by their SSB ID) or remove exising ones."
AdminMembersAdd = "Add"
@ -61,6 +112,9 @@ AdminMemberDetailsAliasRevoke = "Revoke"
AdminMemberDetailsExclusion = "Exclusion from this room"
AdminMemberDetailsRemove = "Remove member"
# invite dashboard
##################
AdminInvitesTitle = "Invites"
AdminInvitesWelcome = "Create invite tokens for people who are not yet members of this room. On this page you can also see previously created invites that are still not unclaimed by new members."
AdminInvitesCreate = "Create new invite"
@ -80,10 +134,8 @@ AdminInviteSuggestedAliasIsShort = "Alias:"
AdminInviteCreatedTitle = "Invite created successfully!"
AdminInviteCreatedInstruct = "Now, copy the link below and paste it to a friend who you want to invite to this room."
NavAdminLanding = "Home"
NavAdminDashboard = "Dashboard"
NavAdminInvites = "Invites"
NavAdminNotices = "Notices"
# public invites
################
InviteFacade = "Join Room"
InviteFacadeTitle = "Join Room"
@ -101,6 +153,9 @@ InviteInsertWelcome = "You can claim your invite by inserting your SSB ID below.
InviteConsumedTitle = "Invite accepted!"
InviteConsumedWelcome = "You are now a member of this room. If you need a multiserver address to connect to the room, you can copy-paste the one below:"
# notices (mini-CMS)
####################
NoticeEditTitle = "Edit Notice"
NoticeList = "Notices"
NoticeListWelcome = "Here you can manage the contents of the landing page and other important documents such as code of conduct and privacy policy."
@ -111,19 +166,12 @@ NoticeNews = "News"
NoticeDescription = "Description"
NoticePrivacyPolicy = "Privacy Policy"
ModeOpen = "Open"
ModeCommunity = "Community"
ModeRestricted = "Restricted"
SetPrivacyModeTitle = "Set Privacy Mode"
PrivacyModesTitle = "Privacy Modes"
RoomsSpecification = "rooms 2 specification"
ExplanationPrivacyModes = "The privacy mode of this room determines who can create invites and who can connect to the room. For more information, see the"
ExplanationOpen = "Open invite codes, anyone may connect"
ExplanationCommunity = "Members can create invites, anyone may connect"
ExplanationRestricted = "Only admins/mods can create invites, only members may connect"
Settings = "Settings"
# Plurals
#########
# These need to use this form and get {{.Count}}
# [Label]
# one = "singular"
# other = "{{.Count}} things"
[MemberCount]
description = "Number of members"

View File

@ -139,6 +139,18 @@ func (l Localizer) LocalizeSimple(messageID string) string {
panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err))
}
func (l Localizer) LocalizeWithData(messageID string, tplData map[string]string) string {
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
MessageID: messageID,
TemplateData: tplData,
})
if err == nil {
return msg
}
panic(fmt.Sprintf("i18n/error: failed to localize label %s: %s", messageID, err))
}
func (l Localizer) LocalizePlurals(messageID string, pluralCount int) string {
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
MessageID: messageID,