feat(04-02): apply locale fallback and missing-key logging
- Resolve pl-PL then parent then app.fallback_locale then the raw key - Read towel.Locale from request context and default app.locale/fallback to en - Log each missing key once outside production and never log parameters Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package phrasebook
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -55,6 +56,7 @@ type Translator struct {
|
||||
locale string
|
||||
fallback string
|
||||
production bool
|
||||
missing sync.Map
|
||||
}
|
||||
|
||||
// NewTranslator returns a translator that reads from cat. Empty locale or
|
||||
@@ -122,7 +124,7 @@ func (t *Translator) GetIn(locale, key string, params map[string]string) string
|
||||
if t == nil {
|
||||
return key
|
||||
}
|
||||
e, ok := t.cat.lookup(locale, key)
|
||||
e, _, ok := t.find(locale, key)
|
||||
if !ok {
|
||||
return key
|
||||
}
|
||||
@@ -143,20 +145,74 @@ func (t *Translator) ChoiceIn(locale, key string, count any, params map[string]s
|
||||
if t == nil {
|
||||
return key
|
||||
}
|
||||
e, ok := t.cat.lookup(locale, key)
|
||||
e, found, ok := t.find(locale, key)
|
||||
if !ok {
|
||||
return key
|
||||
}
|
||||
text := e.text
|
||||
switch {
|
||||
case len(e.pipes) > 0:
|
||||
text = selectPipe(e.pipes, locale, count)
|
||||
text = selectPipe(e.pipes, found, count)
|
||||
case e.plurals != nil:
|
||||
text = selectMap(e.plurals, locale, count)
|
||||
text = selectMap(e.plurals, found, count)
|
||||
}
|
||||
return interpolate(text, withCount(params, count))
|
||||
}
|
||||
|
||||
func (t *Translator) find(locale, key string) (entry, string, bool) {
|
||||
if t == nil {
|
||||
return entry{}, locale, false
|
||||
}
|
||||
fallback := defaultFallback
|
||||
if t.fallback != "" {
|
||||
fallback = t.fallback
|
||||
}
|
||||
for _, step := range fallbackChain(locale, fallback) {
|
||||
if e, ok := t.cat.lookup(step, key); ok {
|
||||
return e, step, true
|
||||
}
|
||||
}
|
||||
t.logMissing(key)
|
||||
return entry{}, locale, false
|
||||
}
|
||||
|
||||
func (t *Translator) logMissing(key string) {
|
||||
if t.production {
|
||||
return
|
||||
}
|
||||
if _, loaded := t.missing.LoadOrStore(key, struct{}{}); loaded {
|
||||
return
|
||||
}
|
||||
slog.Warn("phrasebook: missing translation key", "key", key)
|
||||
}
|
||||
|
||||
func fallbackChain(requested, fallback string) []string {
|
||||
out := make([]string, 0, 4)
|
||||
seen := make(map[string]struct{}, 4)
|
||||
add := func(s string) {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[s]; ok {
|
||||
return
|
||||
}
|
||||
seen[s] = struct{}{}
|
||||
out = append(out, s)
|
||||
}
|
||||
add(requested)
|
||||
if tag, err := language.Parse(requested); err == nil {
|
||||
add(tag.String())
|
||||
for p := tag.Parent(); !p.IsRoot(); p = p.Parent() {
|
||||
add(p.String())
|
||||
}
|
||||
} else if i := strings.IndexAny(requested, "-_"); i > 0 {
|
||||
add(requested[:i])
|
||||
}
|
||||
add(fallback)
|
||||
return out
|
||||
}
|
||||
|
||||
func selectMap(plurals map[string]string, locale string, count any) string {
|
||||
cat, err := cldrCategory(locale, count)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user