add default language admin ui functionality

This commit is contained in:
cblgh 2021-04-16 14:33:31 +02:00
parent e941e10f24
commit c97b7d44c3
15 changed files with 413 additions and 110 deletions

View File

@ -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

View File

@ -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{}{}

View File

@ -1,8 +1,9 @@
-- +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
CHECK (id == 0) -- should only ever store one row
);
@ -10,9 +11,10 @@ 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) 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
);
-- +migrate Down

View File

@ -23,19 +23,22 @@ 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"`
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
}{
ID: "id",
PrivacyMode: "privacyMode",
ID: "id",
PrivacyMode: "privacyMode",
DefaultLanguage: "defaultLanguage",
}
// Generated where
@ -62,11 +65,13 @@ func (w whereHelperroomdb_PrivacyMode) GTE(x roomdb.PrivacyMode) qm.QueryMod {
}
var ConfigWhere = struct {
ID whereHelperint64
PrivacyMode whereHelperroomdb_PrivacyMode
ID whereHelperint64
PrivacyMode whereHelperroomdb_PrivacyMode
DefaultLanguage whereHelperstring
}{
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\""},
}
// ConfigRels is where relationship names are stored.
@ -86,8 +91,8 @@ func (*configR) NewStruct() *configR {
type configL struct{}
var (
configAllColumns = []string{"id", "privacyMode"}
configColumnsWithoutDefault = []string{"privacyMode"}
configAllColumns = []string{"id", "privacyMode", "defaultLanguage"}
configColumnsWithoutDefault = []string{"privacyMode", "defaultLanguage"}
configColumnsWithDefault = []string{"id"}
configPrimaryKeyColumns = []string{"id"}
)

View File

@ -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,46 @@ func (c Config) SetPrivacyMode(ctx context.Context, pm roomdb.PrivacyMode) error
return nil // alles gut!!
}
// TODO: use proper language tag from "golang.org/x/text/language"?
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!!
}

View File

@ -22,6 +22,7 @@ import (
"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/router"
"github.com/ssb-ngi-pointer/go-ssb-room/web/i18n"
)
// HTMLTemplates define the list of files the template system should load.
@ -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,
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

View File

@ -13,56 +13,68 @@ 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"
)
type settingsHandler struct {
r *render.Renderer
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" {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request"))
return false
}
if err := req.ParseForm(); err != nil {
// TODO: proper error type
h.r.Error(w, req, http.StatusBadRequest, fmt.Errorf("bad request: %w", 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)
}

View File

@ -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)
}
@ -131,7 +132,7 @@ func newSession(t *testing.T) *testSession {
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() string { return "" }
testFuncs["list_languages"] = func(*url.URL, string) string { return "" }
testFuncs["relative_time"] = func(when time.Time) string { return humanize.Time(when) }
renderOpts := []render.Option{
@ -153,6 +154,7 @@ func newSession(t *testing.T) *testSession {
r,
ts.RoomState,
flashHelper,
locHelper,
Databases{
Aliases: ts.AliasesDB,
Config: ts.ConfigDB,

View File

@ -74,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
}
@ -130,11 +130,9 @@ func New(
}),
render.InjectTemplateFunc("list_languages", func(r *http.Request) interface{} {
urlTo := web.NewURLTo(m)
route := urlTo(router.CompleteSetLanguage).String()
csrfElement := csrf.TemplateField(r)
createFormElement := func(tag, translation string) string {
createFormElement := func(postRoute, tag, translation, classList string) string {
return fmt.Sprintf(`
<form
action="%s"
@ -146,16 +144,16 @@ func New(
<input
type="submit"
value="%s"
class="text-gray-500 hover:underline cursor-pointer"
class="%s"
/>
</form>
`, route, csrfElement, tag, r.RequestURI, translation)
`, postRoute, csrfElement, tag, r.RequestURI, translation, classList)
}
return func() template.HTML {
return func(postRoute *url.URL, classList string) template.HTML {
languages := locHelper.ListLanguages()
languageOptions := make([]string, len(languages))
for tag, translation := range languages {
languageOptions = append(languageOptions, createFormElement(tag, translation))
languageOptions = append(languageOptions, createFormElement(postRoute.String(), tag, translation, classList))
}
return (template.HTML)(strings.Join(languageOptions, "\n"))
}
@ -264,6 +262,7 @@ func New(
r,
roomState,
flashHelper,
locHelper,
admin.Databases{
Aliases: dbs.Aliases,
Config: dbs.Config,
@ -283,14 +282,14 @@ func New(
session, err := cookieStore.Get(req, i18n.LanguageCookieName)
if err != nil {
fmt.Printf("cookie error? %w\n", err)
fmt.Errorf("cookie error? %w\n", err)
return
}
session.Values["lang"] = lang
err = session.Save(req, w)
if err != nil {
fmt.Printf("we failed to save the language session cookie %w\n", err)
fmt.Errorf("we failed to save the language session cookie %w\n", err)
}
http.Redirect(w, req, previousRoute, http.StatusSeeOther)

View File

@ -83,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

View File

@ -16,6 +16,7 @@ import (
"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"
@ -29,9 +30,10 @@ type Helper struct {
bundle *i18n.Bundle
languages map[string]string
cookieStore *sessions.CookieStore
config roomdb.RoomConfig
}
func New(r repo.Interface) (*Helper, error) {
func New(r repo.Interface, config roomdb.RoomConfig) (*Helper, error) {
bundle := i18n.NewBundle(language.English)
bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
@ -134,7 +136,7 @@ func New(r repo.Interface) (*Helper, error) {
// create a mapping of language tags to the translated language names
langmap := listLanguages(bundle)
return &Helper{bundle: bundle, languages: langmap, cookieStore: cookieStore}, nil
return &Helper{bundle: bundle, languages: langmap, cookieStore: cookieStore, config: config}, nil
}
func listLanguages(bundle *i18n.Bundle) map[string]string {
@ -164,6 +166,13 @@ func (h Helper) ListLanguages() map[string]string {
return h.languages
}
func (h Helper) ChooseTranslation(tag string) string {
if translation, ok := h.languages[tag]; ok {
return translation
}
return tag
}
type Localizer struct {
loc *i18n.Localizer
}
@ -193,7 +202,15 @@ func (h Helper) FromRequest(r *http.Request) *Localizer {
return h.newLocalizer(prevCookie.(string), lang, accept)
}
return h.newLocalizer(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 {

View File

@ -6,13 +6,16 @@ import (
"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 TestListAllLanguages(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)
helper, err := i18n.New(r, configDB)
a.NoError(err)
t.Log(helper)
langmap := helper.ListLanguages()

View File

@ -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)

View File

@ -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 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}}

View File

@ -70,6 +70,7 @@
{{block "footer" .}}
{{$cocUrl := urlToNotice "NoticeCodeOfConduct"}}
{{$ppUrl := urlToNotice "NoticePrivacyPolicy"}}
{{$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
@ -98,7 +99,7 @@
Language
</summary>
<div class="grid grid-cols-2 justify-items-center gap-x-1">
{{ list_languages }}
{{ list_languages $setLanguageUrl "text-gray-500 hover:underline cursor-pointer" }}
</div>
</details>
{{ end }}