feat(quick-261016-0fm): per-admin theme and locale preferences
- GET and PUT /account/preferences (own row only, interactive sessions, CSRF header on cookie writes) store admin_theme and admin_locale in backend_users.metadata, keeping every other key - GET /auth/me and the My account answers carry preferences - phrasebook.Translator.Locales lists the locales defining a prefix - route tables, conformance cases, OpenAPI document, TS types and docs
This commit is contained in:
@@ -28,7 +28,7 @@ Use cases:
|
||||
- Lookups by request locale (`phrasebook.Translator.Get`, read from the context through `towel.Locale`) or by explicit locale (`phrasebook.Translator.GetIn`), walking the locale, its parent tags (`pt-BR` to `pt`) and then the fallback locale. A missing key returns the key itself.
|
||||
- Laravel-style placeholders: `:name`, `:Name` (first letter upper-cased) and `:NAME` (upper-cased).
|
||||
- Pluralization with `phrasebook.Translator.Choice` and `phrasebook.Translator.ChoiceIn`: CLDR plural maps (`one`, `few`, `many`, `other`, ...) validated against the locale's categories, or Laravel pipes with exact (`{0}`) and range (`[2,*]`) conditions. `:count` is filled in automatically.
|
||||
- Export for the admin SPA: `phrasebook.Translator.Forms` returns a key as CLDR plural forms, `phrasebook.Translator.Bundle` returns every key under a prefix, and `phrasebook.Translator.Resolved` reports which locale such a bundle mostly comes from. Activation fails if a `backend::` string cannot be expressed as CLDR forms.
|
||||
- Export for the admin SPA: `phrasebook.Translator.Forms` returns a key as CLDR plural forms, `phrasebook.Translator.Bundle` returns every key under a prefix, `phrasebook.Translator.Resolved` reports which locale such a bundle mostly comes from, and `phrasebook.Translator.Locales` lists the locales that define keys under a prefix (the admin's supported languages are `Locales("backend::lang.")`). Activation fails if a `backend::` string cannot be expressed as CLDR forms.
|
||||
- Missing keys are logged once per key through `log/slog`, except in the production environment.
|
||||
|
||||
## Usage
|
||||
@@ -104,6 +104,7 @@ func Title(ctx context.Context, app *backpack.App) string {
|
||||
| `phrasebook.Translator.Forms` | Returns a key as CLDR plural forms for the admin SPA. |
|
||||
| `phrasebook.Translator.Bundle` | Returns all keys under a prefix as CLDR forms, merged over the fallback chain. |
|
||||
| `phrasebook.Translator.Resolved` | Returns the first locale in the fallback chain that has keys under a prefix. |
|
||||
| `phrasebook.Translator.Locales` | Returns the sorted locale codes that define at least one key under a prefix. |
|
||||
|
||||
## Configuration
|
||||
|
||||
|
||||
@@ -19,6 +19,11 @@ nav:
|
||||
expand: Expand menu
|
||||
user_menu: User menu
|
||||
view_site: View site
|
||||
theme: "Theme: :mode"
|
||||
theme_system: System
|
||||
theme_light: Light
|
||||
theme_dark: Dark
|
||||
language: Language
|
||||
page:
|
||||
not_found: Page not found.
|
||||
list:
|
||||
@@ -341,6 +346,8 @@ myaccount:
|
||||
current_password_invalid: The current password is incorrect.
|
||||
field_not_editable: This field cannot be changed on My account.
|
||||
subtitle_unavailable: The subtitle cannot be saved because the stored profile metadata is not a JSON object.
|
||||
preferences_unavailable: The preference cannot be saved because the stored profile metadata is not a JSON object.
|
||||
preferences_failed: Your preference could not be saved.
|
||||
saved: Account saved
|
||||
avatar: Avatar
|
||||
avatar_upload: Upload picture
|
||||
|
||||
@@ -21,6 +21,11 @@ nav:
|
||||
expand: Rozwiń menu
|
||||
user_menu: Menu użytkownika
|
||||
view_site: Zobacz stronę
|
||||
theme: "Motyw: :mode"
|
||||
theme_system: Systemowy
|
||||
theme_light: Jasny
|
||||
theme_dark: Ciemny
|
||||
language: Język
|
||||
page:
|
||||
not_found: Nie znaleziono strony.
|
||||
list:
|
||||
@@ -363,6 +368,8 @@ myaccount:
|
||||
current_password_invalid: Obecne hasło jest nieprawidłowe.
|
||||
field_not_editable: Tego pola nie można zmienić w ustawieniach konta.
|
||||
subtitle_unavailable: Nie można zapisać podtytułu, ponieważ zapisane metadane profilu nie są obiektem JSON.
|
||||
preferences_unavailable: Nie można zapisać ustawienia, ponieważ zapisane metadane profilu nie są obiektem JSON.
|
||||
preferences_failed: Nie udało się zapisać ustawienia.
|
||||
saved: Zapisano konto
|
||||
avatar: Awatar
|
||||
avatar_upload: Wgraj zdjęcie
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -533,6 +534,26 @@ func (t *Translator) Resolved(locale, prefix string) string {
|
||||
return t.fallbackLocale()
|
||||
}
|
||||
|
||||
// Locales returns the sorted locale codes whose catalog defines at least one
|
||||
// key starting with prefix: the locales a Bundle under prefix exists in. A
|
||||
// nil translator or catalog returns nil.
|
||||
func (t *Translator) Locales(prefix string) []string {
|
||||
if t == nil || t.cat == nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for locale, byKey := range t.cat.messages {
|
||||
for key := range byKey {
|
||||
if strings.HasPrefix(key, prefix) {
|
||||
out = append(out, locale)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
slices.Sort(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// Has reports whether any locale of the catalog defines key.
|
||||
func (t *Translator) Has(key string) bool {
|
||||
if t == nil || t.cat == nil {
|
||||
|
||||
Reference in New Issue
Block a user