From 70fc00d7eb2cf77564a07d6d0b664f71579651b9 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 19 Dec 2024 19:50:19 +0100 Subject: [PATCH] Allow to choose enabled languages Fixes #1835 Signed-off-by: Nicola Murino --- internal/config/config.go | 7 +++++ internal/config/config_test.go | 9 +++++++ internal/httpd/httpd.go | 6 +++++ internal/httpd/server.go | 2 ++ internal/httpd/web.go | 4 +++ internal/httpd/webadmin.go | 8 ++++++ internal/httpd/webclient.go | 19 +++++++++++--- sftpgo.json | 1 + templates/common/base.html | 48 +++++++++++++++++++++------------- 9 files changed, 82 insertions(+), 22 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index cf7aa871..137daee0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -118,6 +118,7 @@ var ( ClientIPHeaderDepth: 0, HideLoginURL: 0, RenderOpenAPI: true, + Languages: []string{"en"}, OIDC: httpd.OIDC{ ClientID: "", ClientSecret: "", @@ -1853,6 +1854,12 @@ func getHTTPDBindingFromEnv(idx int) { //nolint:gocyclo isSet = true } + languages, ok := lookupStringListFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%d__LANGUAGES", idx)) + if ok { + binding.Languages = languages + isSet = true + } + enableHTTPS, ok := lookupBoolFromEnv(fmt.Sprintf("SFTPGO_HTTPD__BINDINGS__%v__ENABLE_HTTPS", idx)) if ok { binding.EnableHTTPS = enableHTTPS diff --git a/internal/config/config_test.go b/internal/config/config_test.go index a9965aa9..5052bba3 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -1173,6 +1173,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API", "0") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS", "3") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI", "0") + os.Setenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES", "en,es") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_HTTPS", "1 ") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__MIN_TLS_VERSION", "13") os.Setenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE", "1") @@ -1241,6 +1242,7 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLE_REST_API") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__ENABLED_LOGIN_METHODS") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__RENDER_OPENAPI") + os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__LANGUAGES") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__CLIENT_AUTH_TYPE") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_CIPHER_SUITES") os.Unsetenv("SFTPGO_HTTPD__BINDINGS__2__TLS_PROTOCOLS") @@ -1302,6 +1304,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.True(t, bindings[0].EnableRESTAPI) require.Equal(t, 0, bindings[0].EnabledLoginMethods) require.True(t, bindings[0].RenderOpenAPI) + require.Len(t, bindings[0].Languages, 1) + assert.Contains(t, bindings[0].Languages, "en") require.Len(t, bindings[0].TLSCipherSuites, 1) require.Equal(t, 0, bindings[0].ProxyMode) require.Empty(t, bindings[0].OIDC.ConfigURL) @@ -1321,6 +1325,8 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.True(t, bindings[1].EnableRESTAPI) require.Equal(t, 0, bindings[1].EnabledLoginMethods) require.True(t, bindings[1].RenderOpenAPI) + require.Len(t, bindings[1].Languages, 1) + assert.Contains(t, bindings[1].Languages, "en") require.Nil(t, bindings[1].TLSCipherSuites) require.Equal(t, 1, bindings[1].HideLoginURL) require.Empty(t, bindings[1].OIDC.ClientID) @@ -1341,6 +1347,9 @@ func TestHTTPDBindingsFromEnv(t *testing.T) { require.False(t, bindings[2].EnableRESTAPI) require.Equal(t, 3, bindings[2].EnabledLoginMethods) require.False(t, bindings[2].RenderOpenAPI) + require.Len(t, bindings[2].Languages, 2) + assert.Contains(t, bindings[2].Languages, "en") + assert.Contains(t, bindings[2].Languages, "es") require.Equal(t, 1, bindings[2].ClientAuthType) require.Len(t, bindings[2].TLSCipherSuites, 2) require.Equal(t, "TLS_AES_256_GCM_SHA384", bindings[2].TLSCipherSuites[0]) diff --git a/internal/httpd/httpd.go b/internal/httpd/httpd.go index dfdb1863..a50b47c7 100644 --- a/internal/httpd/httpd.go +++ b/internal/httpd/httpd.go @@ -608,6 +608,8 @@ type Binding struct { HideLoginURL int `json:"hide_login_url" mapstructure:"hide_login_url"` // Enable the built-in OpenAPI renderer RenderOpenAPI bool `json:"render_openapi" mapstructure:"render_openapi"` + // Languages defines the list of enabled translations for the WebAdmin and WebClient UI. + Languages []string `json:"languages" mapstructure:"languages"` // Defining an OIDC configuration the web admin and web client UI will use OpenID to authenticate users. OIDC OIDC `json:"oidc" mapstructure:"oidc"` // Security defines security headers to add to HTTP responses and allows to restrict allowed hosts @@ -642,6 +644,10 @@ func (b *Binding) webClientBranding() UIBranding { return dbBrandingConfig.mergeBrandingConfig(b.Branding.WebClient, true) } +func (b *Binding) languages() []string { + return b.Languages +} + func (b *Binding) parseAllowedProxy() error { if filepath.IsAbs(b.Address) && len(b.ProxyAllowed) > 0 { // unix domain socket diff --git a/internal/httpd/server.go b/internal/httpd/server.go index 51962844..fc030355 100644 --- a/internal/httpd/server.go +++ b/internal/httpd/server.go @@ -177,6 +177,7 @@ func (s *httpdServer) renderClientLoginPage(w http.ResponseWriter, r *http.Reque Error: err, CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseClientPath), Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), FormDisabled: s.binding.isWebClientLoginFormDisabled(), CheckRedirect: true, } @@ -595,6 +596,7 @@ func (s *httpdServer) renderAdminLoginPage(w http.ResponseWriter, r *http.Reques Error: err, CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseAdminPath), Branding: s.binding.webAdminBranding(), + Languages: s.binding.languages(), FormDisabled: s.binding.isWebAdminLoginFormDisabled(), CheckRedirect: false, } diff --git a/internal/httpd/web.go b/internal/httpd/web.go index 35b2cd7b..a14ba497 100644 --- a/internal/httpd/web.go +++ b/internal/httpd/web.go @@ -67,6 +67,7 @@ type loginPage struct { OpenIDLoginURL string Title string Branding UIBranding + Languages []string FormDisabled bool CheckRedirect bool } @@ -79,6 +80,7 @@ type twoFactorPage struct { RecoveryURL string Title string Branding UIBranding + Languages []string CheckRedirect bool } @@ -90,6 +92,7 @@ type forgotPwdPage struct { LoginURL string Title string Branding UIBranding + Languages []string CheckRedirect bool } @@ -101,6 +104,7 @@ type resetPwdPage struct { LoginURL string Title string Branding UIBranding + Languages []string CheckRedirect bool } diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index fdbc58b8..0a1cbc34 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -155,6 +155,7 @@ type basePage struct { LoggedUser *dataprovider.Admin IsLoggedToShare bool Branding UIBranding + Languages []string } type statusPage struct { @@ -262,6 +263,7 @@ type setupPage struct { HideSupportLink bool Title string Branding UIBranding + Languages []string CheckRedirect bool } @@ -669,6 +671,7 @@ func (s *httpdServer) getBasePageData(title, currentURL string, w http.ResponseW HasExternalLogin: isLoggedInWithOIDC(r), CSRFToken: csrfToken, Branding: s.binding.webAdminBranding(), + Languages: s.binding.languages(), } } @@ -727,6 +730,7 @@ func (s *httpdServer) renderForgotPwdPage(w http.ResponseWriter, r *http.Request LoginURL: webAdminLoginPath, Title: util.I18nForgotPwdTitle, Branding: s.binding.webAdminBranding(), + Languages: s.binding.languages(), } renderAdminTemplate(w, templateForgotPassword, data) } @@ -740,6 +744,7 @@ func (s *httpdServer) renderResetPwdPage(w http.ResponseWriter, r *http.Request, LoginURL: webAdminLoginPath, Title: util.I18nResetPwdTitle, Branding: s.binding.webAdminBranding(), + Languages: s.binding.languages(), } renderAdminTemplate(w, templateResetPassword, data) } @@ -753,6 +758,7 @@ func (s *httpdServer) renderTwoFactorPage(w http.ResponseWriter, r *http.Request CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath), RecoveryURL: webAdminTwoFactorRecoveryPath, Branding: s.binding.webAdminBranding(), + Languages: s.binding.languages(), } renderAdminTemplate(w, templateTwoFactor, data) } @@ -765,6 +771,7 @@ func (s *httpdServer) renderTwoFactorRecoveryPage(w http.ResponseWriter, r *http Error: err, CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseAdminPath), Branding: s.binding.webAdminBranding(), + Languages: s.binding.languages(), } renderAdminTemplate(w, templateTwoFactorRecovery, data) } @@ -863,6 +870,7 @@ func (s *httpdServer) renderAdminSetupPage(w http.ResponseWriter, r *http.Reques HideSupportLink: hideSupportLink, Error: err, Branding: s.binding.webAdminBranding(), + Languages: s.binding.languages(), } renderAdminTemplate(w, templateSetup, data) diff --git a/internal/httpd/webclient.go b/internal/httpd/webclient.go index ffb3a1b3..e1363695 100644 --- a/internal/httpd/webclient.go +++ b/internal/httpd/webclient.go @@ -98,6 +98,7 @@ type baseClientPage struct { LoggedUser *dataprovider.User IsLoggedToShare bool Branding UIBranding + Languages []string } type dirMapping struct { @@ -107,9 +108,10 @@ type dirMapping struct { type viewPDFPage struct { commonBasePage - Title string - URL string - Branding UIBranding + Title string + URL string + Branding UIBranding + Languages []string } type editFilePage struct { @@ -152,6 +154,7 @@ type shareLoginPage struct { CSRFToken string Title string Branding UIBranding + Languages []string } type shareDownloadPage struct { @@ -550,6 +553,7 @@ func (s *httpdServer) getBaseClientPageData(title, currentURL string, w http.Res LoggedUser: getUserFromToken(r), IsLoggedToShare: false, Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), } if !strings.HasPrefix(r.RequestURI, webClientPubSharesPath) { data.LoginURL = webClientLoginPath @@ -566,6 +570,7 @@ func (s *httpdServer) renderClientForgotPwdPage(w http.ResponseWriter, r *http.R LoginURL: webClientLoginPath, Title: util.I18nForgotPwdTitle, Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), } renderClientTemplate(w, templateForgotPassword, data) } @@ -579,6 +584,7 @@ func (s *httpdServer) renderClientResetPwdPage(w http.ResponseWriter, r *http.Re LoginURL: webClientLoginPath, Title: util.I18nResetPwdTitle, Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), } renderClientTemplate(w, templateResetPassword, data) } @@ -591,6 +597,7 @@ func (s *httpdServer) renderShareLoginPage(w http.ResponseWriter, r *http.Reques Error: err, CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, xid.New().String(), webBaseClientPath), Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), } renderClientTemplate(w, templateShareLogin, data) } @@ -641,6 +648,7 @@ func (s *httpdServer) renderClientTwoFactorPage(w http.ResponseWriter, r *http.R CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath), RecoveryURL: webClientTwoFactorRecoveryPath, Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), } if next := r.URL.Query().Get("next"); strings.HasPrefix(next, webClientFilesPath) { data.CurrentURL += "?next=" + url.QueryEscape(next) @@ -656,6 +664,7 @@ func (s *httpdServer) renderClientTwoFactorRecoveryPage(w http.ResponseWriter, r Error: err, CSRFToken: createCSRFToken(w, r, s.csrfTokenAuth, "", webBaseClientPath), Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), } renderClientTemplate(w, templateTwoFactorRecovery, data) } @@ -1110,7 +1119,8 @@ func (s *httpdServer) handleShareViewPDF(w http.ResponseWriter, r *http.Request) Title: path.Base(name), URL: fmt.Sprintf("%s?path=%s&_=%d", path.Join(webClientPubSharesPath, share.ShareID, "getpdf"), url.QueryEscape(name), time.Now().UTC().Unix()), - Branding: s.binding.webClientBranding(), + Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), } renderClientTemplate(w, templateClientViewPDF, data) } @@ -1795,6 +1805,7 @@ func (s *httpdServer) handleClientViewPDF(w http.ResponseWriter, r *http.Request Title: path.Base(name), URL: fmt.Sprintf("%s?path=%s&_=%d", webClientGetPDFPath, url.QueryEscape(name), time.Now().UTC().Unix()), Branding: s.binding.webClientBranding(), + Languages: s.binding.languages(), } renderClientTemplate(w, templateClientViewPDF, data) } diff --git a/sftpgo.json b/sftpgo.json index a5c6e2c8..94a14849 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -285,6 +285,7 @@ "client_ip_header_depth": 0, "hide_login_url": 0, "render_openapi": true, + "languages": ["en"], "oidc": { "client_id": "", "client_secret": "", diff --git a/templates/common/base.html b/templates/common/base.html index 580b7f8a..da444d54 100644 --- a/templates/common/base.html +++ b/templates/common/base.html @@ -172,10 +172,17 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). element.setAttribute("data-kt-initialized", "1"); } - const lngs = { - en: { nativeName: 'English' }, - it: { nativeName: 'Italiano' } - }; + const lngs = {}; + //{{- range .Languages}} + //{{- if eq . "en"}} + lngs.en = { nativeName: 'English' }; + //{{- else if eq . "it" }} + lngs.it = { nativeName: 'Italiano' }; + //{{- end}} + //{{- end}} + if (Object.keys(lngs).length == 0){ + lngs.en = { nativeName: 'English' }; + } const renderI18n = () => { document.documentElement.setAttribute('lang', i18next.resolvedLanguage); @@ -194,7 +201,7 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). .init({ debug: false, supportedLngs: Object.keys(lngs), - fallbackLng: 'en', + fallbackLng: Object.keys(lngs)[0], load: 'languageOnly', backend: { backends: [ @@ -216,20 +223,25 @@ explicit grant from the SFTPGo Team (support@sftpgo.com). jqueryI18next.init(i18next, $, { useOptionsAttr: true }); var languageSwitcher = $('#languageSwitcher'); - if (languageSwitcher){ - Object.keys(lngs).map((lng) => { - const opt = new Option(lngs[lng].nativeName, lng); - if (lng === i18next.resolvedLanguage) { - opt.setAttribute("selected", "selected"); - } - languageSwitcher.append(opt); - }); - languageSwitcher.on('change', function(){ - const chosenLng = $(this).find("option:selected").attr('value'); - i18next.changeLanguage(chosenLng, () => { - renderI18n(); + if (languageSwitcher.length){ + if (Object.keys(lngs).length > 1) { + languageSwitcher.removeClass("d-none"); + Object.keys(lngs).map((lng) => { + const opt = new Option(lngs[lng].nativeName, lng); + if (lng === i18next.resolvedLanguage) { + opt.setAttribute("selected", "selected"); + } + languageSwitcher.append(opt); }); - }); + languageSwitcher.on('change', function () { + const chosenLng = $(this).find("option:selected").attr('value'); + i18next.changeLanguage(chosenLng, () => { + renderI18n(); + }); + }); + } else { + languageSwitcher.addClass("d-none"); + } } renderI18n();