-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Changes from all commits
f347ebf
bdac181
3c49cb2
5d8e05b
2c7a6a7
90c2b93
b79ff92
f8ce028
5bae10b
ea40c03
8285af5
8a3d664
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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(): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It also seems like too much to have this function There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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': | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this change here. Maybe it's better to just move the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I mean, what I don't understand is why you're looking for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Since this method runs for every child (any other section or plugin) then I need this to avoid an error of calling _save_lang when it does not exist. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The other way would be to make the save_lang method part of one of the ancestors so that EVERY section would alwyas make that save... but since it is only necessary for main... There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Too much work :-) Then leave this part as it is now. Please see if you can move the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will do! ;-) |
||
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()) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Moved here, right at the end of the init :-), should work fine now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
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,13 +756,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) | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @ccordoba12 this should be clearer now There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, much better, thanks! Now I don't have objections. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) | ||
|
@@ -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" | ||
|
There was a problem hiding this comment.
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.There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 :-)
There was a problem hiding this comment.
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