From c97b7d44c362b73cfbe671ffe94fe0f88e4b6f44 Mon Sep 17 00:00:00 2001 From: cblgh Date: Fri, 16 Apr 2021 14:33:31 +0200 Subject: [PATCH] add default language admin ui functionality --- roomdb/interface.go | 2 + roomdb/mockdb/roomconfig.go | 155 +++++++++++++++++++++++ roomdb/sqlite/migrations/03-config.sql | 10 +- roomdb/sqlite/models/config.go | 29 +++-- roomdb/sqlite/roomconfig.go | 44 ++++++- web/handlers/admin/handler.go | 5 +- web/handlers/admin/settings.go | 102 +++++++++++---- web/handlers/admin/setup_test.go | 6 +- web/handlers/http.go | 19 ++- web/i18n/defaults/active.en.toml | 4 + web/i18n/helper.go | 23 +++- web/i18n/i18ntesting/i18n_helper_test.go | 5 +- web/router/admin.go | 6 +- web/templates/admin/settings.tmpl | 110 +++++++++------- web/templates/base.tmpl | 3 +- 15 files changed, 413 insertions(+), 110 deletions(-) diff --git a/roomdb/interface.go b/roomdb/interface.go index 9c0b3b7..1387d40 100644 --- a/roomdb/interface.go +++ b/roomdb/interface.go @@ -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 diff --git a/roomdb/mockdb/roomconfig.go b/roomdb/mockdb/roomconfig.go index 64c7555..b6a8f1b 100644 --- a/roomdb/mockdb/roomconfig.go +++ b/roomdb/mockdb/roomconfig.go @@ -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{}{} diff --git a/roomdb/sqlite/migrations/03-config.sql b/roomdb/sqlite/migrations/03-config.sql index 8b20193..0b89598 100644 --- a/roomdb/sqlite/migrations/03-config.sql +++ b/roomdb/sqlite/migrations/03-config.sql @@ -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 diff --git a/roomdb/sqlite/models/config.go b/roomdb/sqlite/models/config.go index 6b8fcf1..d3f867b 100644 --- a/roomdb/sqlite/models/config.go +++ b/roomdb/sqlite/models/config.go @@ -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"} ) diff --git a/roomdb/sqlite/roomconfig.go b/roomdb/sqlite/roomconfig.go index a456ef1..9527c40 100644 --- a/roomdb/sqlite/roomconfig.go +++ b/roomdb/sqlite/roomconfig.go @@ -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!! +} diff --git a/web/handlers/admin/handler.go b/web/handlers/admin/handler.go index 49e8097..c1f89c0 100644 --- a/web/handlers/admin/handler.go +++ b/web/handlers/admin/handler.go @@ -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 diff --git a/web/handlers/admin/settings.go b/web/handlers/admin/settings.go index 0259a17..5d29fa3 100644 --- a/web/handlers/admin/settings.go +++ b/web/handlers/admin/settings.go @@ -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) } diff --git a/web/handlers/admin/setup_test.go b/web/handlers/admin/setup_test.go index 04282f5..c8bb2f8 100644 --- a/web/handlers/admin/setup_test.go +++ b/web/handlers/admin/setup_test.go @@ -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, diff --git a/web/handlers/http.go b/web/handlers/http.go index 0a1a920..5540a6a 100644 --- a/web/handlers/http.go +++ b/web/handlers/http.go @@ -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(`
- `, 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) diff --git a/web/i18n/defaults/active.en.toml b/web/i18n/defaults/active.en.toml index d8b1f75..cd222a3 100644 --- a/web/i18n/defaults/active.en.toml +++ b/web/i18n/defaults/active.en.toml @@ -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 diff --git a/web/i18n/helper.go b/web/i18n/helper.go index f2076cc..cb21bda 100644 --- a/web/i18n/helper.go +++ b/web/i18n/helper.go @@ -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 { diff --git a/web/i18n/i18ntesting/i18n_helper_test.go b/web/i18n/i18ntesting/i18n_helper_test.go index 745054b..d7a6c5b 100644 --- a/web/i18n/i18ntesting/i18n_helper_test.go +++ b/web/i18n/i18ntesting/i18n_helper_test.go @@ -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() diff --git a/web/router/admin.go b/web/router/admin.go index b5e48c2..f4e3e23 100644 --- a/web/router/admin.go +++ b/web/router/admin.go @@ -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) diff --git a/web/templates/admin/settings.tmpl b/web/templates/admin/settings.tmpl index d1f674e..f2100a8 100644 --- a/web/templates/admin/settings.tmpl +++ b/web/templates/admin/settings.tmpl @@ -4,55 +4,71 @@ class="text-3xl tracking-tight font-black text-black mt-2 mb-0" >{{ i18n "Settings" }} -
-
-

{{ i18n "PrivacyModesTitle" }}

-

- {{ i18n "ExplanationPrivacyModes" }} - {{ i18n "RoomsSpecification" }}. -

-

{{ i18n "SetPrivacyModeTitle" }}

-
- - {{ i18n .CurrentMode.String }} - +
+

{{ i18n "PrivacyModesTitle" }}

+

+ {{ i18n "ExplanationPrivacyModes" }} + {{ i18n "RoomsSpecification" }}. +

+

{{ i18n "SetPrivacyModeTitle" }}

+
+ + {{ i18n .CurrentMode.String }} + -
- {{ range .PrivacyModes }} - {{ if ne . $.CurrentMode }} -
- {{ $.csrfField }} - - -
- {{ else }} -
-
- - - -
- {{ i18n .String }} +
+ {{ range .PrivacyModes }} + {{ if ne . $.CurrentMode }} +
+ {{ $.csrfField }} + + +
+ {{ else }} +
+
+ + +
- {{end}} - {{end}} -
-
-
-
{{ i18n "ModeOpen" }}
-
{{ i18n "ExplanationOpen" }}
-
{{ i18n "ModeCommunity" }}
-
{{ i18n "ExplanationCommunity" }}
-
{{ i18n "ModeRestricted" }}
-
{{ i18n "ExplanationRestricted" }}
-
+ {{ i18n .String }} +
+ {{end}} + {{end}} +
+ +
+
{{ i18n "ModeOpen" }}
+
{{ i18n "ExplanationOpen" }}
+
{{ i18n "ModeCommunity" }}
+
{{ i18n "ExplanationCommunity" }}
+
{{ i18n "ModeRestricted" }}
+
{{ i18n "ExplanationRestricted" }}
+
+

{{ i18n "DefaultLanguageTitle" }}

+

+ {{ i18n "ExplanationDefaultLanguage" }} +

+

{{ i18n "SetDefaultLanguageTitle" }}

+
+ + {{ $.CurrentLanguage }} + +
+ {{ $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" }} +
+
+
+ + {{end}} diff --git a/web/templates/base.tmpl b/web/templates/base.tmpl index 40e0576..0aff24d 100644 --- a/web/templates/base.tmpl +++ b/web/templates/base.tmpl @@ -70,6 +70,7 @@ {{block "footer" .}} {{$cocUrl := urlToNotice "NoticeCodeOfConduct"}} {{$ppUrl := urlToNotice "NoticePrivacyPolicy"}} + {{$setLanguageUrl := urlTo "complete:set-language"}}