Repository: privacyidea/privacyidea

Issue:
How Change Languages
i modified 
/opt/privacyidea/lib/python3.8/site-packages/privacyidea/app.py
![image](https://user-images.githubusercontent.com/46963951/161051848-24030262-e35a-4a0d-9d62-edec00dc1936.png)
/opt/privacyidea/lib/python3.8/site-packages/privacyidea/webui/login.py
![image](https://user-images.githubusercontent.com/46963951/161051922-2f5fd029-675f-446e-98af-ad6342b6bdcb.png)
but I have also tried with several browsers, in all the language settings are zh_Hant and in all of them the pages are shown in English


Discussion:
Thank you for filing an issue and sharing your observations or ideas. Please be sure to provide as many information as possible to help us working on this issue.
up
I think you should put "zh_Hans/t" in front of "en". Otherwise your browser could be fine with "en".

Resolving patch:
diff --git a/privacyidea/app.py b/privacyidea/app.py
--- a/privacyidea/app.py
+++ b/privacyidea/app.py
@@ -45,7 +45,7 @@
 from privacyidea.api.caconnector import caconnector_blueprint
 from privacyidea.api.register import register_blueprint
 from privacyidea.api.auth import jwtauth
-from privacyidea.webui.login import login_blueprint
+from privacyidea.webui.login import login_blueprint, get_accepted_language
 from privacyidea.webui.certificate import cert_blueprint
 from privacyidea.api.machineresolver import machineresolver_blueprint
 from privacyidea.api.machine import machine_blueprint
@@ -218,15 +218,7 @@ def create_app(config_name="development",
 
     @babel.localeselector
     def get_locale():
-        # if we are not in the request context, return None to use the default
-        # locale
-        if not request:
-            return None
-        # otherwise try to guess the language from the user accept
-        # header the browser transmits.  We support de/fr/en in this
-        # example.  The best match wins.
-        return request.accept_languages.best_match(['de', 'nl', 'fr', 'cs',
-                                                    'es', 'en', 'tr', 'zh_Hant'])
+        return get_accepted_language(request)
 
     queue.register_app(app)
 
diff --git a/privacyidea/webui/login.py b/privacyidea/webui/login.py
--- a/privacyidea/webui/login.py
+++ b/privacyidea/webui/login.py
@@ -34,6 +34,7 @@
                    current_app, g)
 from privacyidea.api.lib.utils import send_html
 from privacyidea.api.lib.prepolicy import is_remote_user_allowed
+from privacyidea.lib.framework import get_app_config_value
 from privacyidea.lib.passwordreset import is_password_reset
 from privacyidea.lib.error import HSMException
 from privacyidea.lib.realm import get_realms
@@ -44,10 +45,21 @@
 from privacyidea.lib.queue import has_job_queue
 
 DEFAULT_THEME = "/static/contrib/css/bootstrap-theme.css"
+DEFAULT_LANGUAGE_LIST = ['en', 'de', 'nl', 'zh_Hant', 'fr', 'es', 'tr']
 
 login_blueprint = Blueprint('login_blueprint', __name__)
 
 
+def get_accepted_language(req):
+    # if we are not in the request context, return None to use the default locale
+    if not req:
+        return None
+    # read pi.cfg and checks if preferred language is set. Otherwise, default list is selected.
+    pi_lang_list = get_app_config_value("PI_PREFERRED_LANGUAGE", default=DEFAULT_LANGUAGE_LIST)
+    # try to match the language from the users accept header the browser transmits.
+    # (The best match wins)
+    return req.accept_languages.best_match(pi_lang_list, default=pi_lang_list[0])
+
 @login_blueprint.before_request
 def before_request():
     """
@@ -90,7 +102,7 @@ def single_page_application():
     translation_warning = current_app.config.get("PI_TRANSLATION_WARNING", False)
     # Get the logo file
     logo = current_app.config.get("PI_LOGO", "privacyIDEA1.png")
-    browser_lang = request.accept_languages.best_match(["en", "de", "de-DE", "nl", "fr", "cs", "it", "es", "tr"], default="en").split("-")[0]
+    browser_lang = get_accepted_language(request)
     # The page title can be configured in pi.cfg
     page_title = current_app.config.get("PI_PAGE_TITLE", "privacyIDEA Authentication System")
     # check if login with REMOTE_USER is allowed.
@@ -100,7 +112,7 @@ def single_page_application():
         request.all_data = {}
     # Depending on displaying the realm dropdown, we fill realms or not.
     realms = ""
-    realm_dropdown = Match.action_only(g, scope=SCOPE.WEBUI, action=ACTION.REALMDROPDOWN)\
+    realm_dropdown = Match.action_only(g, scope=SCOPE.WEBUI, action=ACTION.REALMDROPDOWN) \
         .policies(write_to_audit_log=False)
     show_node = get_privacyidea_node() \
         if Match.generic(g, scope=SCOPE.WEBUI, action=ACTION.SHOW_NODE).any(write_to_audit_log=False) else ""
@@ -129,7 +141,7 @@ def single_page_application():
     # and baseline. get_action_values returns an array!
     sub_state = subscription_status()
     customization_menu_file = Match.action_only(g, action=ACTION.CUSTOM_MENU,
-                                                scope=SCOPE.WEBUI)\
+                                                scope=SCOPE.WEBUI) \
         .action_values(unique=True, allow_white_space_in_action=True, write_to_audit_log=False)
     if len(customization_menu_file) and list(customization_menu_file)[0] \
             and sub_state not in [1, 2]:


Test patch:
diff --git a/tests/test_ui_login.py b/tests/test_ui_login.py
--- a/tests/test_ui_login.py
+++ b/tests/test_ui_login.py
@@ -5,7 +5,7 @@
 
 implementation is contained webui/login.py
 """
-from .base import MyTestCase
+from .base import MyTestCase, MyApiTestCase
 from privacyidea.lib.policy import set_policy, SCOPE, ACTION, PolicyClass, delete_all_policies
 from privacyidea.lib.utils import to_unicode
 import re
@@ -117,4 +117,25 @@ def test_07_privacy_statement_link(self):
                                            method='GET'):
             res = self.app.full_dispatch_request()
             self.assertTrue(res.status_code == 200, res)
-            self.assertTrue(b"https://privacyidea.org/" in res.data)
\ No newline at end of file
+            self.assertTrue(b"https://privacyidea.org/" in res.data)
+
+
+class LanguageTestCase(MyApiTestCase):
+
+    def test_01_check_for_english_translation(self):
+        with self.app.test_request_context('/auth/rights',
+                                           method='GET',
+                                           headers={
+                                               'Authorization': self.at,
+                                               'Accept-Language': 'en'}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.json['result']['value']['totp'], 'TOTP: Time based One Time Passwords.')
+
+    def test_02_check_for_german_translation(self):
+        with self.app.test_request_context('/auth/rights',
+                                           method='GET',
+                                           headers={
+                                               'Authorization': self.at,
+                                               'Accept-Language': 'de'}):
+            res = self.app.full_dispatch_request()
+            self.assertEqual(res.json['result']['value']['totp'], 'TOTP: Zeitbasiertes Einmalpasswort.')