diff options
Diffstat (limited to 'internal/web/server.go')
| -rw-r--r-- | internal/web/server.go | 72 |
1 files changed, 60 insertions, 12 deletions
diff --git a/internal/web/server.go b/internal/web/server.go index a7af06b..4bb408e 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -12,6 +12,7 @@ import ( "github.com/gramanas/blogspace/internal/auth" "github.com/gramanas/blogspace/internal/config" + "github.com/gramanas/blogspace/internal/i18n" "github.com/gramanas/blogspace/internal/store" ) @@ -27,7 +28,7 @@ type Server struct { } func NewServer(cfg *config.Config, st *store.Store) *Server { - s := &Server{cfg: cfg, st: st, tpl: newTemplates(cfg.Dev, funcs)} + s := &Server{cfg: cfg, st: st, tpl: newTemplates(cfg.Dev)} s.root = s.rootRoutes() s.blog = s.subdomainRoutes() return s @@ -70,6 +71,7 @@ const ( ctxBlog // the blog a handler operates on (public page, or /b/{sub}/ management) ctxBlogStore // that blog's store, bound to its own database ctxHostSub // subdomain derived from the Host header + ctxLang // language of the page, once a middleware has settled it ) func currentUser(r *http.Request) *store.User { @@ -104,7 +106,8 @@ func (s *Server) resolveBlog(r *http.Request, sub string) (*http.Request, error) return r.WithContext(context.WithValue(ctx, ctxBlogStore, bs)), nil } -// hostBlog resolves the blog named by the Host header into the context for public pages. +// hostBlog resolves the blog named by the Host header into the context for +// public pages, which speak the blog's language. func (s *Server) hostBlog(next http.HandlerFunc) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { sub, _ := r.Context().Value(ctxHostSub).(string) @@ -117,8 +120,47 @@ func (s *Server) hostBlog(next http.HandlerFunc) http.HandlerFunc { s.serverError(w, err) return } - next(w, r) + next(w, withLang(r, currentBlog(r).Language)) + } +} + +func withLang(r *http.Request, lang string) *http.Request { + return r.WithContext(context.WithValue(r.Context(), ctxLang, lang)) +} + +// lang is the language of the page being served: settled by hostBlog/withBlog, +// otherwise the user's own blog's, otherwise whatever the browser asks for. +func (s *Server) lang(r *http.Request) string { + if l, ok := r.Context().Value(ctxLang).(string); ok { + return l + } + if u := currentUser(r); u != nil { + return s.userLang(r, u) + } + return i18n.Match(r.Header.Get("Accept-Language")) +} + +// userLang is the language of the user's own blog (a user has one blog), so +// the dashboard reads the same on every page, including another user's blog. +func (s *Server) userLang(r *http.Request, u *store.User) string { + blog, err := s.st.BlogByOwner(r.Context(), u.ID) + if err == nil { + _, err = s.st.Open(r.Context(), blog) } + if err != nil { + if !errors.Is(err, store.ErrNotFound) { + log.Printf("language of %s: %v", u.Username, err) + } + return i18n.Default + } + return blog.Language +} + +// tr translates a message for the request's language; trf also formats it. +func (s *Server) tr(r *http.Request, key string) string { return i18n.T(s.lang(r), key) } + +func (s *Server) trf(r *http.Request, key string, args ...any) string { + return i18n.Tf(s.lang(r), key, args...) } // session loads the user from the JWT cookie (if any) into the context. @@ -159,14 +201,14 @@ func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc { if err := parseForm(r); err != nil { var tooBig *http.MaxBytesError if errors.As(err, &tooBig) { - s.plainError(w, http.StatusRequestEntityTooLarge, fmt.Sprintf("Upload too large: the limit is %d MB.", s.cfg.MaxUploadBytes>>20)) + s.plainError(w, http.StatusRequestEntityTooLarge, s.trf(r, "Upload too large: the limit is %d MB.", s.cfg.MaxUploadBytes>>20)) return } - s.plainError(w, http.StatusBadRequest, "Could not read the form.") + s.plainError(w, http.StatusBadRequest, s.tr(r, "Could not read the form.")) return } if !auth.CheckCSRF(s.cfg.JWTSecret, u.ID, u.TokenVersion, r.FormValue("_csrf")) { - s.plainError(w, http.StatusForbidden, "Form expired or invalid. Go back, reload the page and try again.") + s.plainError(w, http.StatusForbidden, s.tr(r, "Form expired or invalid. Go back, reload the page and try again.")) return } } @@ -186,7 +228,7 @@ func parseForm(r *http.Request) error { func (s *Server) requireAdmin(next http.HandlerFunc) http.HandlerFunc { return s.requireAuth(func(w http.ResponseWriter, r *http.Request) { if !currentUser(r).IsSuperadmin() { - s.plainError(w, http.StatusForbidden, "Superadmin only.") + s.plainError(w, http.StatusForbidden, s.tr(r, "Superadmin only.")) return } next(w, r) @@ -206,11 +248,16 @@ func (s *Server) withBlog(next http.HandlerFunc) http.HandlerFunc { s.serverError(w, err) return } - if blog := currentBlog(r); blog.OwnerID != u.ID && !u.IsSuperadmin() { - s.plainError(w, http.StatusForbidden, "This is not your blog.") + blog := currentBlog(r) + if blog.OwnerID != u.ID && !u.IsSuperadmin() { + s.plainError(w, http.StatusForbidden, s.tr(r, "This is not your blog.")) return } - next(w, r) + lang := blog.Language + if blog.OwnerID != u.ID { + lang = s.userLang(r, u) + } + next(w, withLang(r, lang)) }) } @@ -226,6 +273,7 @@ type view struct { BlogURL string RootURL string Path string + Lang string // i18n code, also the <html lang> Data map[string]any } @@ -237,7 +285,7 @@ func (s *Server) renderStatus(w http.ResponseWriter, r *http.Request, status int if data == nil { data = map[string]any{} } - v := view{User: currentUser(r), Blog: currentBlog(r), RootURL: s.cfg.RootURL(), Path: r.URL.Path, Data: data} + v := view{User: currentUser(r), Blog: currentBlog(r), RootURL: s.cfg.RootURL(), Path: r.URL.Path, Lang: s.lang(r), Data: data} if v.User != nil { v.CSRF = auth.CSRFToken(s.cfg.JWTSecret, v.User.ID, v.User.TokenVersion) } @@ -250,7 +298,7 @@ func (s *Server) renderStatus(w http.ResponseWriter, r *http.Request, status int } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) - if err := s.tpl.render(w, name, v); err != nil { + if err := s.tpl.render(w, v.Lang, name, v); err != nil { log.Printf("render %s: %v", name, err) fmt.Fprintf(w, "<pre>template error: %v</pre>", err) } |
