feat: translation middleware
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
package page
|
||||
|
||||
type Page struct {
|
||||
UID string
|
||||
Title string
|
||||
CoverImageUID string
|
||||
}
|
||||
|
||||
func (s *Service) GetPages() (pages []Page, err error) {
|
||||
pages = []Page{
|
||||
{
|
||||
UID: "asdf",
|
||||
Title: "asdfAasdf!",
|
||||
CoverImageUID: "500-1000",
|
||||
},
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package page
|
||||
|
||||
type Service struct{}
|
||||
|
||||
func New() *Service {
|
||||
return &Service{}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
package translate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"path"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
serviceKey contextKey = "translationService"
|
||||
langKey contextKey = "language"
|
||||
)
|
||||
|
||||
func Language(ctx context.Context) string {
|
||||
lang, ok := ctx.Value(langKey).(string)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return lang
|
||||
}
|
||||
|
||||
func SupportedLanguages(ctx context.Context) []string {
|
||||
s, ok := ctx.Value(serviceKey).(*Service)
|
||||
if !ok {
|
||||
slog.Error("could not extract translation service from context for getting supported languages")
|
||||
}
|
||||
return s.SupportedLanguages()
|
||||
}
|
||||
|
||||
func T(ctx context.Context, key string) string {
|
||||
s, ok := ctx.Value(serviceKey).(*Service)
|
||||
if !ok {
|
||||
slog.Error("could not extract translation service from context", "ctx", ctx, "key", key)
|
||||
return key
|
||||
}
|
||||
|
||||
lang := Language(ctx)
|
||||
|
||||
translation, ok := s.translations[lang][key]
|
||||
if !ok {
|
||||
slog.Warn("could not translate", "key", key, "language", lang)
|
||||
return key
|
||||
}
|
||||
|
||||
return translation
|
||||
}
|
||||
|
||||
func Path(ctx context.Context, pp ...string) string {
|
||||
return PathForLang(ctx, Language(ctx), pp...)
|
||||
}
|
||||
|
||||
func PathForLang(ctx context.Context, lang string, pp ...string) string {
|
||||
slog.Info("path for lang", "path", pp)
|
||||
s, ok := ctx.Value(serviceKey).(*Service)
|
||||
if !ok {
|
||||
slog.Error("could not extract translation service from context for translating path", "ctx", ctx, "path", pp)
|
||||
return path.Join(pp...)
|
||||
}
|
||||
|
||||
if len(pp) > 0 {
|
||||
if _, ok := s.translations[pp[0]]; ok {
|
||||
pp = pp[1:]
|
||||
}
|
||||
}
|
||||
|
||||
return path.Join(append([]string{lang}, pp...)...)
|
||||
}
|
||||
|
||||
func (s *Service) SupportedLanguages() (languages []string) {
|
||||
for lang := range s.translations {
|
||||
languages = append(languages, lang)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Service) DefaultLanguage() string {
|
||||
return s.defaultLanguage
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package translate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (s *Service) Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
path := r.URL.Path
|
||||
segments := []string{}
|
||||
for segment := range strings.SplitSeq(path, "/") {
|
||||
if segment != "" {
|
||||
segments = append(segments, segment)
|
||||
}
|
||||
}
|
||||
|
||||
// 1. Check if the first segment is a supported language.
|
||||
var currentLang string
|
||||
if len(segments) >= 1 {
|
||||
potentialLang := segments[0]
|
||||
if _, ok := s.translations[potentialLang]; ok {
|
||||
currentLang = potentialLang
|
||||
}
|
||||
}
|
||||
|
||||
if currentLang != "" {
|
||||
// CASE A: Language is present in the URL.
|
||||
// Remove the language from the path so the router sees the clean path.
|
||||
newPath := "/" + strings.Join(segments[1:], "/")
|
||||
if newPath == "" {
|
||||
newPath = "/"
|
||||
}
|
||||
|
||||
// Update the request URL path.
|
||||
r.URL.Path = newPath
|
||||
|
||||
// Inject translator into context.
|
||||
ctx := context.WithValue(r.Context(), serviceKey, s)
|
||||
ctx = context.WithValue(ctx, langKey, currentLang)
|
||||
|
||||
// Pass control to the next handler with the modified request.
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
return
|
||||
}
|
||||
|
||||
// CASE B: Language is NOT in the URL.
|
||||
// Detect best language from Accept-Language header.
|
||||
detectedLang := s.detectLanguage(r.Header.Get("Accept-Language"))
|
||||
|
||||
// Redirect to the URL with the language prefix.
|
||||
targetURL := "/" + detectedLang + path
|
||||
if r.URL.RawQuery != "" {
|
||||
targetURL += "?" + r.URL.RawQuery
|
||||
}
|
||||
|
||||
http.Redirect(w, r, targetURL, http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
// detectLanguage parses the Accept-Language header and returns the best matching supported language.
|
||||
func (s *Service) detectLanguage(header string) string {
|
||||
if header == "" {
|
||||
return s.defaultLanguage
|
||||
}
|
||||
|
||||
// Simple parser for Accept-Language: en-US,en;q=0.9,de;q=0.8
|
||||
// We only care about the first match for simplicity here, or we could rank them.
|
||||
parts := strings.Split(header, ",")
|
||||
for _, part := range parts {
|
||||
tag := strings.Split(strings.TrimSpace(part), ";")[0]
|
||||
// Check for exact match (e.g., "en-US")
|
||||
if _, ok := s.translations[tag]; ok {
|
||||
return tag
|
||||
}
|
||||
// Check for base match (e.g., "en" from "en-US")
|
||||
base := strings.Split(tag, "-")[0]
|
||||
if _, ok := s.translations[base]; ok {
|
||||
return base
|
||||
}
|
||||
}
|
||||
|
||||
return s.defaultLanguage
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package translate
|
||||
|
||||
type Translations map[string]string // key -> translation
|
||||
|
||||
type Service struct {
|
||||
translations map[string]Translations // language -> translations
|
||||
defaultLanguage string
|
||||
}
|
||||
|
||||
func New() *Service {
|
||||
return &Service{
|
||||
defaultLanguage: "de",
|
||||
translations: map[string]Translations{
|
||||
"de": {
|
||||
"home": "Startseite",
|
||||
},
|
||||
"en": {
|
||||
"home": "Home",
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package handlehome
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/layouts"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/pages"
|
||||
)
|
||||
|
||||
func (h *Handler) home(w http.ResponseWriter, r *http.Request) {
|
||||
pageHeaders, err := h.src.GetPages()
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
layouts.Render(r.Context(), w, r, pages.Home(pageHeaders))
|
||||
}
|
||||
|
||||
func (h *Handler) page(w http.ResponseWriter, r *http.Request) {
|
||||
uid := r.PathValue("uid")
|
||||
layouts.Render(r.Context(), w, r, pages.ContentPage(page.Page{Title: uid}))
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/pages"
|
||||
)
|
||||
|
||||
func Home() http.Handler {
|
||||
return templ.Handler(pages.Index())
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package handlehome
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
src pageService
|
||||
}
|
||||
|
||||
func New(mux *http.ServeMux, srv pageService) *Handler {
|
||||
h := &Handler{
|
||||
src: srv,
|
||||
}
|
||||
|
||||
mux.HandleFunc("GET /{$}", h.home)
|
||||
mux.HandleFunc("GET /{uid}", h.page)
|
||||
return h
|
||||
}
|
||||
|
||||
type pageService interface {
|
||||
GetPages() ([]page.Page, error)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web/pages"
|
||||
)
|
||||
|
||||
func Home() http.Handler {
|
||||
return templ.Handler(pages.Index())
|
||||
}
|
||||
@@ -5,19 +5,36 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handler"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/page"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/components/translate"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/handlers/handlehome"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/internal/templer"
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web"
|
||||
)
|
||||
|
||||
func (s *Server) RegisterRoutes() http.Handler {
|
||||
func (s *Server) RegisterRoutes() (h http.Handler) {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
fileServer := http.FileServer(http.FS(web.GetStaticFS()))
|
||||
mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
|
||||
registerStatic(mux)
|
||||
mux.Handle("/", registerOther())
|
||||
|
||||
mux.Handle("/", handler.Home())
|
||||
h = mux
|
||||
h = templer.Middleware(h)
|
||||
h = s.loggingMiddleware(h)
|
||||
return h
|
||||
}
|
||||
|
||||
return s.loggingMiddleware(mux)
|
||||
func registerOther() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
pager := page.New()
|
||||
|
||||
_ = handlehome.New(mux, pager)
|
||||
|
||||
mux.Handle("/", http.FileServer(http.FS(web.GetStaticFS())))
|
||||
|
||||
translator := translate.New()
|
||||
return translator.Middleware(mux)
|
||||
}
|
||||
|
||||
func (s *Server) loggingMiddleware(next http.Handler) http.Handler {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
|
||||
"git.schreifuchs.ch/schreifuchs/schreifuchs.ch/web"
|
||||
)
|
||||
|
||||
// registerStatic registers all the static files.
|
||||
func registerStatic(mux *http.ServeMux) {
|
||||
fileSystem := web.GetStaticFS()
|
||||
|
||||
// we still use go's fileServer to avoid unnecessary implementation
|
||||
fileServer := http.FileServer(http.FS(fileSystem))
|
||||
|
||||
err := fs.WalkDir(fileSystem, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
path = "/" + path
|
||||
|
||||
slog.Info("registering file", "path", path)
|
||||
|
||||
mux.Handle(path, fileServer)
|
||||
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
slog.Error("could not register static files", "err", err)
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package templer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ctxKey int
|
||||
|
||||
const (
|
||||
pathKey ctxKey = iota
|
||||
)
|
||||
|
||||
func Middleware(next http.Handler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
ctx = context.WithValue(ctx, pathKey, r.URL.Path)
|
||||
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// Path returns the current path without a leading slash
|
||||
func Path(ctx context.Context) string {
|
||||
path, ok := ctx.Value(pathKey).(string)
|
||||
if !ok {
|
||||
slog.Error("could not extract request path from context")
|
||||
return ""
|
||||
}
|
||||
return strings.TrimPrefix(path, "/")
|
||||
}
|
||||
Reference in New Issue
Block a user