aboutsummaryrefslogtreecommitdiffstats
path: root/internal/web/server.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/web/server.go')
-rw-r--r--internal/web/server.go72
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)
}