Repository: spyder-ide/spyder

Issue:
How to change UI language
_From [mentat...@gmail.com](https://code.google.com/u/116256814944482205201/) on 2012-04-21T12:22:36Z_

Hi,

I'd like to change the UI language: default from OS is French in my computer, but I want it in English. I thought it was obvious, but I can't find the settings.

Any idea?
Thanks

_Original issue: http://code.google.com/p/spyderlib/issues/detail?id=1001_



Discussion:
_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2012-04-22T00:10:58Z_

Well, it's not so common for people to choose a language different from their locale: that's why language selection is automatic in Spyder.

However, you may override this behavior by defining a standard variable environment (and this should also change the UI language of any installed software using GNU gettext):
LANG=en_US

**Status:** Done  

_From [ajit....@gmail.com](https://code.google.com/u/103127063444650459571/) on 2014-08-11T08:22:03Z_

Hi,

I too have the same problem.I installed Python(x,y) on windows. I want to change UI language from Spanish to English and I cannot find the location of LANG.

Any help ?

Thanks
Ajit

_From [ajit....@gmail.com](https://code.google.com/u/103127063444650459571/) on 2014-08-11T08:32:13Z_

Hi,

I was able to locate language and region and made changes to UK. Things worked.

_From [ccordoba12](https://code.google.com/u/ccordoba12/) on 2014-08-11T17:20:44Z_

How do you do it?

Tools-->Current user environment variables-->OK-->Right click-->Insert-->Key-->en-US-->OK.
Exit, restart.

This was asked several times, so we need an option in Preferences to do it.

As soon as the restart #2312 is merged I can take this one, now it would be friendly to ask for a restart.

Good! Please add this to

```
Preferences > General
```

:+1: 


Resolving patch:
diff --git a/spyderlib/baseconfig.py b/spyderlib/baseconfig.py
--- a/spyderlib/baseconfig.py
+++ b/spyderlib/baseconfig.py
@@ -14,6 +14,8 @@
 
 from __future__ import print_function
 
+import codecs
+import locale
 import os.path as osp
 import os
 import sys
@@ -39,7 +41,8 @@
 #==============================================================================
 # Debug helpers
 #==============================================================================
-STDOUT = sys.stdout
+# This is needed after restarting and using debug_print
+STDOUT = sys.stdout if PY3 else codecs.getwriter('utf-8')(sys.stdout)
 STDERR = sys.stderr
 def _get_debug_env():
     debug_env = os.environ.get('SPYDER_DEBUG', '')
@@ -52,7 +55,13 @@ def debug_print(*message):
     """Output debug messages to stdout"""
     if DEBUG:
         ss = STDOUT
-        print(*message, file=ss)
+        if PY3:
+            # This is needed after restarting and using debug_print
+            for m in message:
+                ss.buffer.write(str(m).encode('utf-8'))
+            print('', file=ss)
+        else:
+            print(*message, file=ss)
 
 #==============================================================================
 # Configuration paths
@@ -204,18 +213,105 @@ def get_image_path(name, default="not_found.png"):
 #==============================================================================
 # Translations
 #==============================================================================
+LANG_FILE = get_conf_path('langconfig')
+DEFAULT_LANGUAGE = 'en'
+
+# This needs to be updated every time a new language is added to spyder, and is
+# used by the Preferences configuration to populate the Language QComboBox
+LANGUAGE_CODES = {'en': u'English',
+                  'fr': u'Français',
+                  'es': u'Español',
+                  'pt_BR': u'Português'
+                  }
+
+
+def get_available_translations():
+    """
+    List available translations for spyder based on the folders found in the
+    locale folder. This function checks if LANGUAGE_CODES contain the same
+    information that is found in the 'locale' folder to ensure that when a new
+    language is added, LANGUAGE_CODES is updated.
+    """
+    locale_path = get_module_data_path("spyderlib", relpath="locale",
+                                       attr_name='LOCALEPATH')
+    listdir = os.listdir(locale_path)
+    langs = [d for d in listdir if osp.isdir(osp.join(locale_path, d))]
+    langs = [DEFAULT_LANGUAGE] + langs
+
+    # Check that there is a language code available in case a new translation
+    # is added, to ensure LANGUAGE_CODES is updated.
+    for lang in langs:
+        if lang not in LANGUAGE_CODES:
+            error = _('Update LANGUAGE_CODES (inside baseconfig.py) if a new '
+                      'translation has been added to Spyder')
+            raise Exception(error)
+    return langs
+
+
+def get_interface_language():
+    """
+    If Spyder has a translation available for the locale language, it will
+    return the version provided by Spyder adjusted for language subdifferences,
+    otherwise it will return DEFAULT_LANGUAGE.
+
+    Example:
+    1.) Spyder provides ('en',  'fr', 'es' and 'pt_BR'), if the locale is
+    either 'en_US' or 'en' or 'en_UK', this function will return 'en'
+
+    2.) Spyder provides ('en',  'fr', 'es' and 'pt_BR'), if the locale is
+    either 'pt' or 'pt_BR', this function will return 'pt_BR'
+    """
+    locale_language = locale.getdefaultlocale()[0]
+
+    if locale_language is None:
+        language = DEFAULT_LANGUAGE
+    else:
+        spyder_languages = get_available_translations()
+        for lang in spyder_languages:
+            if locale_language == lang:
+                language = locale_language
+                break
+            elif locale_language.startswith(lang) or \
+              lang.startswith(locale_language):
+                language = lang
+                break
+
+    return language
+
+
+def save_lang_conf(value):
+    """Save language setting to language config file"""
+    with open(LANG_FILE, 'w') as f:
+        f.write(value)
+
+
+def load_lang_conf():
+    """
+    Load language setting from language config file if it exists, otherwise
+    try to use the local settings if Spyder provides a translation, or
+    return the default if no translation provided.
+    """
+    if osp.isfile(LANG_FILE):
+        with open(LANG_FILE, 'r') as f:
+            lang = f.read()
+    else:
+        lang = get_interface_language()
+        save_lang_conf(lang)
+    return lang
+
+
 def get_translation(modname, dirname=None):
     """Return translation callback for module *modname*"""
     if dirname is None:
         dirname = modname
     locale_path = get_module_data_path(dirname, relpath="locale",
                                        attr_name='LOCALEPATH')
-    # fixup environment var LANG in case it's unknown
-    if "LANG" not in os.environ:
-        import locale
-        lang = locale.getdefaultlocale()[0]
-        if lang is not None:
-            os.environ["LANG"] = lang
+
+    # fixup environment var LANG, LANGUAGE
+    language = load_lang_conf()
+    os.environ["LANG"] = language      # Works on Windows
+    os.environ["LANGUAGE"] = language  # Works on Linux
+
     import gettext
     try:
         _trans = gettext.translation(modname, locale_path, codeset="utf-8")
diff --git a/spyderlib/plugins/configdialog.py b/spyderlib/plugins/configdialog.py
--- a/spyderlib/plugins/configdialog.py
+++ b/spyderlib/plugins/configdialog.py
@@ -21,7 +21,8 @@
 from spyderlib.qt.compat import (to_qvariant, from_qvariant,
                                  getexistingdirectory, getopenfilename)
 
-from spyderlib.baseconfig import _, running_in_mac_app
+from spyderlib.baseconfig import (_, running_in_mac_app, LANGUAGE_CODES,
+                                  save_lang_conf, load_lang_conf)
 from spyderlib.config import CONF
 from spyderlib.guiconfig import (CUSTOM_COLOR_SCHEME_NAME,
                                  set_default_color_scheme)
@@ -90,8 +91,21 @@ def apply_changes(self):
             self.save_to_conf()
             if self.apply_callback is not None:
                 self.apply_callback()
+
+            # Since the language cannot be retrieved by CONF and the language
+            # is needed before loading CONF, this is an extra method needed to
+            # ensure that when changes are applied, they are copied to a
+            # specific file storing the language value. This only applies to
+            # the main section config.
+            if self.CONF_SECTION == u'main':
+                self._save_lang()
+
+            for restart_option in self.restart_options:
+                if restart_option in self.changed_options:
+                    self.prompt_restart_required()
+                    break  # Ensure a single popup is displayed
             self.set_modified(False)
-    
+
     def load_from_conf(self):
         """Load settings from configuration file"""
         raise NotImplementedError
@@ -151,6 +165,9 @@ def __init__(self, parent=None):
 
         self.setWindowTitle(_("Preferences"))
         self.setWindowIcon(get_icon("configure.png"))
+
+        # Ensures that the config is present on spyder first run
+        CONF.set('main', 'interface_language', load_lang_conf())
         
     def get_current_index(self):
         """Return current page index"""
@@ -238,6 +255,7 @@ def __init__(self, parent):
         self.coloredits = {}
         self.scedits = {}
         self.changed_options = set()
+        self.restart_options = dict()  # Dict to store name and localized text
         self.default_button_group = None
         
     def apply_settings(self, options):
@@ -301,6 +319,9 @@ def load_from_conf(self):
             combobox.setCurrentIndex(index)
             combobox.currentIndexChanged.connect(lambda _foo, opt=option:
                                                  self.has_been_modified(opt))
+            if combobox.restart_required:
+                self.restart_options[option] = combobox.label_text
+
         for (fontbox, sizebox), option in list(self.fontboxes.items()):
             font = self.get_font(option)
             fontbox.setCurrentFont(font)
@@ -344,7 +365,7 @@ def load_from_conf(self):
             else:
                 cb_italic.clicked.connect(lambda opt=option:
                                           self.has_been_modified(opt))
-    
+
     def save_to_conf(self):
         """Save settings to configuration file"""
         for checkbox, (option, _default) in list(self.checkboxes.items()):
@@ -374,7 +395,7 @@ def save_to_conf(self):
     def has_been_modified(self, option):
         self.set_modified(True)
         self.changed_options.add(option)
-    
+
     def create_checkbox(self, text, option, default=NoDefault,
                         tip=None, msg_warning=None, msg_info=None,
                         msg_if_enabled=False):
@@ -579,7 +600,7 @@ def create_scedit(self, text, option, default=NoDefault, tip=None,
         return widget
     
     def create_combobox(self, text, choices, option, default=NoDefault,
-                        tip=None):
+                        tip=None, restart=False):
         """choices: couples (name, key)"""
         label = QLabel(text)
         combobox = QComboBox()
@@ -595,6 +616,8 @@ def create_combobox(self, text, choices, option, default=NoDefault,
         layout.setContentsMargins(0, 0, 0, 0)
         widget = QWidget(self)
         widget.setLayout(layout)
+        combobox.restart_required = restart
+        combobox.label_text = text
         return widget
     
     def create_fontgroup(self, option=None, text=None,
@@ -661,13 +684,42 @@ def get_icon(self):
     def apply_settings(self, options):
         raise NotImplementedError
 
+    def prompt_restart_required(self):
+        """Prompt the user with a request to restart."""
+        restart_opts = self.restart_options
+        changed_opts = self.changed_options
+        options = [restart_opts[o] for o in changed_opts if o in restart_opts]
+
+        if len(options) == 1:
+            msg_start = _("Spyder needs to restart to change the following "
+                          "setting:")
+        else:
+            msg_start = _("Spyder needs to restart to change the following "
+                          "settings:")
+        msg_end = _("Do you wish to restart now?")
+
+        msg_options = ""
+        for option in options:
+            msg_options += "<li>{0}</li>".format(option)
+
+        msg_title = _("Information")
+        msg = "{0}<ul>{1}</ul><br>{2}".format(msg_start, msg_options, msg_end)
+        answer = QMessageBox.information(self, msg_title, msg,
+                                         QMessageBox.Yes | QMessageBox.No)
+        if answer == QMessageBox.Yes:
+            self.restart()
+
+    def restart(self):
+        """Restart Spyder."""
+        self.main.restart()
+
 
 class MainConfigPage(GeneralConfigPage):
     CONF_SECTION = "main"
     
     NAME = _("General")
     ICON = "genprefs.png"
-    
+
     def setup_page(self):
         newcb = self.create_checkbox
 
@@ -704,6 +756,12 @@ def setup_page(self):
         margins_layout.addWidget(margin_spin)
         prompt_box = newcb(_("Prompt when exiting"), 'prompt_on_exit')
 
+        # Language chooser
+        choices = sorted([(val, key) for key, val in LANGUAGE_CODES.items()])
+        language_combo = self.create_combobox(_('Language'), choices,
+                                              'interface_language',
+                                              restart=True)
+
         # Decide if it's possible to activate or not singie instance mode
         if running_in_mac_app():
             self.set_option("single_instance", True)
@@ -711,6 +769,7 @@ def setup_page(self):
 
         interface_layout = QVBoxLayout()
         interface_layout.addWidget(style_combo)
+        interface_layout.addWidget(language_combo)
         interface_layout.addWidget(single_instance_box)
         interface_layout.addWidget(vertdock_box)
         interface_layout.addWidget(verttabs_box)
@@ -777,6 +836,18 @@ def setup_page(self):
     def apply_settings(self, options):
         self.main.apply_settings()
 
+    def _save_lang(self):
+        """
+        Get selected language setting and save to language configuration file.
+        """
+        for combobox, (option, _default) in list(self.comboboxes.items()):
+            if option == 'interface_language':
+                data = combobox.itemData(combobox.currentIndex())
+                value = from_qvariant(data, to_text_string)
+                break
+        save_lang_conf(value)
+        self.set_option('interface_language', value)
+
 
 class ColorSchemeConfigPage(GeneralConfigPage):
     CONF_SECTION = "color_schemes"
diff --git a/spyderlib/utils/iofuncs.py b/spyderlib/utils/iofuncs.py
--- a/spyderlib/utils/iofuncs.py
+++ b/spyderlib/utils/iofuncs.py
@@ -362,7 +362,8 @@ def load_dictionary(filename):
                       'spyder.ini', 'temp.py', 'temp.spydata', 'template.py',
                       'history.py', 'history_internal.py', 'workingdir',
                       '.projects', '.spyderproject', '.ropeproject',
-                      'monitor.log', 'monitor_debug.log', 'rope.log')
+                      'monitor.log', 'monitor_debug.log', 'rope.log',
+                      'langconfig')
 
 def reset_session():
     """Remove all config files"""


Test patch: