feat: multi-language with FuncMap + lng/ files
This commit is contained in:
parent
45f2509fe4
commit
bde90a8186
2 changed files with 73 additions and 59 deletions
|
|
@ -185,6 +185,11 @@ else
|
|||
echo "[4/5] Swapping binary..."
|
||||
cp "$BINARY_NAME" "$TARGET_DIR/$BINARY_NAME"
|
||||
|
||||
# Copy language files
|
||||
if [ -d "$SCRIPT_DIR/lng" ]; then
|
||||
cp -r "$SCRIPT_DIR/lng" "$TARGET_DIR/"
|
||||
fi
|
||||
|
||||
# Ensure AUTHELIA_SECRET is in .env
|
||||
AUTHELIA_SECRET=$(grep -oP 'session_secret: \K.*' "$TARGET_DIR/config/authelia/configuration.yml" 2>/dev/null || echo "")
|
||||
if [ -n "$AUTHELIA_SECRET" ]; then
|
||||
|
|
|
|||
127
main.go
127
main.go
|
|
@ -190,13 +190,12 @@ func healthHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc {
|
||||
tmpl := template.Must(template.New("launcher").Parse(launcherHTML))
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
user := r.Header.Get("Remote-User")
|
||||
groupsHeader := r.Header.Get("Remote-Groups")
|
||||
userGroups := strings.Split(groupsHeader, ",")
|
||||
settings, _ := loadSettings(os.Getenv("CONFIG_DIR"))
|
||||
lang := getUserLang(r)
|
||||
|
||||
var allowedApps []AppEntry
|
||||
for _, app := range apps {
|
||||
|
|
@ -213,6 +212,7 @@ func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc {
|
|||
Apps []AppEntry
|
||||
CompanyLogo string
|
||||
Subtitle string
|
||||
Lang string
|
||||
}{
|
||||
AppName: cfg.App.Name,
|
||||
Description: cfg.App.Description,
|
||||
|
|
@ -221,9 +221,10 @@ func launcherHandler(cfg *Config, apps []AppEntry) http.HandlerFunc {
|
|||
Apps: allowedApps,
|
||||
CompanyLogo: settings.Company.Logo,
|
||||
Subtitle: settings.Company.Subtitle,
|
||||
Lang: lang,
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
tmpl.Execute(w, data)
|
||||
launcherTmpl.Execute(w, data)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -280,12 +281,6 @@ func adminHandler(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
|
||||
funcMap := template.FuncMap{
|
||||
"t": func(key string) string {
|
||||
return T(settings.Company.Language, key)
|
||||
},
|
||||
}
|
||||
tmpl := template.Must(template.New("admin").Funcs(funcMap).Parse(adminHTML))
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
|
||||
autheliaUp := false
|
||||
|
|
@ -294,11 +289,20 @@ func adminHandler(w http.ResponseWriter, r *http.Request) {
|
|||
resp.Body.Close()
|
||||
}
|
||||
|
||||
tmpl.Execute(w, map[string]interface{}{
|
||||
userLang := os.Getenv("LANG")
|
||||
if userLang == "" {
|
||||
userLang = "en"
|
||||
}
|
||||
if settings != nil && settings.Company.Language != "" {
|
||||
userLang = settings.Company.Language
|
||||
}
|
||||
|
||||
adminTmpl.Execute(w, map[string]interface{}{
|
||||
"Settings": settings,
|
||||
"Domain": domain,
|
||||
"AdminEmail": adminEmail,
|
||||
"AutheliaUp": autheliaUp,
|
||||
"Lang": userLang,
|
||||
})
|
||||
|
||||
case http.MethodPost:
|
||||
|
|
@ -465,59 +469,53 @@ func publicSettingsHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
// --- Translation system ---
|
||||
|
||||
type Translations map[string]string
|
||||
var translations = make(map[string]map[string]string)
|
||||
var launcherTmpl *template.Template
|
||||
var adminTmpl *template.Template
|
||||
|
||||
var translationCache map[string]Translations
|
||||
|
||||
func loadTranslations(lang string) Translations {
|
||||
if translationCache != nil {
|
||||
if cached, ok := translationCache[lang]; ok {
|
||||
return cached
|
||||
}
|
||||
}
|
||||
basePath := filepath.Join("/opt/nextworkspace", "lng", lang)
|
||||
result := Translations{}
|
||||
func loadTranslations(lngDir string) {
|
||||
langs := []string{"en", "de"}
|
||||
files := []string{"launcher.yaml", "admin.yaml"}
|
||||
for _, f := range files {
|
||||
data, err := os.ReadFile(filepath.Join(basePath, f))
|
||||
if err != nil {
|
||||
continue
|
||||
|
||||
for _, lang := range langs {
|
||||
if translations[lang] == nil {
|
||||
translations[lang] = make(map[string]string)
|
||||
}
|
||||
var m map[string]string
|
||||
yaml.Unmarshal(data, &m)
|
||||
for k, v := range m {
|
||||
result[k] = v
|
||||
for _, file := range files {
|
||||
path := filepath.Join(lngDir, lang, file)
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
var keys map[string]string
|
||||
if err := yaml.Unmarshal(data, &keys); err != nil {
|
||||
continue
|
||||
}
|
||||
for k, v := range keys {
|
||||
translations[lang][k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if translationCache == nil {
|
||||
translationCache = make(map[string]Translations)
|
||||
}
|
||||
translationCache[lang] = result
|
||||
return result
|
||||
}
|
||||
|
||||
func T(lang, key string, args ...string) string {
|
||||
t := loadTranslations(lang)
|
||||
text, ok := t[key]
|
||||
if !ok {
|
||||
en := loadTranslations("en")
|
||||
text = en[key]
|
||||
if text == "" {
|
||||
return key
|
||||
}
|
||||
func t(lang, key string, args ...string) string {
|
||||
text := translations[lang][key]
|
||||
if text == "" {
|
||||
text = translations["en"][key]
|
||||
}
|
||||
if text == "" {
|
||||
return key
|
||||
}
|
||||
for i := 0; i < len(args); i += 2 {
|
||||
if i+1 < len(args) {
|
||||
text = strings.ReplaceAll(text, "{"+args[i]+"}", args[i+1])
|
||||
}
|
||||
text = strings.ReplaceAll(text, "{"+args[i]+"}", args[i+1])
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func getUserLang(r *http.Request) string {
|
||||
headerLang := r.Header.Get("Remote-Lang")
|
||||
if headerLang == "de" || headerLang == "en" {
|
||||
return headerLang
|
||||
lang := r.Header.Get("Accept-Language")
|
||||
if strings.HasPrefix(lang, "de") {
|
||||
return "de"
|
||||
}
|
||||
settings, _ := loadSettings(os.Getenv("CONFIG_DIR"))
|
||||
if settings != nil && settings.Company.Language == "de" {
|
||||
|
|
@ -620,9 +618,9 @@ const launcherHTML = `<!DOCTYPE html>
|
|||
{{if .Subtitle}}<p>{{.Subtitle}}</p>{{end}}
|
||||
</header>
|
||||
<div class="user-banner">
|
||||
<span>Welcome, {{.User}}</span>
|
||||
<a href="/settings">Settings</a>
|
||||
<a href="https://auth.nextwks.eu/logout">Logout</a>
|
||||
<span>{{t .Lang "welcome" "user" .User}}</span>
|
||||
<a href="/settings">{{t .Lang "settings"}}</a>
|
||||
<a href="https://auth.nextwks.eu/logout">{{t .Lang "logout"}}</a>
|
||||
</div>
|
||||
<div class="grid">
|
||||
{{range .Apps}}
|
||||
|
|
@ -897,15 +895,15 @@ const adminHTML = `<!DOCTYPE html>
|
|||
<div class="layout">
|
||||
<nav class="sidebar">
|
||||
<div class="section-label">Core Settings</div>
|
||||
<a href="#" class="active" data-section="global" onclick="return switchSection('global')"><span class="icon">⚙</span> Global</a>
|
||||
<a href="#" data-section="access" onclick="return switchSection('access')"><span class="icon">👥</span> Access</a>
|
||||
<a href="#" data-section="security" onclick="return switchSection('security')"><span class="icon">🔒</span> Security</a>
|
||||
<a href="#" data-section="domain" onclick="return switchSection('domain')"><span class="icon">🌐</span> Domain</a>
|
||||
<a href="#" class="active" data-section="global" onclick="return switchSection('global')"><span class="icon">⚙</span> {{t .Lang "nav_global"}}</a>
|
||||
<a href="#" data-section="access" onclick="return switchSection('access')"><span class="icon">👥</span> {{t .Lang "nav_access"}}</a>
|
||||
<a href="#" data-section="security" onclick="return switchSection('security')"><span class="icon">🔒</span> {{t .Lang "nav_security"}}</a>
|
||||
<a href="#" data-section="domain" onclick="return switchSection('domain')"><span class="icon">🌐</span> {{t .Lang "nav_domain"}}</a>
|
||||
|
||||
<div class="section-label">App Settings</div>
|
||||
<a href="#" class="disabled"><span class="icon">📦</span> Mail</a>
|
||||
<a href="#" class="disabled"><span class="icon">📄</span> Docs</a>
|
||||
<a href="#" class="disabled"><span class="icon">📅</span> Calendar</a>
|
||||
<a href="#" class="disabled"><span class="icon">📦</span> {{t .Lang "nav_mail"}}</a>
|
||||
<a href="#" class="disabled"><span class="icon">📄</span> {{t .Lang "nav_docs"}}</a>
|
||||
<a href="#" class="disabled"><span class="icon">📅</span> {{t .Lang "nav_calendar"}}</a>
|
||||
|
||||
<div class="spacer"></div>
|
||||
<div class="footer">NextWorkspace v0.1.0.0011</div>
|
||||
|
|
@ -1128,6 +1126,17 @@ func main() {
|
|||
companyName = settings.Company.Name
|
||||
}
|
||||
|
||||
// Load translations from lng/ directory
|
||||
lngDir := filepath.Join(configDir, "../../lng")
|
||||
loadTranslations(lngDir)
|
||||
|
||||
// Create template FuncMap for translations
|
||||
funcMap := template.FuncMap{"t": t}
|
||||
|
||||
// Parse templates with FuncMap
|
||||
launcherTmpl = template.Must(template.New("launcher").Funcs(funcMap).Parse(launcherHTML))
|
||||
adminTmpl = template.Must(template.New("admin").Funcs(funcMap).Parse(adminHTML))
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.Port)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
|
|
|||
Loading…
Reference in a new issue