Resolve the page UI language once and let each ExApp fill its own Phrases.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-11 11:45:25 +02:00
co-authored by Cursor
parent afbd76445d
commit 623ada5f99
14 changed files with 1051 additions and 53 deletions
+163
View File
@@ -0,0 +1,163 @@
package gonexapp
import (
"context"
_ "embed"
"encoding/json"
"net/http"
"net/url"
"strings"
)
//go:embed i18n/en.json
var chromeENJSON []byte
//go:embed i18n/de.json
var chromeDEJSON []byte
// chromeSets is the Library's own chrome. It is not part of the map the page script sees.
var chromeSets = map[string]map[string]string{
"en": mustChrome(chromeENJSON),
"de": mustChrome(chromeDEJSON),
}
func mustChrome(raw []byte) map[string]string {
var m map[string]string
if err := json.Unmarshal(raw, &m); err != nil {
panic(err)
}
return m
}
type uiLanguageKey struct{}
// UILanguage is the language of the Phrases for this request, "en" or "de".
// It reads the Requesting user's language from the Provisioning OCS user
// record and forwards the request's Accept-Language on that read.
// The locale field is ignored. A more specific code uses the broader
// language when this Library provides that language. Any other code, a
// missing Requesting user, or a failed read is English, and the page still renders.
// A second call with the request returned from a page render reuses that result.
func (c Credentials) UILanguage(r *http.Request) string {
if r == nil {
return "en"
}
if lang, ok := r.Context().Value(uiLanguageKey{}).(string); ok {
return lang
}
return c.readUILanguage(r)
}
func (c Credentials) readUILanguage(r *http.Request) string {
if c.BaseURL == "" {
return "en"
}
userID, err := UserFromRequest(r)
if err != nil || userID == "" {
return "en"
}
ocs := OCSClient{Cred: c.WithUser(userID)}
if al := r.Header.Get("Accept-Language"); al != "" {
ocs.Header = http.Header{"Accept-Language": {al}}
}
raw, err := ocs.Call(http.MethodGet, "cloud/users/"+url.PathEscape(userID), nil)
if err != nil {
return "en"
}
return matchLanguage(userLanguage(raw), chromeHas)
}
func chromeHas(code string) bool {
_, ok := chromeSets[code]
return ok
}
// matchLanguage picks a Phrase set for the code Nextcloud returned.
// The code itself wins when that set exists. Otherwise the part before "_"
// wins when that set exists. Otherwise English.
func matchLanguage(code string, has func(string) bool) string {
code = strings.TrimSpace(code)
if code != "" && has(code) {
return code
}
if i := strings.IndexByte(code, '_'); i > 0 {
if base := code[:i]; has(base) {
return base
}
}
return "en"
}
func userLanguage(raw []byte) string {
var parsed struct {
OCS struct {
Data struct {
Language string `json:"language"`
} `json:"data"`
} `json:"ocs"`
}
if err := json.Unmarshal(raw, &parsed); err != nil {
return ""
}
return parsed.OCS.Data.Language
}
func chromePhrase(lang, id string, holes map[string]string) string {
if _, ok := chromeSets[lang]; !ok {
lang = "en"
}
text := chromeSets[lang][id]
if text == "" && lang != "en" {
text = chromeSets["en"][id]
}
if text == "" {
return id
}
return fillHoles(text, holes)
}
// fillHoles replaces {name} with the plain-text value.
// A hole with no value stays visible as {name}.
// A value is not scanned for further holes.
func fillHoles(text string, holes map[string]string) string {
if len(holes) == 0 || !strings.Contains(text, "{") {
return text
}
var b strings.Builder
b.Grow(len(text))
for i := 0; i < len(text); {
if text[i] != '{' {
b.WriteByte(text[i])
i++
continue
}
end := strings.IndexByte(text[i+1:], '}')
if end < 0 {
b.WriteByte(text[i])
i++
continue
}
name := text[i+1 : i+1+end]
value, ok := holes[name]
if !ok {
b.WriteByte(text[i])
i++
continue
}
b.WriteString(value)
i += end + 2
}
return b.String()
}
// rememberUILanguage returns r carrying lang so a later UILanguage call
// on this page does not read Nextcloud again.
func rememberUILanguage(r *http.Request, lang string) *http.Request {
if r == nil {
return nil
}
if _, ok := r.Context().Value(uiLanguageKey{}).(string); ok {
return r
}
return r.WithContext(context.WithValue(r.Context(), uiLanguageKey{}, lang))
}