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:
+163
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user