feat: translation middleware

This commit is contained in:
2026-02-19 21:13:01 +01:00
parent bbae5b1c93
commit cbc2a1a803
35 changed files with 1500 additions and 82 deletions
+81
View File
@@ -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
}
+22
View File
@@ -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",
},
},
}
}