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-keys": "^8.1.0",
|
||||||
"ssb-replicate": "^1.3.2",
|
"ssb-replicate": "^1.3.2",
|
||||||
"ssb-room": "^1.3.0",
|
"ssb-room": "^1.3.0",
|
||||||
"ssb-room-client": "^0.4.0",
|
"ssb-room-client": "^0.10.0",
|
||||||
"tap-spec": "^5.0.0",
|
"tap-spec": "^5.0.0",
|
||||||
"tape": "^5.2.2"
|
"tape": "^5.2.2"
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,8 @@ import (
|
||||||
type RoomConfig interface {
|
type RoomConfig interface {
|
||||||
GetPrivacyMode(context.Context) (PrivacyMode, error)
|
GetPrivacyMode(context.Context) (PrivacyMode, error)
|
||||||
SetPrivacyMode(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
|
// AuthFallbackService allows password authentication which might be helpful for scenarios
|
||||||
|
|
|
@ -9,6 +9,19 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type FakeRoomConfig struct {
|
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)
|
GetPrivacyModeStub func(context.Context) (roomdb.PrivacyMode, error)
|
||||||
getPrivacyModeMutex sync.RWMutex
|
getPrivacyModeMutex sync.RWMutex
|
||||||
getPrivacyModeArgsForCall []struct {
|
getPrivacyModeArgsForCall []struct {
|
||||||
|
@ -22,6 +35,18 @@ type FakeRoomConfig struct {
|
||||||
result1 roomdb.PrivacyMode
|
result1 roomdb.PrivacyMode
|
||||||
result2 error
|
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
|
SetPrivacyModeStub func(context.Context, roomdb.PrivacyMode) error
|
||||||
setPrivacyModeMutex sync.RWMutex
|
setPrivacyModeMutex sync.RWMutex
|
||||||
setPrivacyModeArgsForCall []struct {
|
setPrivacyModeArgsForCall []struct {
|
||||||
|
@ -38,6 +63,70 @@ type FakeRoomConfig struct {
|
||||||
invocationsMutex sync.RWMutex
|
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) {
|
func (fake *FakeRoomConfig) GetPrivacyMode(arg1 context.Context) (roomdb.PrivacyMode, error) {
|
||||||
fake.getPrivacyModeMutex.Lock()
|
fake.getPrivacyModeMutex.Lock()
|
||||||
ret, specificReturn := fake.getPrivacyModeReturnsOnCall[len(fake.getPrivacyModeArgsForCall)]
|
ret, specificReturn := fake.getPrivacyModeReturnsOnCall[len(fake.getPrivacyModeArgsForCall)]
|
||||||
|
@ -102,6 +191,68 @@ func (fake *FakeRoomConfig) GetPrivacyModeReturnsOnCall(i int, result1 roomdb.Pr
|
||||||
}{result1, result2}
|
}{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 {
|
func (fake *FakeRoomConfig) SetPrivacyMode(arg1 context.Context, arg2 roomdb.PrivacyMode) error {
|
||||||
fake.setPrivacyModeMutex.Lock()
|
fake.setPrivacyModeMutex.Lock()
|
||||||
ret, specificReturn := fake.setPrivacyModeReturnsOnCall[len(fake.setPrivacyModeArgsForCall)]
|
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{} {
|
func (fake *FakeRoomConfig) Invocations() map[string][][]interface{} {
|
||||||
fake.invocationsMutex.RLock()
|
fake.invocationsMutex.RLock()
|
||||||
defer fake.invocationsMutex.RUnlock()
|
defer fake.invocationsMutex.RUnlock()
|
||||||
|
fake.getDefaultLanguageMutex.RLock()
|
||||||
|
defer fake.getDefaultLanguageMutex.RUnlock()
|
||||||
fake.getPrivacyModeMutex.RLock()
|
fake.getPrivacyModeMutex.RLock()
|
||||||
defer fake.getPrivacyModeMutex.RUnlock()
|
defer fake.getPrivacyModeMutex.RUnlock()
|
||||||
|
fake.setDefaultLanguageMutex.RLock()
|
||||||
|
defer fake.setDefaultLanguageMutex.RUnlock()
|
||||||
fake.setPrivacyModeMutex.RLock()
|
fake.setPrivacyModeMutex.RLock()
|
||||||
defer fake.setPrivacyModeMutex.RUnlock()
|
defer fake.setPrivacyModeMutex.RUnlock()
|
||||||
copiedInvocations := map[string][][]interface{}{}
|
copiedInvocations := map[string][][]interface{}{}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
-- +migrate Up
|
-- +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 (
|
CREATE TABLE config (
|
||||||
id integer NOT NULL PRIMARY KEY,
|
id integer NOT NULL PRIMARY KEY,
|
||||||
privacyMode integer NOT NULL, -- open, community, restricted
|
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
|
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
|
-- the config table will only ever contain one row: the rooms current settings
|
||||||
-- we update that row whenever the config changes.
|
-- we update that row whenever the config changes.
|
||||||
-- to have something to update, we insert the first and only row at id 0
|
-- to have something to update, we insert the first and only row at id 0
|
||||||
INSERT INTO config (id, privacyMode) VALUES (
|
INSERT INTO config (id, privacyMode, defaultLanguage, use_subdomain_for_aliases) VALUES (
|
||||||
0, -- the constant id we will query
|
0, -- the constant id we will query
|
||||||
2 -- community is the default mode unless overridden
|
2, -- community is the default mode unless overridden
|
||||||
|
"en", -- english is the default language for all installs
|
||||||
|
1 -- use subdomain for aliases
|
||||||
);
|
);
|
||||||
|
|
||||||
-- +migrate Down
|
-- +migrate Down
|
||||||
|
|
|
@ -23,19 +23,25 @@ import (
|
||||||
|
|
||||||
// Config is an object representing the database table.
|
// Config is an object representing the database table.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
|
ID int64 `boil:"id" json:"id" toml:"id" yaml:"id"`
|
||||||
PrivacyMode roomdb.PrivacyMode `boil:"privacyMode" json:"privacyMode" toml:"privacyMode" yaml:"privacyMode"`
|
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:"-"`
|
R *configR `boil:"-" json:"-" toml:"-" yaml:"-"`
|
||||||
L configL `boil:"-" json:"-" toml:"-" yaml:"-"`
|
L configL `boil:"-" json:"-" toml:"-" yaml:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var ConfigColumns = struct {
|
var ConfigColumns = struct {
|
||||||
ID string
|
ID string
|
||||||
PrivacyMode string
|
PrivacyMode string
|
||||||
|
DefaultLanguage string
|
||||||
|
UseSubdomainForAliases string
|
||||||
}{
|
}{
|
||||||
ID: "id",
|
ID: "id",
|
||||||
PrivacyMode: "privacyMode",
|
PrivacyMode: "privacyMode",
|
||||||
|
DefaultLanguage: "defaultLanguage",
|
||||||
|
UseSubdomainForAliases: "use_subdomain_for_aliases",
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generated where
|
// Generated where
|
||||||
|
@ -61,12 +67,25 @@ func (w whereHelperroomdb_PrivacyMode) GTE(x roomdb.PrivacyMode) qm.QueryMod {
|
||||||
return qmhelper.Where(w.field, qmhelper.GTE, x)
|
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 {
|
var ConfigWhere = struct {
|
||||||
ID whereHelperint64
|
ID whereHelperint64
|
||||||
PrivacyMode whereHelperroomdb_PrivacyMode
|
PrivacyMode whereHelperroomdb_PrivacyMode
|
||||||
|
DefaultLanguage whereHelperstring
|
||||||
|
UseSubdomainForAliases whereHelperbool
|
||||||
}{
|
}{
|
||||||
ID: whereHelperint64{field: "\"config\".\"id\""},
|
ID: whereHelperint64{field: "\"config\".\"id\""},
|
||||||
PrivacyMode: whereHelperroomdb_PrivacyMode{field: "\"config\".\"privacyMode\""},
|
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.
|
// ConfigRels is where relationship names are stored.
|
||||||
|
@ -86,8 +105,8 @@ func (*configR) NewStruct() *configR {
|
||||||
type configL struct{}
|
type configL struct{}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
configAllColumns = []string{"id", "privacyMode"}
|
configAllColumns = []string{"id", "privacyMode", "defaultLanguage", "use_subdomain_for_aliases"}
|
||||||
configColumnsWithoutDefault = []string{"privacyMode"}
|
configColumnsWithoutDefault = []string{"privacyMode", "defaultLanguage", "use_subdomain_for_aliases"}
|
||||||
configColumnsWithDefault = []string{"id"}
|
configColumnsWithDefault = []string{"id"}
|
||||||
configPrimaryKeyColumns = []string{"id"}
|
configPrimaryKeyColumns = []string{"id"}
|
||||||
)
|
)
|
||||||
|
|
|
@ -48,15 +48,6 @@ var InviteColumns = struct {
|
||||||
|
|
||||||
// Generated where
|
// 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 {
|
var InviteWhere = struct {
|
||||||
ID whereHelperint64
|
ID whereHelperint64
|
||||||
HashedToken whereHelperstring
|
HashedToken whereHelperstring
|
||||||
|
|
|
@ -40,7 +40,6 @@ func (c Config) SetPrivacyMode(ctx context.Context, pm roomdb.PrivacyMode) error
|
||||||
return err
|
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 {
|
err = transact(c.db, func(tx *sql.Tx) error {
|
||||||
// get the settings row
|
// get the settings row
|
||||||
config, err := models.FindConfig(ctx, tx, configRowID)
|
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!!
|
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/roomstate"
|
||||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
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"
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -65,6 +66,7 @@ func Handler(
|
||||||
r *render.Renderer,
|
r *render.Renderer,
|
||||||
roomState *roomstate.Manager,
|
roomState *roomstate.Manager,
|
||||||
fh *weberrors.FlashHelper,
|
fh *weberrors.FlashHelper,
|
||||||
|
locHelper *i18n.Helper,
|
||||||
dbs Databases,
|
dbs Databases,
|
||||||
) http.Handler {
|
) http.Handler {
|
||||||
mux := &http.ServeMux{}
|
mux := &http.ServeMux{}
|
||||||
|
@ -84,11 +86,12 @@ func Handler(
|
||||||
var sh = settingsHandler{
|
var sh = settingsHandler{
|
||||||
r: r,
|
r: r,
|
||||||
urlTo: urlTo,
|
urlTo: urlTo,
|
||||||
|
db: dbs.Config,
|
||||||
db: dbs.Config,
|
loc: locHelper,
|
||||||
}
|
}
|
||||||
mux.HandleFunc("/settings", r.HTML("admin/settings.tmpl", sh.overview))
|
mux.HandleFunc("/settings", r.HTML("admin/settings.tmpl", sh.overview))
|
||||||
mux.HandleFunc("/settings/set-privacy", sh.setPrivacy)
|
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) {
|
mux.HandleFunc("/menu", r.HTML("admin/menu.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
return map[string]interface{}{}, nil
|
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/roomdb"
|
||||||
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
"github.com/ssb-ngi-pointer/go-ssb-room/web"
|
||||||
weberrors "github.com/ssb-ngi-pointer/go-ssb-room/web/errors"
|
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/members"
|
||||||
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
"github.com/ssb-ngi-pointer/go-ssb-room/web/router"
|
||||||
)
|
)
|
||||||
|
@ -20,49 +21,60 @@ import (
|
||||||
type settingsHandler struct {
|
type settingsHandler struct {
|
||||||
r *render.Renderer
|
r *render.Renderer
|
||||||
urlTo web.URLMaker
|
urlTo web.URLMaker
|
||||||
|
db roomdb.RoomConfig
|
||||||
db roomdb.RoomConfig
|
loc *i18n.Helper
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h settingsHandler) overview(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
func (h settingsHandler) overview(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
privacyModes := []roomdb.PrivacyMode{roomdb.ModeOpen, roomdb.ModeCommunity, roomdb.ModeRestricted}
|
privacyModes := []roomdb.PrivacyMode{roomdb.ModeOpen, roomdb.ModeCommunity, roomdb.ModeRestricted}
|
||||||
|
|
||||||
currentMode, err := h.db.GetPrivacyMode(req.Context())
|
currentMode, err := h.db.GetPrivacyMode(req.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to retrieve current privacy mode: %w", err)
|
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{}{
|
return map[string]interface{}{
|
||||||
"CurrentMode": currentMode,
|
"CurrentMode": currentMode,
|
||||||
"PrivacyModes": privacyModes,
|
"CurrentLanguage": h.loc.ChooseTranslation(currentLanguage),
|
||||||
csrf.TemplateTag: csrf.TemplateField(req),
|
"PrivacyModes": privacyModes,
|
||||||
|
csrf.TemplateTag: csrf.TemplateField(req),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h settingsHandler) setPrivacy(w http.ResponseWriter, req *http.Request) {
|
func (h settingsHandler) setLanguage(w http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != "POST" {
|
if !h.verifyPostRequirements(w, req) {
|
||||||
// TODO: proper error type
|
|
||||||
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := req.ParseForm(); err != nil {
|
// handles error cases & make sures the member is an admin
|
||||||
// TODO: proper error type
|
currentMember := h.getMember(w, req)
|
||||||
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())
|
|
||||||
if currentMember == nil {
|
if currentMember == nil {
|
||||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("not a registered member")}
|
|
||||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure the member is an admin
|
langTag := req.Form.Get("lang")
|
||||||
if currentMember.Role != roomdb.RoleAdmin {
|
|
||||||
err := weberrors.ErrForbidden{Details: fmt.Errorf("yr not an admin! naughty naughty")}
|
err := h.db.SetDefaultLanguage(req.Context(), langTag)
|
||||||
h.r.Error(w, req, http.StatusInternalServerError, err)
|
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
|
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
|
// we successfully set the privacy mode! time to redirect to the updated settings overview
|
||||||
overview := h.urlTo(router.AdminSettings).String()
|
h.redirect(router.AdminSettings, w, req)
|
||||||
http.Redirect(w, req, overview, http.StatusFound)
|
}
|
||||||
|
|
||||||
|
/* 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)
|
ts.ConfigDB = new(mockdb.FakeRoomConfig)
|
||||||
// default mode for all tests
|
// default mode for all tests
|
||||||
ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil)
|
ts.ConfigDB.GetPrivacyModeReturns(roomdb.ModeCommunity, nil)
|
||||||
|
ts.ConfigDB.GetDefaultLanguageReturns("en", nil)
|
||||||
ts.DeniedKeysDB = new(mockdb.FakeDeniedKeysService)
|
ts.DeniedKeysDB = new(mockdb.FakeDeniedKeysService)
|
||||||
ts.MembersDB = new(mockdb.FakeMembersService)
|
ts.MembersDB = new(mockdb.FakeMembersService)
|
||||||
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
|
ts.PinnedDB = new(mockdb.FakePinnedNoticesService)
|
||||||
|
@ -107,7 +108,7 @@ func newSession(t *testing.T) *testSession {
|
||||||
i18ntesting.WriteReplacement(t)
|
i18ntesting.WriteReplacement(t)
|
||||||
|
|
||||||
testRepo := repo.New(testPath)
|
testRepo := repo.New(testPath)
|
||||||
locHelper, err := i18n.New(testRepo)
|
locHelper, err := i18n.New(testRepo, ts.ConfigDB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -130,6 +131,8 @@ func newSession(t *testing.T) *testSession {
|
||||||
testFuncs["current_page_is"] = func(routeName string) bool { return true }
|
testFuncs["current_page_is"] = func(routeName string) bool { return true }
|
||||||
testFuncs["is_logged_in"] = func() *roomdb.Member { return &ts.User }
|
testFuncs["is_logged_in"] = func() *roomdb.Member { return &ts.User }
|
||||||
testFuncs["urlToNotice"] = func(name string) string { return "" }
|
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) }
|
testFuncs["relative_time"] = func(when time.Time) string { return humanize.Time(when) }
|
||||||
|
|
||||||
renderOpts := []render.Option{
|
renderOpts := []render.Option{
|
||||||
|
@ -151,6 +154,7 @@ func newSession(t *testing.T) *testSession {
|
||||||
r,
|
r,
|
||||||
ts.RoomState,
|
ts.RoomState,
|
||||||
flashHelper,
|
flashHelper,
|
||||||
|
locHelper,
|
||||||
Databases{
|
Databases{
|
||||||
Aliases: ts.AliasesDB,
|
Aliases: ts.AliasesDB,
|
||||||
Config: ts.ConfigDB,
|
Config: ts.ConfigDB,
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -73,7 +74,7 @@ func New(
|
||||||
m := router.CompleteApp()
|
m := router.CompleteApp()
|
||||||
urlTo := web.NewURLTo(m, netInfo)
|
urlTo := web.NewURLTo(m, netInfo)
|
||||||
|
|
||||||
locHelper, err := i18n.New(repo)
|
locHelper, err := i18n.New(repo, dbs.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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{} {
|
render.InjectTemplateFunc("urlToNotice", func(r *http.Request) interface{} {
|
||||||
return func(name string) *url.URL {
|
return func(name string) *url.URL {
|
||||||
noticeName := roomdb.PinnedNoticeName(name)
|
noticeName := roomdb.PinnedNoticeName(name)
|
||||||
|
@ -167,6 +198,7 @@ func New(
|
||||||
}
|
}
|
||||||
|
|
||||||
CSRF := csrf.Protect(csrfKey,
|
CSRF := csrf.Protect(csrfKey,
|
||||||
|
csrf.Path("/"),
|
||||||
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
csrf.ErrorHandler(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
||||||
err := csrf.FailureReason(req)
|
err := csrf.FailureReason(req)
|
||||||
// TODO: localize error?
|
// TODO: localize error?
|
||||||
|
@ -225,6 +257,7 @@ func New(
|
||||||
r,
|
r,
|
||||||
roomState,
|
roomState,
|
||||||
flashHelper,
|
flashHelper,
|
||||||
|
locHelper,
|
||||||
admin.Databases{
|
admin.Databases{
|
||||||
Aliases: dbs.Aliases,
|
Aliases: dbs.Aliases,
|
||||||
Config: dbs.Config,
|
Config: dbs.Config,
|
||||||
|
@ -237,6 +270,28 @@ func New(
|
||||||
)
|
)
|
||||||
mainMux.Handle("/admin/", members.AuthenticateFromContext(r)(adminHandler))
|
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
|
// landing page
|
||||||
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
m.Get(router.CompleteIndex).Handler(r.HTML("landing/index.tmpl", func(w http.ResponseWriter, req *http.Request) (interface{}, error) {
|
||||||
// TODO: try websocket upgrade (issue #)
|
// TODO: try websocket upgrade (issue #)
|
||||||
|
|
|
@ -116,7 +116,7 @@ func TestInviteConsumeInviteHTTP(t *testing.T) {
|
||||||
doc, resp := ts.Client.GetHTML(validAcceptURL)
|
doc, resp := ts.Client.GetHTML(validAcceptURL)
|
||||||
a.Equal(http.StatusOK, resp.Code)
|
a.Equal(http.StatusOK, resp.Code)
|
||||||
|
|
||||||
form := doc.Find("form#consume")
|
form := doc.Find("form#inviteConsume")
|
||||||
r.Equal(1, form.Length())
|
r.Equal(1, form.Length())
|
||||||
|
|
||||||
consumeInviteURLString, has := form.Attr("action")
|
consumeInviteURLString, has := form.Attr("action")
|
||||||
|
@ -131,7 +131,7 @@ func TestInviteConsumeInviteHTTP(t *testing.T) {
|
||||||
})
|
})
|
||||||
|
|
||||||
// get the corresponding token from the page
|
// 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())
|
a.Equal(1, csrfTokenElem.Length())
|
||||||
csrfName, has := csrfTokenElem.Attr("name")
|
csrfName, has := csrfTokenElem.Attr("name")
|
||||||
a.True(has, "should have a name attribute")
|
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()
|
csrfCookie := resp.Result().Cookies()
|
||||||
a.True(len(csrfCookie) > 0, "should have one cookie for CSRF protection validation")
|
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())
|
a.Equal(1, csrfTokenElem.Length())
|
||||||
|
|
||||||
csrfName, has := csrfTokenElem.Attr("name")
|
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
|
# default localiztion file for english
|
||||||
|
|
||||||
|
# the name of this language, in its language. Used for language picking
|
||||||
|
LanguageName = "English"
|
||||||
|
|
||||||
# generic terms
|
# generic terms
|
||||||
GenericConfirm = "Yes"
|
GenericConfirm = "Yes"
|
||||||
GenericGoBack = "Back"
|
GenericGoBack = "Back"
|
||||||
|
@ -80,6 +83,10 @@ ExplanationOpen = "Open invite codes, anyone may connect"
|
||||||
ExplanationCommunity = "Members can create invites, anyone may connect"
|
ExplanationCommunity = "Members can create invites, anyone may connect"
|
||||||
ExplanationRestricted = "Only admins/mods can create invites, only members 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"
|
Settings = "Settings"
|
||||||
|
|
||||||
# banned dashboard
|
# banned dashboard
|
||||||
|
|
|
@ -11,25 +11,51 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/BurntSushi/toml"
|
"github.com/BurntSushi/toml"
|
||||||
|
"github.com/gorilla/sessions"
|
||||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
"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"
|
"go.mindeco.de/http/render"
|
||||||
"golang.org/x/text/language"
|
"golang.org/x/text/language"
|
||||||
|
|
||||||
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
"github.com/ssb-ngi-pointer/go-ssb-room/internal/repo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Helper struct {
|
const LanguageCookieName = "gossbroom-language"
|
||||||
bundle *i18n.Bundle
|
|
||||||
|
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 := i18n.NewBundle(language.English)
|
||||||
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
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
|
// parse toml files and add them to the bundle
|
||||||
walkFn := func(path string, info os.FileInfo, rs io.Reader, err error) error {
|
walkFn := func(path string, info os.FileInfo, rs io.Reader, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -57,7 +83,7 @@ func New(r repo.Interface) (*Helper, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// walk the embedded defaults
|
// 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 {
|
if err != nil {
|
||||||
return err
|
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 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 {
|
func listLanguages(bundle *i18n.Bundle) []TagTranslation {
|
||||||
var opts = []render.Option{
|
languageTags := bundle.LanguageTags()
|
||||||
render.InjectTemplateFunc("i18npl", func(r *http.Request) interface{} {
|
tags := make([]string, 0, len(languageTags))
|
||||||
loc := h.FromRequest(r)
|
langslice := make([]TagTranslation, 0, len(languageTags))
|
||||||
return loc.LocalizePlurals
|
|
||||||
}),
|
|
||||||
|
|
||||||
render.InjectTemplateFunc("i18n", func(r *http.Request) interface{} {
|
// convert from i18n language tags to a slice of strings
|
||||||
loc := h.FromRequest(r)
|
for _, langTag := range languageTags {
|
||||||
return loc.LocalizeSimple
|
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 {
|
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,
|
// FromRequest returns a new Localizer for the passed helper,
|
||||||
// using form value 'lang' and Accept-Language http header from the passed request.
|
// 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 {
|
func (h Helper) FromRequest(r *http.Request) *Localizer {
|
||||||
lang := r.FormValue("lang")
|
lang := r.FormValue("lang")
|
||||||
accept := r.Header.Get("Accept-Language")
|
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 {
|
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"
|
AdminDashboard = "admin:dashboard"
|
||||||
AdminMenu = "admin:menu"
|
AdminMenu = "admin:menu"
|
||||||
|
|
||||||
AdminSettings = "admin:settings:overview"
|
AdminSettings = "admin:settings:overview"
|
||||||
AdminSettingsSetPrivacy = "admin:settings:set-privacy"
|
AdminSettingsSetPrivacy = "admin:settings:set-privacy"
|
||||||
|
AdminSettingsSetLanguage = "admin:settings:set-language"
|
||||||
|
|
||||||
AdminAliasesRevokeConfirm = "admin:aliases:revoke:confirm"
|
AdminAliasesRevokeConfirm = "admin:aliases:revoke:confirm"
|
||||||
AdminAliasesRevoke = "admin:aliases:revoke"
|
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").Methods("GET").Name(AdminSettings)
|
||||||
m.Path("/settings/set-privacy").Methods("POST").Name(AdminSettingsSetPrivacy)
|
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)
|
m.Path("/menu").Methods("GET").Name(AdminMenu)
|
||||||
|
|
||||||
|
|
|
@ -14,6 +14,8 @@ const (
|
||||||
CompleteNoticeShow = "complete:notice:show"
|
CompleteNoticeShow = "complete:notice:show"
|
||||||
CompleteNoticeList = "complete:notice:list"
|
CompleteNoticeList = "complete:notice:list"
|
||||||
|
|
||||||
|
CompleteSetLanguage = "complete:set-language"
|
||||||
|
|
||||||
CompleteAliasResolve = "complete:alias:resolve"
|
CompleteAliasResolve = "complete:alias:resolve"
|
||||||
|
|
||||||
CompleteInviteFacade = "complete:invite:accept"
|
CompleteInviteFacade = "complete:invite:accept"
|
||||||
|
@ -42,5 +44,7 @@ func CompleteApp() *mux.Router {
|
||||||
m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow)
|
m.Path("/notice/show").Methods("GET").Name(CompleteNoticeShow)
|
||||||
m.Path("/notice/list").Methods("GET").Name(CompleteNoticeList)
|
m.Path("/notice/list").Methods("GET").Name(CompleteNoticeList)
|
||||||
|
|
||||||
|
m.Path("/set-language").Methods("POST").Name(CompleteSetLanguage)
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,55 +4,71 @@
|
||||||
class="text-3xl tracking-tight font-black text-black mt-2 mb-0"
|
class="text-3xl tracking-tight font-black text-black mt-2 mb-0"
|
||||||
>{{ i18n "Settings" }}</h1>
|
>{{ i18n "Settings" }}</h1>
|
||||||
|
|
||||||
<div class="flex flex-col-reverse sm:flex-row justify-start items-stretch ">
|
<div class="max-w-2xl">
|
||||||
<div class="max-w-lg">
|
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "PrivacyModesTitle" }}</h2>
|
||||||
<h2 class="text-xl tracking-tight font-bold text-black mt-2 mb-2">{{ i18n "PrivacyModesTitle" }}</h2>
|
<p class="mb-4">
|
||||||
<p class="mb-4">
|
{{ i18n "ExplanationPrivacyModes" }}
|
||||||
{{ i18n "ExplanationPrivacyModes" }}
|
<a class="text-pink-600 underline" href="https://ssb-ngi-pointer.github.io/rooms2/#privacy-modes">{{ i18n "RoomsSpecification" }}</a>.
|
||||||
<a class="text-pink-600 underline" href="https://ssb-ngi-pointer.github.io/rooms2/#privacy-modes">{{ i18n "RoomsSpecification" }}</a>.
|
</p>
|
||||||
</p>
|
<h3 class="text-gray-400 text-sm font-bold mb-2">{{ i18n "SetPrivacyModeTitle" }}</h3>
|
||||||
<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">
|
||||||
<details class="mb-8 self-start w-96" 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">
|
||||||
<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 }}
|
||||||
{{ i18n .CurrentMode.String }}
|
</summary>
|
||||||
</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">
|
<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 }}
|
{{ range .PrivacyModes }}
|
||||||
{{ if ne . $.CurrentMode }}
|
{{ if ne . $.CurrentMode }}
|
||||||
<form
|
<form
|
||||||
action="{{ urlTo "admin:settings:set-privacy" }}"
|
action="{{ urlTo "admin:settings:set-privacy" }}"
|
||||||
method="POST"
|
method="POST"
|
||||||
>
|
>
|
||||||
{{ $.csrfField }}
|
{{ $.csrfField }}
|
||||||
<input type="hidden" name="privacy_mode" value="{{.}}">
|
<input type="hidden" name="privacy_mode" value="{{.}}">
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="{{ i18n .String }}"
|
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"
|
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>
|
</form>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
<div class="pr-3 py-2 text-gray-600 flex flex-row items-center cursor-default">
|
<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">
|
<div class="w-10 flex flex-row items-center justify-center">
|
||||||
<svg class="w-4 h-4" viewBox="0 0 24 24">
|
<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" />
|
<path fill="currentColor" d="M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z" />
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
|
||||||
<span>{{ i18n .String }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
<span>{{ i18n .String }}</span>
|
||||||
{{end}}
|
</div>
|
||||||
</div>
|
{{end}}
|
||||||
</details>
|
{{end}}
|
||||||
<div class="grid max-w-lg grid-cols-3 gap-y-2 mb-8">
|
</div>
|
||||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeOpen" }}</div>
|
</details>
|
||||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationOpen" }}</div>
|
<div class="grid max-w-lg grid-cols-3 gap-y-2 mb-8">
|
||||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeCommunity" }}</div>
|
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeOpen" }}</div>
|
||||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationCommunity" }}</div>
|
<div class="text-md col-span-2 italic">{{ i18n "ExplanationOpen" }}</div>
|
||||||
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeRestricted" }}</div>
|
<div class="text-xl text-gray-500 font-bold">{{ i18n "ModeCommunity" }}</div>
|
||||||
<div class="text-md col-span-2 italic">{{ i18n "ExplanationRestricted" }}</div>
|
<div class="text-md col-span-2 italic">{{ i18n "ExplanationCommunity" }}</div>
|
||||||
</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>
|
</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}}
|
{{end}}
|
||||||
|
|
|
@ -12,7 +12,7 @@
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png">
|
<link rel="icon" type="image/png" sizes="16x16" href="/assets/favicon/favicon-16x16.png">
|
||||||
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
<link rel="manifest" href="/assets/favicon/site.webmanifest">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-100">
|
<body class="bg-gray-100 overflow-y-scroll">
|
||||||
<div class="sm:mx-auto sm:container">
|
<div class="sm:mx-auto sm:container">
|
||||||
<div class="flex flex-row justify-end space-x-4 my-4">
|
<div class="flex flex-row justify-end space-x-4 my-4">
|
||||||
{{$user := is_logged_in}}
|
{{$user := is_logged_in}}
|
||||||
|
@ -70,23 +70,40 @@
|
||||||
{{block "footer" .}}
|
{{block "footer" .}}
|
||||||
{{$cocUrl := urlToNotice "NoticeCodeOfConduct"}}
|
{{$cocUrl := urlToNotice "NoticeCodeOfConduct"}}
|
||||||
{{$ppUrl := urlToNotice "NoticePrivacyPolicy"}}
|
{{$ppUrl := urlToNotice "NoticePrivacyPolicy"}}
|
||||||
<footer class="mb-4 flex flex-row items-center justify-center divide-x divide-gray-300">
|
{{$setLanguageUrl := urlTo "complete:set-language"}}
|
||||||
<a
|
<footer class="grid auto-rows-min mb-12">
|
||||||
href="{{urlTo "complete:index"}}"
|
<div class="mb-4 flex flex-row items-center justify-center divide-x divide-gray-300">
|
||||||
class="px-4 text-gray-500 hover:underline"
|
<a
|
||||||
>{{i18n "NavAdminLanding"}}</a>
|
href="{{urlTo "complete:index"}}"
|
||||||
{{if $cocUrl}}
|
class="px-4 text-gray-500 hover:underline"
|
||||||
<a
|
>{{i18n "NavAdminLanding"}}</a>
|
||||||
href="{{$cocUrl}}"
|
{{if $cocUrl}}
|
||||||
class="px-4 text-gray-500 hover:underline"
|
<a
|
||||||
>{{i18n "NoticeCodeOfConduct"}}</a>
|
href="{{$cocUrl}}"
|
||||||
{{end}}
|
class="px-4 text-gray-500 hover:underline"
|
||||||
{{if $ppUrl}}
|
>{{i18n "NoticeCodeOfConduct"}}</a>
|
||||||
<a
|
{{end}}
|
||||||
href="{{$ppUrl}}"
|
{{if $ppUrl}}
|
||||||
class="px-4 text-gray-500 hover:underline"
|
<a
|
||||||
>{{i18n "NoticePrivacyPolicy"}}</a>
|
href="{{$ppUrl}}"
|
||||||
{{end}}
|
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>
|
</footer>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
<span id="welcome" class="text-center mt-8">{{i18n "InviteInsertWelcome"}}</span>
|
<span id="welcome" class="text-center mt-8">{{i18n "InviteInsertWelcome"}}</span>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
id="consume"
|
id="inviteConsume"
|
||||||
action="{{urlTo "complete:invite:consume"}}"
|
action="{{urlTo "complete:invite:consume"}}"
|
||||||
method="POST"
|
method="POST"
|
||||||
class="flex flex-col items-center self-stretch"
|
class="flex flex-col items-center self-stretch"
|
||||||
|
@ -24,4 +24,4 @@
|
||||||
>{{i18n "GenericSubmit"}}</button>
|
>{{i18n "GenericSubmit"}}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
Loading…
Reference in New Issue