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
97 changes: 91 additions & 6 deletions spyderlib/baseconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from __future__ import print_function

import locale
import os.path as osp
import os
import sys
Expand Down Expand Up @@ -204,18 +205,102 @@ def get_image_path(name, default="not_found.png"):
#==============================================================================
# Translations
#==============================================================================
LANG_FILE = osp.join(get_conf_path(), 'langconfig')
Copy link
Member

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

LANG_FILE = get_conf_path('langconfig')

That's the way get_conf_path works :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

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():
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.
"""
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 spyder_language in spyder_languages:
Copy link
Member

Choose a reason for hiding this comment

The 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

Copy link
Member Author

Choose a reason for hiding this comment

The 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\
Copy link
Member

Choose a reason for hiding this comment

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

Missing space: or\ -> or \

Copy link
Member Author

Choose a reason for hiding this comment

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

Choose a reason for hiding this comment

The 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
Copy link
Member

Choose a reason for hiding this comment

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

works in windows -> Works on Windows

The same for Ubuntu below, but changing it to Linux :-)

Copy link
Member Author

Choose a reason for hiding this comment

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

Is this going to work on Mac for sure?

Copy link
Member

Choose a reason for hiding this comment

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

Translations on Mac never have worked because Mac doesn't define proper locale values.

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")
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
78 changes: 73 additions & 5 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,
get_available_translations, 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,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):
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
Copy link
Member

Choose a reason for hiding this comment

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

Why this break 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.

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)

Copy link
Member

Choose a reason for hiding this comment

The 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):
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_opt = self.restart_options
changed_opt = self.changed_options
Copy link
Member

Choose a reason for hiding this comment

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

restart_opt -> restart_opts (because it's plural)
changed_opt -> changed_opts

Copy link
Member Author

Choose a reason for hiding this comment

The 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

Expand Down Expand Up @@ -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))
Copy link
Member

Choose a reason for hiding this comment

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

What this choices variable hold? I mean, is it something like

[('en': 'English', 'es': 'Spanish')]

?

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 copied this from code above (the choices part and adapted it...)

It holds what is stored in LANGUAGE_CODES in the baseconfig.py

So the combo box display Español, English... etc... and the value is ...es, en... etc..

Copy link
Member Author

Choose a reason for hiding this comment

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

So actually it is more like

choices = [(English, 'en'), ('Español', 'es'), ...]

Copy link
Member

Choose a reason for hiding this comment

The 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 LANGUAGE_CODES and get_available_translations).

What will happen when someone adds pt_PT or en_UK?

Copy link
Member Author

Choose a reason for hiding this comment

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

When someone adds this where?

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 could just use the LANGUAGE_CODES variable and remove the call to langs = get_available_translations()

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 +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"
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