瀏覽代碼

Allow to choose enabled languages

Fixes #1835

Signed-off-by: Nicola Murino <[email protected]>
Nicola Murino 10 月之前
父節點
當前提交
70fc00d7eb

+ 7 - 0
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

+ 9 - 0
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])

+ 6 - 0
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

+ 2 - 0
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,
 	}

+ 4 - 0
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
 }
 

+ 8 - 0
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)

+ 15 - 4
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)
 }

+ 1 - 0
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": "",

+ 30 - 18
templates/common/base.html

@@ -172,10 +172,17 @@ explicit grant from the SFTPGo Team ([email protected]).
         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 ([email protected]).
             .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 ([email protected]).
                     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();