Merge pull request #143 from ssb-ngi-pointer/language-picking
Add language picker
This commit is contained in:
commit
2007c73295
File diff suppressed because it is too large
Load Diff
|
@ -18,7 +18,7 @@
|
|||
"ssb-keys": "^8.1.0",
|
||||
"ssb-replicate": "^1.3.2",
|
||||
"ssb-room": "^1.3.0",
|
||||
"ssb-room-client": "^0.4.0",
|
||||
"ssb-room-client": "^0.10.0",
|
||||
"tap-spec": "^5.0.0",
|
||||
"tape": "^5.2.2"
|
||||
}
|
||||
|
|
|
@ -20,6 +20,8 @@ import (
|
|||
type RoomConfig interface {
|
||||
GetPrivacyMode(context.Context) (PrivacyMode, error)
|
||||
SetPrivacyMode(context.Context, PrivacyMode) error
|
||||
GetDefaultLanguage(context.Context) (string, error)
|
||||
SetDefaultLanguage(context.Context, string) error
|
||||
}
|
||||
|
||||
// AuthFallbackService allows password authentication which might be helpful for scenarios
|
||||
|
|
|
@ -9,6 +9,19 @@ import (
|
|||
)
|
||||
|
||||
type FakeRoomConfig struct {
|
||||
GetDefaultLanguageStub func(context.Context) (string, error)
|
||||
getDefaultLanguageMutex sync.RWMutex
|
||||
getDefaultLanguageArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
}
|
||||
getDefaultLanguageReturns struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
getDefaultLanguageReturnsOnCall map[int]struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}
|
||||
GetPrivacyModeStub func(context.Context) (roomdb.PrivacyMode, error)
|
||||
getPrivacyModeMutex sync.RWMutex
|
||||
getPrivacyModeArgsForCall []struct {
|
||||
|
@ -22,6 +35,18 @@ type FakeRoomConfig struct {
|
|||
result1 roomdb.PrivacyMode
|
||||
result2 error
|
||||
}
|
||||
SetDefaultLanguageStub func(context.Context, string) error
|
||||
setDefaultLanguageMutex sync.RWMutex
|
||||
setDefaultLanguageArgsForCall []struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}
|
||||
setDefaultLanguageReturns struct {
|
||||
result1 error
|
||||
}
|
||||
setDefaultLanguageReturnsOnCall map[int]struct {
|
||||
result1 error
|
||||
}
|
||||
SetPrivacyModeStub func(context.Context, roomdb.PrivacyMode) error
|
||||
setPrivacyModeMutex sync.RWMutex
|
||||
setPrivacyModeArgsForCall []struct {
|
||||
|
@ -38,6 +63,70 @@ type FakeRoomConfig struct {
|
|||
invocationsMutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguage(arg1 context.Context) (string, error) {
|
||||
fake.getDefaultLanguageMutex.Lock()
|
||||
ret, specificReturn := fake.getDefaultLanguageReturnsOnCall[len(fake.getDefaultLanguageArgsForCall)]
|
||||
fake.getDefaultLanguageArgsForCall = append(fake.getDefaultLanguageArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
}{arg1})
|
||||
stub := fake.GetDefaultLanguageStub
|
||||
fakeReturns := fake.getDefaultLanguageReturns
|
||||
fake.recordInvocation("GetDefaultLanguage", []interface{}{arg1})
|
||||
fake.getDefaultLanguageMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1, ret.result2
|
||||
}
|
||||
return fakeReturns.result1, fakeReturns.result2
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageCallCount() int {
|
||||
fake.getDefaultLanguageMutex.RLock()
|
||||
defer fake.getDefaultLanguageMutex.RUnlock()
|
||||
return len(fake.getDefaultLanguageArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageCalls(stub func(context.Context) (string, error)) {
|
||||
fake.getDefaultLanguageMutex.Lock()
|
||||
defer fake.getDefaultLanguageMutex.Unlock()
|
||||
fake.GetDefaultLanguageStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageArgsForCall(i int) context.Context {
|
||||
fake.getDefaultLanguageMutex.RLock()
|
||||
defer fake.getDefaultLanguageMutex.RUnlock()
|
||||
argsForCall := fake.getDefaultLanguageArgsForCall[i]
|
||||
return argsForCall.arg1
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageReturns(result1 string, result2 error) {
|
||||
fake.getDefaultLanguageMutex.Lock()
|
||||
defer fake.getDefaultLanguageMutex.Unlock()
|
||||
fake.GetDefaultLanguageStub = nil
|
||||
fake.getDefaultLanguageReturns = struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetDefaultLanguageReturnsOnCall(i int, result1 string, result2 error) {
|
||||
fake.getDefaultLanguageMutex.Lock()
|
||||
defer fake.getDefaultLanguageMutex.Unlock()
|
||||
fake.GetDefaultLanguageStub = nil
|
||||
if fake.getDefaultLanguageReturnsOnCall == nil {
|
||||
fake.getDefaultLanguageReturnsOnCall = make(map[int]struct {
|
||||
result1 string
|
||||
result2 error
|
||||
})
|
||||
}
|
||||
fake.getDefaultLanguageReturnsOnCall[i] = struct {
|
||||
result1 string
|
||||
result2 error
|
||||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) GetPrivacyMode(arg1 context.Context) (roomdb.PrivacyMode, error) {
|
||||
fake.getPrivacyModeMutex.Lock()
|
||||
ret, specificReturn := fake.getPrivacyModeReturnsOnCall[len(fake.getPrivacyModeArgsForCall)]
|
||||
|
@ -102,6 +191,68 @@ func (fake *FakeRoomConfig) GetPrivacyModeReturnsOnCall(i int, result1 roomdb.Pr
|
|||
}{result1, result2}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguage(arg1 context.Context, arg2 string) error {
|
||||
fake.setDefaultLanguageMutex.Lock()
|
||||
ret, specificReturn := fake.setDefaultLanguageReturnsOnCall[len(fake.setDefaultLanguageArgsForCall)]
|
||||
fake.setDefaultLanguageArgsForCall = append(fake.setDefaultLanguageArgsForCall, struct {
|
||||
arg1 context.Context
|
||||
arg2 string
|
||||
}{arg1, arg2})
|
||||
stub := fake.SetDefaultLanguageStub
|
||||
fakeReturns := fake.setDefaultLanguageReturns
|
||||
fake.recordInvocation("SetDefaultLanguage", []interface{}{arg1, arg2})
|
||||
fake.setDefaultLanguageMutex.Unlock()
|
||||
if stub != nil {
|
||||
return stub(arg1, arg2)
|
||||
}
|
||||
if specificReturn {
|
||||
return ret.result1
|
||||
}
|
||||
return fakeReturns.result1
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageCallCount() int {
|
||||
fake.setDefaultLanguageMutex.RLock()
|
||||
defer fake.setDefaultLanguageMutex.RUnlock()
|
||||
return len(fake.setDefaultLanguageArgsForCall)
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageCalls(stub func(context.Context, string) error) {
|
||||
fake.setDefaultLanguageMutex.Lock()
|
||||
defer fake.setDefaultLanguageMutex.Unlock()
|
||||
fake.SetDefaultLanguageStub = stub
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageArgsForCall(i int) (context.Context, string) {
|
||||
fake.setDefaultLanguageMutex.RLock()
|
||||
defer fake.setDefaultLanguageMutex.RUnlock()
|
||||
argsForCall := fake.setDefaultLanguageArgsForCall[i]
|
||||
return argsForCall.arg1, argsForCall.arg2
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageReturns(result1 error) {
|
||||
fake.setDefaultLanguageMutex.Lock()
|
||||
defer fake.setDefaultLanguageMutex.Unlock()
|
||||
fake.SetDefaultLanguageStub = nil
|
||||
fake.setDefaultLanguageReturns = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetDefaultLanguageReturnsOnCall(i int, result1 error) {
|
||||
fake.setDefaultLanguageMutex.Lock()
|
||||
defer fake.setDefaultLanguageMutex.Unlock()
|
||||
fake.SetDefaultLanguageStub = nil
|
||||
if fake.setDefaultLanguageReturnsOnCall == nil {
|
||||
fake.setDefaultLanguageReturnsOnCall = make(map[int]struct {
|
||||
result1 error
|
||||
})
|
||||
}
|
||||
fake.setDefaultLanguageReturnsOnCall[i] = struct {
|
||||
result1 error
|
||||
}{result1}
|
||||
}
|
||||
|
||||
func (fake *FakeRoomConfig) SetPrivacyMode(arg1 context.Context, arg2 roomdb.PrivacyMode) error {
|
||||
fake.setPrivacyModeMutex.Lock()
|
||||
ret, specificReturn := fake.setPrivacyModeReturnsOnCall[len(fake.setPrivacyModeArgsForCall)]
|
||||
|
@ -167,8 +318,12 @@ func (fake *FakeRoomConfig) SetPrivacyModeReturnsOnCall(i int, result1 error) {
|
|||
func (fake *FakeRoomConfig) Invocations() map[string][][]interface{} {
|
||||
fake.invocationsMutex.RLock()
|
||||
defer fake.invocationsMutex.RUnlock()
|
||||
fake.getDefaultLanguageMutex.RLock()
|
||||
defer fake.getDefaultLanguageMutex.RUnlock()
|
||||
fake.getPrivacyModeMutex.RLock()
|
||||
defer fake.getPrivacyModeMutex.RUnlock()
|
||||
fake.setDefaultLanguageMutex.RLock()
|
||||
defer fake.setDefaultLanguageMutex.RUnlock()
|
||||
fake.setPrivacyModeMutex.RLock()
|
||||
defer fake.setPrivacyModeMutex.RUnlock()
|
||||
copiedInvocations := map[string][][]interface{}{}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
-- +migrate Up
|
||||
-- the configuration settings for this room, currently only privacy modes
|
||||
-- the configuration settings for this room, currently privacy mode settings and the default translation for the room
|
||||
CREATE TABLE config (
|
||||
id integer NOT NULL PRIMARY KEY,
|
||||
privacyMode integer NOT NULL, -- open, community, restricted
|
||||
defaultLanguage TEXT NOT NULL, -- a language tag, e.g. en, sv, de
|
||||
use_subdomain_for_aliases boolean NOT NULL, -- flag to toggle using subdomains (rather than alias routes) for aliases
|
||||
|
||||
CHECK (id == 0) -- should only ever store one row
|
||||
);
|
||||
|
@ -10,9 +12,11 @@ CREATE TABLE config (
|
|||
-- the config table will only ever contain one row: the rooms current settings
|
||||
-- we update that row whenever the config changes.
|
||||
-- to have something to update, we insert the first and only row at id 0
|
||||
INSERT INTO config (id, privacyMode) VALUES (
|
||||
0, -- the constant id we will query
|
||||
2 -- community is the default mode unless overridden
|
||||
INSERT INTO config (id, privacyMode, defaultLanguage, use_subdomain_for_aliases) VALUES (
|
||||
0, -- the constant id we will query
|
||||
2, -- community is the default mode unless overridden
|
||||
"en", -- english is the default language for all installs
|
||||
1 -- use subdomain for aliases
|
||||
);
|
||||
|
||||
-- +migrate Down
|
||||
|
|
|
@ -23,19 +23,25 @@ import (
|
|||
|
||||
// Config is an object representing the database table.
|
||||
type Config struct {
|
||||
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
|
||||
PrivacyMode roomdb.PrivacyMode `boil:"privacyMode" json:"privacyMode" toml:"privacyMode" yaml:"privacyMode"`
|
||||
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
|
||||
PrivacyMode roomdb.PrivacyMode `boil:"privacyMode" json:"privacyMode" toml:"privacyMode" yaml:"privacyMode"`
|
||||
DefaultLanguage string `boil:"defaultLanguage" json:"defaultLanguage" toml:"defaultLanguage" yaml:"defaultLanguage"`
|
||||
UseSubdomainForAliases bool `boil:"use_subdomain_for_aliases" json:"use_subdomain_for_aliases" toml:"use_subdomain_for_aliases" yaml:"use_subdomain_for_aliases"`
|
||||
|
||||
R *configR `boil:"-" json:"-" toml:"-" yaml:"-"`
|
||||
L configL `boil:"-" json:"-" toml:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
var ConfigColumns = struct {
|
||||
ID string
|
||||
PrivacyMode string
|
||||
ID string
|
||||
PrivacyMode string
|
||||
DefaultLanguage string
|
||||
UseSubdomainForAliases string
|
||||
}{
|
||||
ID: "id",
|
||||
PrivacyMode: "privacyMode",
|
||||
ID: "id",
|
||||
PrivacyMode: "privacyMode",
|
||||
DefaultLanguage: "defaultLanguage",
|
||||
UseSubdomainForAliases: "use_subdomain_for_aliases",
|
||||
}
|
||||
|
||||
// Generated where
|
||||
|
@ -61,12 +67,25 @@ func (w whereHelperroomdb_PrivacyMode) GTE(x roomdb.PrivacyMode) qm.QueryMod {
|
|||
return qmhelper.Where(w.field, qmhelper.GTE, x)
|
||||
}
|
||||
|
||||
type whereHelperbool struct{ field string }
|
||||
|
||||
func (w whereHelperbool) EQ(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }
|
||||
func (w whereHelperbool) NEQ(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) }
|
||||
func (w whereHelperbool) LT(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) }
|
||||
func (w whereHelperbool) LTE(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) }
|
||||
func (w whereHelperbool) GT(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) }
|
||||
func (w whereHelperbool) GTE(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) }
|
||||
|
||||
var ConfigWhere = struct {
|
||||
ID whereHelperint64
|
||||
PrivacyMode whereHelperroomdb_PrivacyMode
|
||||
ID whereHelperint64
|
||||
PrivacyMode whereHelperroomdb_PrivacyMode
|
||||
DefaultLanguage whereHelperstring
|
||||
UseSubdomainForAliases whereHelperbool
|
||||
}{
|
||||
ID: whereHelperint64{field: "\"config\".\"id\""},
|
||||
PrivacyMode: whereHelperroomdb_PrivacyMode{field: "\"config\".\"privacyMode\""},
|
||||
ID: whereHelperint64{field: "\"config\".\"id\""},
|
||||
PrivacyMode: whereHelperroomdb_PrivacyMode{field: "\"config\".\"privacyMode\""},
|
||||
DefaultLanguage: whereHelperstring{field: "\"config\".\"defaultLanguage\""},
|
||||
UseSubdomainForAliases: whereHelperbool{field: "\"config\".\"use_subdomain_for_aliases\""},
|
||||
}
|
||||
|
||||
// ConfigRels is where relationship names are stored.
|
||||
|
@ -86,8 +105,8 @@ func (*configR) NewStruct() *configR {
|
|||
type configL struct{}
|
||||
|
||||
var (
|
||||
configAllColumns = []string{"id", "privacyMode"}
|
||||
configColumnsWithoutDefault = []string{"privacyMode"}
|
||||
configAllColumns = []string{"id", "privacyMode", "defaultLanguage", "use_subdomain_for_aliases"}
|
||||
configColumnsWithoutDefault = []string{"privacyMode", "defaultLanguage", "use_subdomain_for_aliases"}
|
||||
configColumnsWithDefault = []string{"id"}
|
||||
configPrimaryKeyColumns = []string{"id"}
|
||||
)
|
||||
|
|
|
@ -48,15 +48,6 @@ var InviteColumns = struct {
|
|||
|
||||
// Generated where
|
||||
|
||||
type whereHelperbool struct{ field string }
|
||||
|
||||
func (w whereHelperbool) EQ(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.EQ, x) }
|
||||
func (w whereHelperbool) NEQ(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.NEQ, x) }
|
||||
func (w whereHelperbool) LT(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LT, x) }
|
||||
func (w whereHelperbool) LTE(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.LTE, x) }
|
||||
func (w whereHelperbool) GT(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GT, x) }
|
||||
func (w whereHelperbool) GTE(x bool) qm.QueryMod { return qmhelper.Where(w.field, qmhelper.GTE, x) }
|
||||
|
||||
var InviteWhere = struct {
|
||||
ID whereHelperint64
|
||||
HashedToken whereHelperstring
|
||||
|
|
|
@ -40,7 +40,6 @@ func (c Config) SetPrivacyMode(ctx context.Context, pm roomdb.PrivacyMode) error
|
|||
return err
|
||||
}
|
||||
|
||||
// cblgh: a walkthrough of this step (again, now that i have some actual context) would be real good :)
|
||||
err = transact(c.db, func(tx *sql.Tx) error {
|
||||
// get the settings row
|
||||
config, err := models.FindConfig(ctx, tx, configRowID)
|
||||
|
@ -68,3 +67,45 @@ func (c Config) SetPrivacyMode(ctx context.Context, pm roomdb.PrivacyMode) error
|
|||
|
||||
return nil // alles gut!!
|
||||
}
|
||||
|
||||
func (c Config) GetDefaultLanguage(ctx context.Context) (string, error) {
|
||||
config, err := models.FindConfig(ctx, c.db, configRowID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return config.DefaultLanguage, nil
|
||||
}
|
||||
|
||||
func (c Config) SetDefaultLanguage(ctx context.Context, langTag string) error {
|
||||
if len(langTag) == 0 {
|
||||
return fmt.Errorf("language tag cannot be empty")
|
||||
}
|
||||
|
||||
err := transact(c.db, func(tx *sql.Tx) error {
|
||||
// get the settings row
|
||||
config, err := models.FindConfig(ctx, tx, configRowID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// set the new language tag
|
||||
config.DefaultLanguage = langTag
|
||||
// issue update stmt
|
||||
rowsAffected, err := config.Update(ctx, tx, boil.Infer())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if rowsAffected == 0 {
|
||||
return fmt.Errorf("setting default language should have update the settings row, instead 0 rows were updated")
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil // alles gut!!
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/ssb-ngi-pointer/go-ssb-room/roomstate"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
)
|
||||
|
||||
|
@ -65,6 +66,7 @@ func Handler(
|
|||
r *render.Renderer,
|
||||
roomState *roomstate.Manager,
|
||||
fh *weberrors.FlashHelper,
|
||||
locHelper *i18n.Helper,
|
||||
dbs Databases,
|
||||
) http.Handler {
|
||||
mux := &http.ServeMux{}
|
||||
|
@ -84,11 +86,12 @@ func Handler(
|
|||
var sh = settingsHandler{
|
||||
r: r,
|
||||
urlTo: urlTo,
|
||||
|
||||
db: dbs.Config,
|
||||
db: dbs.Config,
|
||||
loc: locHelper,
|
||||
}
|
||||
mux.HandleFunc("/settings", r.HTML("admin/settings.tmpl", sh.overview))
|
||||
mux.HandleFunc("/settings/set-privacy", sh.setPrivacy)
|
||||
mux.HandleFunc("/settings/set-language", sh.setLanguage)
|
||||
|
||||
mux.HandleFunc("/menu", r.HTML("admin/menu.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
return map[string]interface{}{}, nil
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package admin
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
/* can't test English atm due to web/i18n/i18ntesting/testing.go:justTheKeys, which generates translations that are just
|
||||
* translationLabel = "translationLabel"
|
||||
*/
|
||||
// func TestLanguageSetDefaultLanguageEnglish(t *testing.T) {
|
||||
// ts := newSession(t)
|
||||
// a := assert.New(t)
|
||||
//
|
||||
// ts.ConfigDB.GetDefaultLanguageReturns("en", nil)
|
||||
//
|
||||
// u := ts.URLTo(router.AdminSettings)
|
||||
// html, resp := ts.Client.GetHTML(u)
|
||||
// a.Equal(http.StatusOK, resp.Code, "Wrong HTTP status code")
|
||||
//
|
||||
// fmt.Println(html.Html())
|
||||
// summaryElement := html.Find("#language-summary")
|
||||
// summaryText := strings.TrimSpace(summaryElement.Text())
|
||||
// a.Equal("English", summaryText, "summary language should display english translation of language name")
|
||||
// }
|
||||
|
||||
func TestLanguageSetDefaultLanguage(t *testing.T) {
|
||||
ts := newSession(t)
|
||||
a := assert.New(t)
|
||||
|
||||
ts.ConfigDB.GetDefaultLanguageReturns("de", nil)
|
||||
|
||||
u := ts.URLTo(router.AdminSettings)
|
||||
html, resp := ts.Client.GetHTML(u)
|
||||
a.Equal(http.StatusOK, resp.Code, "Wrong HTTP status code")
|
||||
|
||||
summaryElement := html.Find("#language-summary")
|
||||
summaryText := strings.TrimSpace(summaryElement.Text())
|
||||
a.Equal("Deutsch", summaryText, "summary language should display german translation of language name")
|
||||
}
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/members"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
)
|
||||
|
@ -20,49 +21,60 @@ import (
|
|||
type settingsHandler struct {
|
||||
r *render.Renderer
|
||||
urlTo web.URLMaker
|
||||
|
||||
db roomdb.RoomConfig
|
||||
db roomdb.RoomConfig
|
||||
loc *i18n.Helper
|
||||
}
|
||||
|
||||
func (h settingsHandler) overview(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||
privacyModes := []roomdb.PrivacyMode{roomdb.ModeOpen, roomdb.ModeCommunity, roomdb.ModeRestricted}
|
||||
|
||||
currentMode, err := h.db.GetPrivacyMode(req.Context())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve current privacy mode: %w", err)
|
||||
}
|
||||
|
||||
currentLanguage, err := h.db.GetDefaultLanguage(req.Context())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve current privacy mode: %w", err)
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"CurrentMode": currentMode,
|
||||
"PrivacyModes": privacyModes,
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
"CurrentMode": currentMode,
|
||||
"CurrentLanguage": h.loc.ChooseTranslation(currentLanguage),
|
||||
"PrivacyModes": privacyModes,
|
||||
csrf.TemplateTag: csrf.TemplateField(req),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h settingsHandler) setPrivacy(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"))
|
||||
func (h settingsHandler) setLanguage(w http.ResponseWriter, req *http.Request) {
|
||||
if !h.verifyPostRequirements(w, req) {
|
||||
return
|
||||
}
|
||||
|
||||
if err := req.ParseForm(); err != nil {
|
||||
// TODO: proper error type
|
||||
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", err))
|
||||
return
|
||||
}
|
||||
|
||||
// get the member behind the POST
|
||||
currentMember := members.FromContext(req.Context())
|
||||
// handles error cases & make sures the member is an admin
|
||||
currentMember := h.getMember(w, req)
|
||||
if currentMember == nil {
|
||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("not a registered member")}
|
||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
// make sure the member is an admin
|
||||
if currentMember.Role != roomdb.RoleAdmin {
|
||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("yr not an admin! naughty naughty")}
|
||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
||||
langTag := req.Form.Get("lang")
|
||||
|
||||
err := h.db.SetDefaultLanguage(req.Context(), langTag)
|
||||
if err != nil {
|
||||
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("something went wrong when setting the default language: %w", err))
|
||||
}
|
||||
|
||||
// we successfully set the default language! time to redirect to the updated settings overview
|
||||
h.redirect(router.AdminSettings, w, req)
|
||||
}
|
||||
|
||||
func (h settingsHandler) setPrivacy(w http.ResponseWriter, req *http.Request) {
|
||||
if !h.verifyPostRequirements(w, req) {
|
||||
return
|
||||
}
|
||||
// handles error cases & make sures the member is an admin
|
||||
currentMember := h.getMember(w, req)
|
||||
if currentMember == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -78,6 +90,44 @@ func (h settingsHandler) setPrivacy(w http.ResponseWriter, req *http.Request) {
|
|||
}
|
||||
|
||||
// we successfully set the privacy mode! time to redirect to the updated settings overview
|
||||
overview := h.urlTo(router.AdminSettings).String()
|
||||
http.Redirect(w, req, overview, http.StatusFound)
|
||||
h.redirect(router.AdminSettings, w, req)
|
||||
}
|
||||
|
||||
/* common-use functions */
|
||||
|
||||
func (h settingsHandler) getMember(w http.ResponseWriter, req *http.Request) *roomdb.Member {
|
||||
// get the member behind the POST
|
||||
currentMember := members.FromContext(req.Context())
|
||||
if currentMember == nil {
|
||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("not a registered member")}
|
||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// make sure the member is an admin
|
||||
if currentMember.Role != roomdb.RoleAdmin {
|
||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("yr not an admin! naughty naughty")}
|
||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
||||
return nil
|
||||
}
|
||||
return currentMember
|
||||
}
|
||||
|
||||
func (h settingsHandler) verifyPostRequirements(w http.ResponseWriter, req *http.Request) bool {
|
||||
if req.Method != "POST" {
|
||||
err := weberrors.ErrBadRequest{Where: "HTTP Method", Details: fmt.Errorf("expected POST not %s", req.Method)}
|
||||
h.r.Error(w, req, http.StatusBadRequest, err)
|
||||
return false
|
||||
}
|
||||
if err := req.ParseForm(); err != nil {
|
||||
err = weberrors.ErrBadRequest{Where: "Form data", Details: err}
|
||||
h.r.Error(w, req, http.StatusBadRequest, err)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (h settingsHandler) redirect(route string, w http.ResponseWriter, req *http.Request) {
|
||||
overview := h.urlTo(route).String()
|
||||
http.Redirect(w, req, overview, http.StatusSeeOther)
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ func newSession(t *testing.T) *testSession {
|
|||
ts.ConfigDB = new(mockdb.FakeRoomConfig)
|
||||
// default mode for all tests
|
||||
ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil)
|
||||
ts.ConfigDB.GetDefaultLanguageReturns("en", nil)
|
||||
ts.DeniedKeysDB = new(mockdb.FakeDeniedKeysService)
|
||||
ts.MembersDB = new(mockdb.FakeMembersService)
|
||||
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
|
||||
|
@ -107,7 +108,7 @@ func newSession(t *testing.T) *testSession {
|
|||
i18ntesting.WriteReplacement(t)
|
||||
|
||||
testRepo := repo.New(testPath)
|
||||
locHelper, err := i18n.New(testRepo)
|
||||
locHelper, err := i18n.New(testRepo, ts.ConfigDB)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
@ -130,6 +131,8 @@ func newSession(t *testing.T) *testSession {
|
|||
testFuncs["current_page_is"] = func(routeName string) bool { return true }
|
||||
testFuncs["is_logged_in"] = func() *roomdb.Member { return &ts.User }
|
||||
testFuncs["urlToNotice"] = func(name string) string { return "" }
|
||||
testFuncs["language_count"] = func() int { return 1 }
|
||||
testFuncs["list_languages"] = func(*url.URL, string) string { return "" }
|
||||
testFuncs["relative_time"] = func(when time.Time) string { return humanize.Time(when) }
|
||||
|
||||
renderOpts := []render.Option{
|
||||
|
@ -151,6 +154,7 @@ func newSession(t *testing.T) *testSession {
|
|||
r,
|
||||
ts.RoomState,
|
||||
flashHelper,
|
||||
locHelper,
|
||||
Databases{
|
||||
Aliases: ts.AliasesDB,
|
||||
Config: ts.ConfigDB,
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"net/http"
|
||||
|
@ -73,7 +74,7 @@ func New(
|
|||
m := router.CompleteApp()
|
||||
urlTo := web.NewURLTo(m, netInfo)
|
||||
|
||||
locHelper, err := i18n.New(repo)
|
||||
locHelper, err := i18n.New(repo, dbs.Config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -122,6 +123,36 @@ func New(
|
|||
}
|
||||
}),
|
||||
|
||||
render.InjectTemplateFunc("language_count", func(r *http.Request) interface{} {
|
||||
return func() int {
|
||||
return len(locHelper.ListLanguages())
|
||||
}
|
||||
}),
|
||||
|
||||
render.InjectTemplateFunc("list_languages", func(r *http.Request) interface{} {
|
||||
return func(postRoute *url.URL, classList string) (template.HTML, error) {
|
||||
languages := locHelper.ListLanguages()
|
||||
var buf bytes.Buffer
|
||||
|
||||
for _, entry := range languages {
|
||||
data := changeLanguageTemplateData{
|
||||
PostRoute: postRoute.String(),
|
||||
CSRFElement: csrf.TemplateField(r),
|
||||
LangTag: entry.Tag,
|
||||
RedirectPage: r.RequestURI,
|
||||
Translation: entry.Translation,
|
||||
ClassList: classList,
|
||||
}
|
||||
err = changeLanguageTemplate.Execute(&buf, data)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error while executing change language template: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return (template.HTML)(buf.String()), nil
|
||||
}
|
||||
}),
|
||||
|
||||
render.InjectTemplateFunc("urlToNotice", func(r *http.Request) interface{} {
|
||||
return func(name string) *url.URL {
|
||||
noticeName := roomdb.PinnedNoticeName(name)
|
||||
|
@ -167,6 +198,7 @@ func New(
|
|||
}
|
||||
|
||||
CSRF := csrf.Protect(csrfKey,
|
||||
csrf.Path("/"),
|
||||
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
err := csrf.FailureReason(req)
|
||||
// TODO: localize error?
|
||||
|
@ -225,6 +257,7 @@ func New(
|
|||
r,
|
||||
roomState,
|
||||
flashHelper,
|
||||
locHelper,
|
||||
admin.Databases{
|
||||
Aliases: dbs.Aliases,
|
||||
Config: dbs.Config,
|
||||
|
@ -237,6 +270,28 @@ func New(
|
|||
)
|
||||
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
|
||||
|
||||
// handle setting language
|
||||
m.Get(router.CompleteSetLanguage).HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||
lang := req.FormValue("lang")
|
||||
previousRoute := req.FormValue("page")
|
||||
|
||||
session, err := cookieStore.Get(req, i18n.LanguageCookieName)
|
||||
if err != nil {
|
||||
eh.Handle(w, req, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
session.Values["lang"] = lang
|
||||
err = session.Save(req, w)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("we failed to save the language session cookie %w\n", err)
|
||||
eh.Handle(w, req, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, req, previousRoute, http.StatusSeeOther)
|
||||
})
|
||||
|
||||
// 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 #)
|
||||
|
|
|
@ -116,7 +116,7 @@ func TestInviteConsumeInviteHTTP(t *testing.T) {
|
|||
doc, resp := ts.Client.GetHTML(validAcceptURL)
|
||||
a.Equal(http.StatusOK, resp.Code)
|
||||
|
||||
form := doc.Find("form#consume")
|
||||
form := doc.Find("form#inviteConsume")
|
||||
r.Equal(1, form.Length())
|
||||
|
||||
consumeInviteURLString, has := form.Attr("action")
|
||||
|
@ -131,7 +131,7 @@ func TestInviteConsumeInviteHTTP(t *testing.T) {
|
|||
})
|
||||
|
||||
// get the corresponding token from the page
|
||||
csrfTokenElem := doc.Find("input[name='gorilla.csrf.Token']")
|
||||
csrfTokenElem := form.Find(`input[name="gorilla.csrf.Token"]`)
|
||||
a.Equal(1, csrfTokenElem.Length())
|
||||
csrfName, has := csrfTokenElem.Attr("name")
|
||||
a.True(has, "should have a name attribute")
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
package handlers
|
||||
|
||||
import "html/template"
|
||||
|
||||
type changeLanguageTemplateData struct {
|
||||
PostRoute string
|
||||
CSRFElement template.HTML
|
||||
LangTag string
|
||||
RedirectPage string
|
||||
Translation string
|
||||
ClassList string
|
||||
}
|
||||
|
||||
var changeLanguageTemplate = template.Must(template.New("changeLanguageForm").Parse(`
|
||||
<form
|
||||
action="{{ .PostRoute }}"
|
||||
method="POST"
|
||||
>
|
||||
{{ .CSRFElement }}
|
||||
<input type="hidden" name="lang" value="{{ .LangTag }}">
|
||||
<input type="hidden" name="page" value="{{ .RedirectPage }}">
|
||||
<input
|
||||
type="submit"
|
||||
value="{{ .Translation }}"
|
||||
class="{{ .ClassList }}"
|
||||
/>
|
||||
</form>`))
|
|
@ -92,7 +92,7 @@ func TestNoticesEditButtonVisible(t *testing.T) {
|
|||
csrfCookie := resp.Result().Cookies()
|
||||
a.True(len(csrfCookie) > 0, "should have one cookie for CSRF protection validation")
|
||||
|
||||
csrfTokenElem := doc.Find("input[type=hidden]")
|
||||
csrfTokenElem := doc.Find(`form#password-fallback input[type="hidden"]`)
|
||||
a.Equal(1, csrfTokenElem.Length())
|
||||
|
||||
csrfName, has := csrfTokenElem.Attr("name")
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||
)
|
||||
|
||||
func TestLanguageDefaultNoCookie(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a := assert.New(t)
|
||||
route := ts.URLTo(router.CompleteIndex)
|
||||
|
||||
html, res := ts.Client.GetHTML(route)
|
||||
a.Equal(http.StatusOK, res.Code, "wrong HTTP status code")
|
||||
|
||||
languageForms := html.Find("#visitor-set-language form")
|
||||
// two languages: english, deutsch => two <form> elements
|
||||
a.Equal(2, languageForms.Length())
|
||||
|
||||
// verify there is no language cookie to set yet
|
||||
cookieHeader := res.Header()["Set-Cookie"]
|
||||
for _, cookie := range cookieHeader {
|
||||
cookieName := strings.Split(cookie, "=")[0]
|
||||
a.NotEqual(cookieName, i18n.LanguageCookieName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLanguageChooseGerman(t *testing.T) {
|
||||
ts := setup(t)
|
||||
a := assert.New(t)
|
||||
route := ts.URLTo(router.CompleteIndex)
|
||||
postEndpoint := ts.URLTo(router.CompleteSetLanguage)
|
||||
|
||||
html, res := ts.Client.GetHTML(route)
|
||||
a.Equal(http.StatusOK, res.Code, "wrong HTTP status code")
|
||||
|
||||
csrfTokenElem := html.Find(`#visitor-set-language input[name="gorilla.csrf.Token"]`)
|
||||
a.Equal(2, csrfTokenElem.Length())
|
||||
|
||||
csrfName, has := csrfTokenElem.First().Attr("name")
|
||||
a.True(has, "should have a name attribute")
|
||||
|
||||
csrfValue, has := csrfTokenElem.First().Attr("value")
|
||||
a.True(has, "should have value attribute")
|
||||
|
||||
// construct the post request fields, simulating picking a language
|
||||
setLanguageFields := url.Values{
|
||||
"lang": []string{"de"},
|
||||
"page": []string{"/"},
|
||||
csrfName: []string{csrfValue},
|
||||
}
|
||||
|
||||
// set the referer header (important! otherwise our nicely crafted request yields a 500 :'()
|
||||
var refererHeader = make(http.Header)
|
||||
refererHeader.Set("Referer", "https://localhost")
|
||||
ts.Client.SetHeaders(refererHeader)
|
||||
|
||||
// send the post request
|
||||
postRes := ts.Client.PostForm(postEndpoint, setLanguageFields)
|
||||
a.Equal(http.StatusSeeOther, postRes.Code, "wrong HTTP status code for sign in")
|
||||
|
||||
// verify there is one language cookie to set
|
||||
cookieHeader := postRes.Header()["Set-Cookie"]
|
||||
var languageCookies int
|
||||
for _, cookie := range cookieHeader {
|
||||
cookieName := strings.Split(cookie, "=")[0]
|
||||
if cookieName == i18n.LanguageCookieName {
|
||||
languageCookies += 1
|
||||
}
|
||||
}
|
||||
a.Equal(1, languageCookies, "should have one language cookie set after posting")
|
||||
}
|
|
@ -0,0 +1,206 @@
|
|||
LanguageName = "Deutsch"
|
||||
|
||||
# generic terms
|
||||
GenericConfirm = "Ja"
|
||||
GenericGoBack = "Zurück"
|
||||
GenericSave = "Speichern"
|
||||
GenericCreate = "Erstellen"
|
||||
GenericSubmit = "Einreichen"
|
||||
GenericPreview = "Vorschau"
|
||||
GenericLanguage = "Sprache"
|
||||
GenericOpenLink = "Verbindung öffnen"
|
||||
|
||||
PageNotFound = "Die angeforderte Seite wurde nicht gefunden."
|
||||
|
||||
PubKeyRefPlaceholder = "@ .ed25519"
|
||||
|
||||
# roles
|
||||
RoleMember = "Mitglied"
|
||||
RoleModerator = "Moderator"
|
||||
RoleAdmin = "Administrator"
|
||||
|
||||
# navigation labels (should be single words or as short as possible)
|
||||
NavAdminLanding = "Zuhause"
|
||||
NavAdminDashboard = "Armaturenbrett"
|
||||
NavAdminInvites = "Einladungen"
|
||||
NavAdminNotices = "Hinweise"
|
||||
|
||||
# Error messages
|
||||
ErrorAuthBadLogin = "Die angegebenen Authentifizierungsdaten (Benutzername oder Passwort) sind falsch."
|
||||
ErrorNotFound = "Die Datenbank konnte den betreffenden Artikel nicht finden."
|
||||
ErrorAlreadyAdded = "Der öffentliche Schlüssel <strong> {{.Key}} </ strong> ist bereits in der Liste enthalten."
|
||||
ErrorPageNotFound = "Die angeforderte Seite <strong> ({{.Path}}) </ strong> ist nicht vorhanden."
|
||||
ErrorNotAuthorized = "Sie sind nicht autorisiert auf diese Seite zuzugreifen."
|
||||
ErrorForbidden = "Die Anforderung konnte wegen fehlender Berechtigungen ({{.Details}}) nicht ausgeführt werden."
|
||||
ErrorBadRequest = "Bei Ihrer Anfrage ist ein Problem aufgetreten: {{.Where}} ({{.Details}}"
|
||||
|
||||
# authentication
|
||||
################
|
||||
|
||||
AuthTitle = "Mitgliederauthentifizierung"
|
||||
AuthWelcome = "Wenn Sie Mitglied dieses Raums sind, können Sie auf das interne Dashboard zugreifen. Klicken Sie unten auf Ihre bevorzugte Anmeldemethode:"
|
||||
|
||||
AuthSignIn = "Einloggen"
|
||||
AuthSignOut = "Ausloggen"
|
||||
|
||||
# auth with ssb
|
||||
AuthWithSSBTitle = "Mit SSB anmelden"
|
||||
AuthWithSSBInstruct = "Einfache und sichere Methode, wenn Ihre SSB-App dies unterstützt."
|
||||
AuthWithSSBWelcome = "Um sich mit Ihrer auf diesem Gerät gespeicherten SSB-Identität anzumelden, drücken Sie die Taste unten, um eine kompatible SSB-App zu öffnen, falls diese installiert ist."
|
||||
AuthWithSSBInstructQR = "Wenn sich Ihre SSB-App auf einem anderen Gerät befindet, können Sie den folgenden QR-Code scannen, um sich mit der SSB-Identität dieses Geräts anzumelden."
|
||||
AuthWithSSBError = "Anmeldung fehlgeschlagen. Stellen Sie sicher, dass Sie eine SSB-App verwenden, die diese Anmeldemethode unterstützt, und klicken Sie innerhalb einer Minute nach dem Öffnen dieser Seite auf die Schaltfläche oben."
|
||||
|
||||
# auth with password
|
||||
AuthFallbackTitle = "Passwort anmelden"
|
||||
AuthFallbackWelcome = "Eine Anmeldung mit Benutzername und Passwort ist nur möglich, wenn der Administrator Ihnen eines gegeben hat, da wir die Benutzerregistrierung nicht unterstützen."
|
||||
AuthFallbackInstruct = "Diese Methode ist ein akzeptabler Fallback, wenn Sie einen Benutzernamen und ein Passwort haben."
|
||||
|
||||
# general dashboard stuff
|
||||
#########################
|
||||
|
||||
AdminDashboardTitle = "Armaturenbrett"
|
||||
AdminDashboardRoomID = "Der Ausweis dieses Zimmers lautet"
|
||||
|
||||
# privacy modes
|
||||
###############
|
||||
|
||||
ModeOpen = "Öffnen"
|
||||
ModeCommunity = "Gemeinschaft"
|
||||
ModeRestricted = "Eingeschränkt"
|
||||
|
||||
SetPrivacyModeTitle = "Datenschutzmodus einstellen"
|
||||
PrivacyModesTitle = "Datenschutzmodi"
|
||||
RoomsSpecification = "Zimmer 2 Spezifikation"
|
||||
ExplanationPrivacyModes = "Der Datenschutzmodus dieses Raums bestimmt, wer Einladungen erstellen und wer eine Verbindung zum Raum herstellen kann. Weitere Informationen finden Sie unter"
|
||||
ExplanationOpen = "Öffnen Sie Einladungscodes, jeder kann eine Verbindung herstellen"
|
||||
ExplanationCommunity = "Mitglieder können Einladungen erstellen, jeder kann eine Verbindung herstellen"
|
||||
ExplanationRestricted = "Nur Administratoren / Mods können Einladungen erstellen, nur Mitglieder dürfen eine Verbindung herstellen."
|
||||
|
||||
Settings = "Einstellungen"
|
||||
|
||||
DefaultLanguageTitle = "Voreinstellung"
|
||||
ExplanationDefaultLanguage = "Die Standard-Sprachoption steuert die Sprache der Raum-Weboberfläche, die für Erstbesucher angezeigt wird. Die verfügbaren Sprachoptionen werden durch die installierten Übersetzungsdateien definiert."
|
||||
SetDefaultLanguageTitle = "Voreinstellung ändern"
|
||||
|
||||
# banned dashboard
|
||||
##################
|
||||
|
||||
AdminDeniedKeysTitle = "Verboten"
|
||||
AdminDeniedKeysWelcome = "Auf dieser Seite können SSB-IDs gesperrt werden, damit sie nicht mehr auf den Raum zugreifen können."
|
||||
AdminDeniedKeysAdd = "Hinzufügen"
|
||||
AdminDeniedKeysAdded = "Schlüssel wurde zur Liste hinzugefügt."
|
||||
AdminDeniedKeysRemove = "Entfernen"
|
||||
AdminDeniedKeysComment = "Kommentar"
|
||||
AdminDeniedKeysCommentDescription = "Die Person, die dieses Verbot hinzugefügt hat, hat den folgenden Kommentar hinzugefügt"
|
||||
AdminDeniedKeysRemoveConfirmWelcome = "Sind Sie sicher, dass Sie dieses Verbot aufheben möchten? Sie werden wieder Zugang zum Raum haben."
|
||||
AdminDeniedKeysRemoveConfirmTitle = "Mitgliederentfernung bestätigen"
|
||||
|
||||
# members dashboard
|
||||
###################
|
||||
|
||||
AdminMembersTitle = "Mitglieder"
|
||||
AdminMembersWelcome = "Hier sehen Sie alle Mitglieder des Raums und Möglichkeiten, neue hinzuzufügen (anhand ihrer SSB-ID) oder vorhandene zu entfernen."
|
||||
AdminMembersAdd = "Hinzufügen"
|
||||
|
||||
AdminMembersRemoveConfirmTitle = "Mitgliederentfernung bestätigen"
|
||||
AdminMembersRemoveConfirmWelcome = "Sind Sie sicher, dass Sie dieses Mitglied entfernen möchten? Sie verlieren ihren Alias, wenn sie einen haben."
|
||||
|
||||
AdminMemberDetailsTitle = "Mitgliederdetails"
|
||||
AdminMemberDetailsSSBID = "SSB-Kennung"
|
||||
AdminMemberDetailsRole = "Berechtigungsstufe"
|
||||
AdminMemberDetailsAliases = "Aliase"
|
||||
AdminMemberDetailsAliasRevoke = "Widerrufen"
|
||||
AdminMemberDetailsAliasRevoked = "Alias wurde widerrufen"
|
||||
AdminMemberDetailsExclusion = "Ausschluss aus diesem Raum"
|
||||
AdminMemberDetailsRemove = "Mitglied entfernen"
|
||||
|
||||
AdminMemberAdded = "Mitglied erfolgreich hinzugefügt."
|
||||
AdminMemberUpdated = "Mitglied aktualisiert."
|
||||
AdminMemberRemoved = "Mitglied entfernt."
|
||||
|
||||
AdminAliasesRevoke = "Widerrufen"
|
||||
AdminAliasesRevokeConfirmTitle = "Alias widerrufen"
|
||||
AdminAliasesRevokeConfirmWelcome = "Sind Sie sicher, dass Sie diesen Alias widerrufen möchten?"
|
||||
|
||||
# invite dashboard
|
||||
##################
|
||||
|
||||
AdminInvitesTitle = "Einladungen"
|
||||
AdminInvitesWelcome = "Erstellen Sie Einladungs-Token für Personen, die noch keine Mitglieder dieses Raums sind. Auf dieser Seite sehen Sie auch zuvor erstellte Einladungen, die von neuen Mitgliedern noch nicht beansprucht werden."
|
||||
AdminInvitesCreate = "Neue Einladung erstellen"
|
||||
AdminInvitesCreatedAtColumn = "Hergestellt in"
|
||||
AdminInvitesCreatorColumn = "Erstellt von"
|
||||
AdminInvitesActionColumn = "Aktion"
|
||||
AdminInviteRevoke = "Widerrufen"
|
||||
|
||||
AdminInviteRevokeConfirmTitle = "Widerruf der Einladung bestätigen"
|
||||
AdminInviteRevokeConfirmWelcome = "Sind Sie sicher, dass Sie diese Einladung entfernen möchten? Wenn Sie sie bereits gesendet haben, können sie sie nicht verwenden."
|
||||
|
||||
# TODO: add placeholder support to the template helpers (https://github.com/ssb-ngi-pointer/go-ssb-room/issues/60)
|
||||
AdminInviteCreatedBy = "Erstellt von:"
|
||||
AdminInviteSuggestedAliasIs = "Der vorgeschlagene Alias lautet:"
|
||||
AdminInviteSuggestedAliasIsShort = "Alias:"
|
||||
|
||||
AdminInviteCreatedTitle = "Einladung erfolgreich erstellt!"
|
||||
AdminInviteCreatedInstruct = "Kopieren Sie nun den folgenden Link und fügen Sie ihn in einen Freund ein, den Sie in diesen Raum einladen möchten."
|
||||
|
||||
# public invites
|
||||
################
|
||||
|
||||
InviteFacade = "Raum betreten"
|
||||
InviteFacadeTitle = "Raum betreten"
|
||||
InviteFacadeWelcome = "Sie haben die Erlaubnis, Mitglied dieses Raums zu werden, weil jemand diese Einladung mit Ihnen geteilt hat."
|
||||
InviteFacadeInstruct = "Um die Einladung zu erhalten, klicken Sie auf die Schaltfläche unten, um eine kompatible SSB-App zu öffnen, falls diese installiert ist."
|
||||
InviteFacadeJoin = "Tritt diesem Raum bei"
|
||||
InviteFacadeWaiting = "SSB App öffnen"
|
||||
InviteFacadeInstructQR = "Wenn sich Ihre SSB-App auf einem anderen Gerät befindet, können Sie den folgenden QR-Code scannen, um Ihre Einladung auf diesem Gerät zu erhalten:"
|
||||
|
||||
InviteFacadeFallbackWelcome = "Sind Sie neu bei SSB? Es scheint, dass Sie keine SSB-App haben, die diesen Link verstehen kann. Sie können eine dieser Apps installieren:"
|
||||
InviteFacadeFallbackManyverse = "Installiere Manyverse"
|
||||
InviteFacadeFallbackInsertID = "SSB-ID einfügen"
|
||||
|
||||
InviteInsertWelcome = "Sie können Ihre Einladung anfordern, indem Sie unten Ihre SSB-ID eingeben. Danach können Sie in Ihrer SSB-App eine Verbindung zum Raum herstellen."
|
||||
|
||||
InviteConsumedTitle = "Einladung angenommen!"
|
||||
InviteConsumedWelcome = "Sie sind jetzt Mitglied dieses Raums. Wenn Sie eine Multiserver-Adresse benötigen, um eine Verbindung zum Raum herzustellen, können Sie die folgende kopieren und einfügen:"
|
||||
|
||||
# notices (mini-CMS)
|
||||
####################
|
||||
|
||||
NoticeEditTitle = "Hinweis bearbeiten"
|
||||
NoticeList = "Hinweise"
|
||||
NoticeListWelcome = "Hier können Sie den Inhalt der Zielseite und anderer wichtiger Dokumente wie Verhaltenskodex und Datenschutzbestimmungen verwalten."
|
||||
NoticeAddTranslation = "Hinzufügen"
|
||||
NoticeUpdated = "Hinweis aktualisiert"
|
||||
|
||||
NoticeCodeOfConduct = "Verhaltenskodex"
|
||||
NoticeNews = "Nachrichten"
|
||||
NoticeDescription = "Beschreibung"
|
||||
NoticePrivacyPolicy = "Datenschutz-Bestimmungen"
|
||||
|
||||
# Plurals
|
||||
#########
|
||||
# These need to use this form and get {{.Count}}
|
||||
# [Label]
|
||||
# one = singular
|
||||
# other = {{.Count}} things
|
||||
|
||||
[MemberCount]
|
||||
description = "Anzahl der Mitglieder"
|
||||
one = "1 Mitglied"
|
||||
other = "{{.Count}} Mitglieder"
|
||||
|
||||
[ListCount]
|
||||
description = "generische Liste"
|
||||
one = "Es gibt einen Punkt auf der Liste"
|
||||
other = "Die Liste enthält {{.Count}} Elemente"
|
||||
|
||||
[AdminRoomCount]
|
||||
description = "Die Anzahl der Personen in einem Raum"
|
||||
one = "Es ist eine Person im Raum"
|
||||
other = "Es sind {{.Count}} Personen im Raum"
|
||||
|
||||
[AdminInvitesCount]
|
||||
description = "die Anzahl der Einladungen, die noch nicht beansprucht wurden"
|
||||
one = "1 Einladung noch nicht beansprucht"
|
||||
other = "{{.Count}} lädt noch nicht beanspruchte ein"
|
|
@ -1,5 +1,8 @@
|
|||
# default localiztion file for english
|
||||
|
||||
# the name of this language, in its language. Used for language picking
|
||||
LanguageName = "English"
|
||||
|
||||
# generic terms
|
||||
GenericConfirm = "Yes"
|
||||
GenericGoBack = "Back"
|
||||
|
@ -80,6 +83,10 @@ 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"
|
||||
|
||||
DefaultLanguageTitle = "Default Language"
|
||||
ExplanationDefaultLanguage = "The default language option controls the room web interface language displayed for first time visitors. The available languages options are defined by the installed translation files."
|
||||
SetDefaultLanguageTitle = "Set Default Language"
|
||||
|
||||
Settings = "Settings"
|
||||
|
||||
# banned dashboard
|
||||
|
|
|
@ -11,25 +11,51 @@ import (
|
|||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||
"go.mindeco.de/http/render"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
)
|
||||
|
||||
type Helper struct {
|
||||
bundle *i18n.Bundle
|
||||
const LanguageCookieName = "gossbroom-language"
|
||||
|
||||
type TagTranslation struct {
|
||||
Tag string
|
||||
Translation string
|
||||
}
|
||||
|
||||
func New(r repo.Interface) (*Helper, error) {
|
||||
type Helper struct {
|
||||
bundle *i18n.Bundle
|
||||
languages []TagTranslation
|
||||
cookieStore *sessions.CookieStore
|
||||
config roomdb.RoomConfig
|
||||
}
|
||||
|
||||
func New(r repo.Interface, config roomdb.RoomConfig) (*Helper, error) {
|
||||
bundle := i18n.NewBundle(language.English)
|
||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
|
||||
cookieCodec, err := web.LoadOrCreateCookieSecrets(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cookieStore := &sessions.CookieStore{
|
||||
Codecs: cookieCodec,
|
||||
Options: &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 2 * 60 * 60, // two hours in seconds // TODO: configure
|
||||
},
|
||||
}
|
||||
|
||||
// parse toml files and add them to the bundle
|
||||
walkFn := func(path string, info os.FileInfo, rs io.Reader, err error) error {
|
||||
if err != nil {
|
||||
|
@ -57,7 +83,7 @@ func New(r repo.Interface) (*Helper, error) {
|
|||
}
|
||||
|
||||
// walk the embedded defaults
|
||||
err := fs.WalkDir(Defaults, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
err = fs.WalkDir(Defaults, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -114,22 +140,57 @@ func New(r repo.Interface) (*Helper, error) {
|
|||
return nil, fmt.Errorf("i18n: failed to iterate localizations: %w", err)
|
||||
}
|
||||
|
||||
return &Helper{bundle: bundle}, nil
|
||||
// create a mapping of language tags to the translated language names
|
||||
langmap := listLanguages(bundle)
|
||||
return &Helper{bundle: bundle, languages: langmap, cookieStore: cookieStore, config: config}, nil
|
||||
}
|
||||
|
||||
func (h Helper) GetRenderFuncs() []render.Option {
|
||||
var opts = []render.Option{
|
||||
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
|
||||
loc := h.FromRequest(r)
|
||||
return loc.LocalizePlurals
|
||||
}),
|
||||
func listLanguages(bundle *i18n.Bundle) []TagTranslation {
|
||||
languageTags := bundle.LanguageTags()
|
||||
tags := make([]string, 0, len(languageTags))
|
||||
langslice := make([]TagTranslation, 0, len(languageTags))
|
||||
|
||||
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
|
||||
loc := h.FromRequest(r)
|
||||
return loc.LocalizeSimple
|
||||
}),
|
||||
// convert from i18n language tags to a slice of strings
|
||||
for _, langTag := range languageTags {
|
||||
tags = append(tags, langTag.String())
|
||||
}
|
||||
return opts
|
||||
// sort the slice of language tag strings
|
||||
sort.Strings(tags)
|
||||
|
||||
// now that we have a known order, construct a TagTranslation slice mapping language tags to their translations
|
||||
for _, langTag := range tags {
|
||||
var l Localizer
|
||||
l.loc = i18n.NewLocalizer(bundle, langTag)
|
||||
|
||||
msg, err := l.loc.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: "LanguageName",
|
||||
})
|
||||
if err != nil {
|
||||
msg = langTag
|
||||
}
|
||||
|
||||
langslice = append(langslice, TagTranslation{Tag: langTag, Translation: msg})
|
||||
}
|
||||
|
||||
return langslice
|
||||
}
|
||||
|
||||
// ListLanguages returns a slice of the room's translated languages.
|
||||
// The entries of the slice are of the type TagTranslation, consisting of the fields Tag and Translation.
|
||||
// Each Tag fields is a language tag (as strings), and the field Translation is the corresponding translated language
|
||||
// name of that language tag.
|
||||
// Example: {Tag: en, Translation: English}, {Tag: sv, Translation: Svenska} {Tag: de, Translation: Deutsch}
|
||||
func (h Helper) ListLanguages() []TagTranslation {
|
||||
return h.languages
|
||||
}
|
||||
|
||||
func (h Helper) ChooseTranslation(requestedTag string) string {
|
||||
for _, entry := range h.languages {
|
||||
if entry.Tag == requestedTag {
|
||||
return entry.Translation
|
||||
}
|
||||
}
|
||||
return requestedTag
|
||||
}
|
||||
|
||||
type Localizer struct {
|
||||
|
@ -146,11 +207,45 @@ func (h Helper) newLocalizer(lang string, accept ...string) *Localizer {
|
|||
|
||||
// FromRequest returns a new Localizer for the passed helper,
|
||||
// using form value 'lang' and Accept-Language http header from the passed request.
|
||||
// TODO: user settings/cookie values?
|
||||
// If a language cookie is detected, then it takes precedence over the form value & Accept-Lanuage header.
|
||||
func (h Helper) FromRequest(r *http.Request) *Localizer {
|
||||
lang := r.FormValue("lang")
|
||||
accept := r.Header.Get("Accept-Language")
|
||||
return h.newLocalizer(lang, accept)
|
||||
|
||||
session, err := h.cookieStore.Get(r, LanguageCookieName)
|
||||
if err != nil {
|
||||
return h.newLocalizer(lang, accept)
|
||||
}
|
||||
|
||||
prevCookie := session.Values["lang"]
|
||||
if prevCookie != nil {
|
||||
return h.newLocalizer(prevCookie.(string), lang, accept)
|
||||
}
|
||||
|
||||
defaultLang, err := h.config.GetDefaultLanguage(r.Context())
|
||||
|
||||
// if we don't have a default language set, then fallback to whatever we have left :^)
|
||||
if err != nil {
|
||||
return h.newLocalizer(lang, accept)
|
||||
}
|
||||
|
||||
// if we don't have a user cookie set, then use the room's default language setting
|
||||
return h.newLocalizer(defaultLang, accept)
|
||||
}
|
||||
|
||||
func (h Helper) GetRenderFuncs() []render.Option {
|
||||
var opts = []render.Option{
|
||||
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
|
||||
loc := h.FromRequest(r)
|
||||
return loc.LocalizePlurals
|
||||
}),
|
||||
|
||||
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
|
||||
loc := h.FromRequest(r)
|
||||
return loc.LocalizeSimple
|
||||
}),
|
||||
}
|
||||
return opts
|
||||
}
|
||||
|
||||
func (l Localizer) LocalizeSimple(messageID string) string {
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
package i18ntesting
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/roomdb/mockdb"
|
||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
|
||||
)
|
||||
|
||||
func TestListLanguages(t *testing.T) {
|
||||
configDB := new(mockdb.FakeRoomConfig)
|
||||
configDB.GetDefaultLanguageReturns("en", nil)
|
||||
r := repo.New(filepath.Join("testrun", t.Name()))
|
||||
a := assert.New(t)
|
||||
helper, err := i18n.New(r, configDB)
|
||||
a.NoError(err)
|
||||
t.Log(helper)
|
||||
translation := helper.ChooseTranslation("en")
|
||||
a.Equal(translation, "English")
|
||||
}
|
|
@ -9,8 +9,9 @@ const (
|
|||
AdminDashboard = "admin:dashboard"
|
||||
AdminMenu = "admin:menu"
|
||||
|
||||
AdminSettings = "admin:settings:overview"
|
||||
AdminSettingsSetPrivacy = "admin:settings:set-privacy"
|
||||
AdminSettings = "admin:settings:overview"
|
||||
AdminSettingsSetPrivacy = "admin:settings:set-privacy"
|
||||
AdminSettingsSetLanguage = "admin:settings:set-language"
|
||||
|
||||
AdminAliasesRevokeConfirm = "admin:aliases:revoke:confirm"
|
||||
AdminAliasesRevoke = "admin:aliases:revoke"
|
||||
|
@ -49,6 +50,7 @@ func Admin(m *mux.Router) *mux.Router {
|
|||
|
||||
m.Path("/settings").Methods("GET").Name(AdminSettings)
|
||||
m.Path("/settings/set-privacy").Methods("POST").Name(AdminSettingsSetPrivacy)
|
||||
m.Path("/settings/set-language").Methods("POST").Name(AdminSettingsSetLanguage)
|
||||
|
||||
m.Path("/menu").Methods("GET").Name(AdminMenu)
|
||||
|
||||
|
|
|
@ -14,6 +14,8 @@ const (
|
|||
CompleteNoticeShow = "complete:notice:show"
|
||||
CompleteNoticeList = "complete:notice:list"
|
||||
|
||||
CompleteSetLanguage = "complete:set-language"
|
||||
|
||||
CompleteAliasResolve = "complete:alias:resolve"
|
||||
|
||||
CompleteInviteFacade = "complete:invite:accept"
|
||||
|
@ -42,5 +44,7 @@ func CompleteApp() *mux.Router {
|
|||
m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow)
|
||||
m.Path("/notice/list").Methods("GET").Name(CompleteNoticeList)
|
||||
|
||||
m.Path("/set-language").Methods("POST").Name(CompleteSetLanguage)
|
||||
|
||||
return m
|
||||
}
|
||||
|
|
|
@ -4,55 +4,71 @@
|
|||
class="text-3xl tracking-tight font-black text-black mt-2 mb-0"
|
||||
>{{ i18n "Settings" }}</h1>
|
||||
|
||||
<div class="flex flex-col-reverse sm:flex-row justify-start items-stretch ">
|
||||
<div class="max-w-lg">
|
||||
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "PrivacyModesTitle" }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ i18n "ExplanationPrivacyModes" }}
|
||||
<a class="text-pink-600 underline" href="https://ssb-ngi-pointer.github.io/rooms2/#privacy-modes">{{ i18n "RoomsSpecification" }}</a>.
|
||||
</p>
|
||||
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetPrivacyModeTitle" }}</h3>
|
||||
<details class="mb-8 self-start w-96" id="change-privacy">
|
||||
<summary class="px-3 py-1 w-96 rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
|
||||
{{ i18n .CurrentMode.String }}
|
||||
</summary>
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "PrivacyModesTitle" }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ i18n "ExplanationPrivacyModes" }}
|
||||
<a class="text-pink-600 underline" href="https://ssb-ngi-pointer.github.io/rooms2/#privacy-modes">{{ i18n "RoomsSpecification" }}</a>.
|
||||
</p>
|
||||
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetPrivacyModeTitle" }}</h3>
|
||||
<details class="mb-8 self-start max-w-sm" id="change-privacy">
|
||||
<summary class="px-3 py-1 max-w-sm rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
|
||||
{{ i18n .CurrentMode.String }}
|
||||
</summary>
|
||||
|
||||
<div class="absolute w-96 z-10 bg-white mt-2 shadow-xl ring-1 ring-gray-200 rounded divide-y flex flex-col items-stretch overflow-hidden">
|
||||
{{ range .PrivacyModes }}
|
||||
{{ if ne . $.CurrentMode }}
|
||||
<form
|
||||
action="{{ urlTo "admin:settings:set-privacy" }}"
|
||||
method="POST"
|
||||
>
|
||||
{{ $.csrfField }}
|
||||
<input type="hidden" name="privacy_mode" value="{{.}}">
|
||||
<input
|
||||
type="submit"
|
||||
value="{{ i18n .String }}"
|
||||
class="pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer"
|
||||
/>
|
||||
</form>
|
||||
{{ else }}
|
||||
<div class="pr-3 py-2 text-gray-600 flex flex-row items-center cursor-default">
|
||||
<div class="w-10 flex flex-row items-center justify-center">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span>{{ i18n .String }}</span>
|
||||
<div class="absolute max-w-sm z-10 bg-white mt-2 shadow-xl ring-1 ring-gray-200 rounded divide-y flex flex-col items-stretch overflow-hidden">
|
||||
{{ range .PrivacyModes }}
|
||||
{{ if ne . $.CurrentMode }}
|
||||
<form
|
||||
action="{{ urlTo "admin:settings:set-privacy" }}"
|
||||
method="POST"
|
||||
>
|
||||
{{ $.csrfField }}
|
||||
<input type="hidden" name="privacy_mode" value="{{.}}">
|
||||
<input
|
||||
type="submit"
|
||||
value="{{ i18n .String }}"
|
||||
class="pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer"
|
||||
/>
|
||||
</form>
|
||||
{{ else }}
|
||||
<div class="pr-3 py-2 text-gray-600 flex flex-row items-center cursor-default">
|
||||
<div class="w-10 flex flex-row items-center justify-center">
|
||||
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
||||
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
|
||||
</svg>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
<div class="grid max-w-lg grid-cols-3 gap-y-2 mb-8">
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeOpen" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationOpen" }}</div>
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeCommunity" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationCommunity" }}</div>
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeRestricted" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationRestricted" }}</div>
|
||||
</div>
|
||||
<span>{{ i18n .String }}</span>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</details>
|
||||
<div class="grid max-w-lg grid-cols-3 gap-y-2 mb-8">
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeOpen" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationOpen" }}</div>
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeCommunity" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationCommunity" }}</div>
|
||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeRestricted" }}</div>
|
||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationRestricted" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="max-w-2xl">
|
||||
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "DefaultLanguageTitle" }}</h2>
|
||||
<p class="mb-4">
|
||||
{{ i18n "ExplanationDefaultLanguage" }}
|
||||
</p>
|
||||
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetDefaultLanguageTitle" }}</h3>
|
||||
<details class="mb-8 self-start max-w-sm">
|
||||
<summary id="language-summary" class="px-3 py-1 max-w-sm rounded shadow bg-white ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
|
||||
{{ $.CurrentLanguage }}
|
||||
</summary>
|
||||
<div class="mt-2 bg-gray bg-white px-3 rounded ring-1 ring-gray-300">
|
||||
{{ $adminSetLanguageUrl := urlTo "admin:settings:set-language" }}
|
||||
{{ list_languages $adminSetLanguageUrl "pl-10 pr-3 py-2 w-full text-left bg-white text-gray-700 hover:text-gray-900 hover:bg-gray-50 cursor-pointer" }}
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png">
|
||||
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<body class="bg-gray-100 overflow-y-scroll">
|
||||
<div class="sm:mx-auto sm:container">
|
||||
<div class="flex flex-row justify-end space-x-4 my-4">
|
||||
{{$user := is_logged_in}}
|
||||
|
@ -70,23 +70,40 @@
|
|||
{{block "footer" .}}
|
||||
{{$cocUrl := urlToNotice "NoticeCodeOfConduct"}}
|
||||
{{$ppUrl := urlToNotice "NoticePrivacyPolicy"}}
|
||||
<footer class="mb-4 flex flex-row items-center justify-center divide-x divide-gray-300">
|
||||
<a
|
||||
href="{{urlTo "complete:index"}}"
|
||||
class="px-4 text-gray-500 hover:underline"
|
||||
>{{i18n "NavAdminLanding"}}</a>
|
||||
{{if $cocUrl}}
|
||||
<a
|
||||
href="{{$cocUrl}}"
|
||||
class="px-4 text-gray-500 hover:underline"
|
||||
>{{i18n "NoticeCodeOfConduct"}}</a>
|
||||
{{end}}
|
||||
{{if $ppUrl}}
|
||||
<a
|
||||
href="{{$ppUrl}}"
|
||||
class="px-4 text-gray-500 hover:underline"
|
||||
>{{i18n "NoticePrivacyPolicy"}}</a>
|
||||
{{end}}
|
||||
{{$setLanguageUrl := urlTo "complete:set-language"}}
|
||||
<footer class="grid auto-rows-min mb-12">
|
||||
<div class="mb-4 flex flex-row items-center justify-center divide-x divide-gray-300">
|
||||
<a
|
||||
href="{{urlTo "complete:index"}}"
|
||||
class="px-4 text-gray-500 hover:underline"
|
||||
>{{i18n "NavAdminLanding"}}</a>
|
||||
{{if $cocUrl}}
|
||||
<a
|
||||
href="{{$cocUrl}}"
|
||||
class="px-4 text-gray-500 hover:underline"
|
||||
>{{i18n "NoticeCodeOfConduct"}}</a>
|
||||
{{end}}
|
||||
{{if $ppUrl}}
|
||||
<a
|
||||
href="{{$ppUrl}}"
|
||||
class="px-4 text-gray-500 hover:underline"
|
||||
>{{i18n "NoticePrivacyPolicy"}}</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="flex justify-center">
|
||||
{{ $languages := language_count }}
|
||||
{{ if gt $languages 1 }}
|
||||
<details class="w-72">
|
||||
<summary
|
||||
class="mb-2 mx-auto px-3 py-1 text-gray-500 w-32 rounded shadow bg-gray-50 ring-1 ring-gray-300 hover:bg-gray-100 cursor-pointer">
|
||||
Language
|
||||
</summary>
|
||||
<div id="visitor-set-language" class="grid grid-cols-2 justify-items-center gap-x-1">
|
||||
{{ list_languages $setLanguageUrl "text-gray-500 hover:underline cursor-pointer" }}
|
||||
</div>
|
||||
</details>
|
||||
{{ end }}
|
||||
</div>
|
||||
</footer>
|
||||
{{end}}
|
||||
</div>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<span id="welcome" class="text-center mt-8">{{i18n "InviteInsertWelcome"}}</span>
|
||||
|
||||
<form
|
||||
id="consume"
|
||||
id="inviteConsume"
|
||||
action="{{urlTo "complete:invite:consume"}}"
|
||||
method="POST"
|
||||
class="flex flex-col items-center self-stretch"
|
||||
|
@ -24,4 +24,4 @@
|
|||
>{{i18n "GenericSubmit"}}</button>
|
||||
</form>
|
||||
</div>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
|
Loading…
Reference in New Issue