Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for language selection in preferences #2349

Merged
merged 12 commits into from
May 29, 2015
112 changes: 104 additions & 8 deletions spyderlib/baseconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

from __future__ import print_function

import codecs
import locale
import os.path as osp
import os
import sys
Expand All @@ -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', '')
Expand All @@ -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
Expand Down Expand Up @@ -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():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we need a function for this ;-)

There are so few translations (and none on the works), that we can just create the list of langs by hand here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably we do not need it now, but it is a harmless function, and it provides a check when adding a new language to make sure things are done properly. I think we should leave it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, you're right :-)

If you want more translations, you should look at https://www.transifex.com/

I don't know exactly how it works, but I think we need to upload our pot files and volunteers help to translate them to their native languages :-)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will take a look a transifex, I had seen it before, but never got the time to go in depth

"""
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():
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It also seems like too much to have this function

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it might be too much, but why doing stuff by hand, if a function can handle it?

I would really like to have many more translations

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, now that it's written, we should leave it :-)

"""
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")
Expand Down
3 changes: 2 additions & 1 deletion spyderlib/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
# Local import
from spyderlib.userconfig import UserConfig
from spyderlib.baseconfig import (CHECK_ALL, EXCLUDED_NAMES, SUBFOLDER,
get_home_dir, _)
get_home_dir, _, load_lang_conf)
from spyderlib.utils import iofuncs, codeanalysis


Expand Down Expand Up @@ -169,6 +169,7 @@ def is_ubuntu():
'animated_docks': True,
'prompt_on_exit': False,
'panes_locked': True,
'interface_language': load_lang_conf(),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need to add this here because this is dependent on the user system.

The only options that we add in config.py are those that can be changed and/or updated in the future (by bumping CONF_VERSION).

That's why (for example) we don't add here the window size or state.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another example of things we don't add are the run configuration options per file :-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this option from here to finally merge this PR :-)

If I understand the code correctly:

  1. It'll be saved to spyder.ini by the save_lang method of MainConfigPage
  2. It can't be retrieved by CONF directly.

So there's no need to have it here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ccordoba12

If I don't add that method to the config, Spyder will not be able to load the preferences after a reset, cause there will be no interface_language setting in the .ini file.

Suggestions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should leave it as it is, otherwise we would have to made unneded changes either to how the preferences are loaded or do an extra check in spyder.py to see if this is the first time it runs.... here it is just one line of code...

'window/size': (1260, 740),
'window/position': (10, 10),
'window/is_maximized': True,
Expand Down
79 changes: 73 additions & 6 deletions spyderlib/plugins/configdialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
from spyderlib.config import CONF
from spyderlib.guiconfig import (CUSTOM_COLOR_SCHEME_NAME,
set_default_color_scheme)
Expand Down Expand Up @@ -90,8 +91,20 @@ 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.
if getattr(self, 'save_lang', False):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this line necessary?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cause this class is inherited by all config panes. But only the main will for sure have the save_lang method.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how about changing the name to __save_lang ? this way it will be "hidden" to the child class (external plugins included), but the name will be changed at runtime (the class name is added).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sure

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
Expand Down Expand Up @@ -238,6 +251,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):
Expand Down Expand Up @@ -301,6 +315,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)
Expand Down Expand Up @@ -344,7 +361,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()):
Expand Down Expand Up @@ -374,7 +391,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):
Expand Down Expand Up @@ -579,7 +596,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()
Expand All @@ -595,6 +612,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,
Expand Down Expand Up @@ -661,13 +680,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

Expand Down Expand Up @@ -704,13 +752,20 @@ 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)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ccordoba12 this should be clearer now

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, much better, thanks! Now I don't have objections.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

# Decide if it's possible to activate or not singie instance mode
if running_in_mac_app():
self.set_option("single_instance", True)
single_instance_box.setEnabled(False)

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)
Expand Down Expand Up @@ -777,6 +832,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"
Expand Down
3 changes: 2 additions & 1 deletion spyderlib/utils/iofuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"""
Expand Down