-
-
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 6 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,7 @@ | |
|
||
from __future__ import print_function | ||
|
||
import locale | ||
import os.path as osp | ||
import os | ||
import sys | ||
|
@@ -204,18 +205,102 @@ def get_image_path(name, default="not_found.png"): | |
#============================================================================== | ||
# Translations | ||
#============================================================================== | ||
LANG_FILE = osp.join(get_conf_path(), 'langconfig') | ||
DEFAULT_LANGUAGE = 'en' | ||
|
||
# This needs to be updated every time a new language is added to spyder. | ||
LANGUAGE_CODES = {'en': u'English', | ||
'fr': u'Français', | ||
'es': u'Español', | ||
'pt_BR': u'Português' | ||
} | ||
|
||
|
||
def get_available_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. 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 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. 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 commentThe 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 commentThe 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. | ||
""" | ||
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 spyder_language in spyder_languages: | ||
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. Let's simplify the variables here to make it a bit easier to read. What about this? for lang in spyder_languages 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 sure, I will adapt ;-) |
||
if locale_language == spyder_language: | ||
language = locale_language | ||
break | ||
elif locale_language.startswith(spyder_language) or\ | ||
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. Missing space: 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 fix, but probably will be one line with your suggestion |
||
spyder_language.startswith(locale_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. 2 indentation spaces here, not 8, please (I know PEP8, but I don't like it) |
||
language = spyder_language | ||
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 in windows | ||
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 same for Ubuntu below, but changing it to Linux :-) 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. Is this going to work on Mac for sure? 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. Translations on Mac never have worked because Mac doesn't define proper This work will let users select the translation they want to use :-) |
||
os.environ["LANGUAGE"] = language # Works in ubuntu | ||
|
||
import gettext | ||
try: | ||
_trans = gettext.translation(modname, locale_path, codeset="utf-8") | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
||
|
||
|
@@ -169,6 +169,7 @@ def is_ubuntu(): | |
'animated_docks': True, | ||
'prompt_on_exit': False, | ||
'panes_locked': True, | ||
'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. 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 That's why (for example) we don't add here the window 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. Another example of things we don't add are the run configuration options per file :-) 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. Please remove this option from here to finally merge this PR :-) If I understand the code correctly:
So there's no need to have it here. 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. 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 Suggestions? 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 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 |
||
'window/size': (1260, 740), | ||
'window/position': (10, 10), | ||
'window/is_maximized': True, | ||
|
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, | ||
get_available_translations, save_lang_conf) | ||
from spyderlib.config import CONF | ||
from spyderlib.guiconfig import (CUSTOM_COLOR_SCHEME_NAME, | ||
set_default_color_scheme) | ||
|
@@ -90,6 +91,18 @@ 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): | ||
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. Why is this line necessary? 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. Cause this class is inherited by all config panes. But only the main will for sure have the save_lang method. 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. how about changing the name to 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. sure |
||
self.save_lang() | ||
|
||
for restart_option in self.restart_options: | ||
if restart_option in self.changed_options: | ||
self.prompt_restart_required() | ||
break | ||
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. Why this 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 guess that I am under the impression that if I do not break it here, you will get a popup for every option that the user changed (and requires restart) 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 see, it's fine then :-) |
||
|
||
self.set_modified(False) | ||
|
||
def load_from_conf(self): | ||
|
@@ -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): | ||
|
@@ -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) | ||
|
@@ -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()): | ||
|
@@ -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): | ||
|
@@ -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() | ||
|
@@ -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, | ||
|
@@ -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_opt = self.restart_options | ||
changed_opt = self.changed_options | ||
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.
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. fixed |
||
options = [restart_opt[o] for o in changed_opt if o in restart_opt] | ||
|
||
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 +752,21 @@ def setup_page(self): | |
margins_layout.addWidget(margin_spin) | ||
prompt_box = newcb(_("Prompt when exiting"), 'prompt_on_exit') | ||
|
||
# Language chooser | ||
langs = get_available_translations() | ||
choices = list(zip([LANGUAGE_CODES[key] for key in langs], langs)) | ||
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. What this [('en': 'English', 'es': 'Spanish')] ? 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 copied this from code above (the choices part and adapted it...) It holds what is stored in So the combo box display Español, English... etc... and the value is ...es, en... etc.. 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. So actually it is more like
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. This seems a bit too complex to my taste (I mean, the difference between What will happen when someone adds 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. When someone adds this where? 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 could just use the |
||
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 +833,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.
Please rewrite this line as
That's the way
get_conf_path
works :-)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.
Done