diff --git a/deluge/common.py b/deluge/common.py index 1720f0201..232b2cd42 100644 --- a/deluge/common.py +++ b/deluge/common.py @@ -10,7 +10,6 @@ """Common functions for various parts of Deluge to use.""" import base64 -import gettext import locale import logging import numbers @@ -903,11 +902,6 @@ def create_localclient_account(append=False): fd.close() -def get_translations_path(): - """Get the absolute path to the directory containing translation files""" - return resource_filename("deluge", "i18n") - - def set_env_variable(name, value): ''' :param name: environment variable name @@ -970,75 +964,6 @@ def set_env_variable(name, value): log.debug('Set Env Var \'%s\' to \'%s\' (\'%s._putenv\')', name, value, msvcrtname) -def set_language(lang): - """ - Set the language to use. - - gettext and GtkBuilder will load the translations from the specified - language. - - :param lang: the language, e.g. "en", "de" or "en_GB" - :type lang: str - """ - lang = str(lang) - # Necessary to set these environment variables for GtkBuilder - set_env_variable('LANGUAGE', lang) # Windows/Linux - set_env_variable('LANG', lang) # For OSX - - translations_path = get_translations_path() - try: - ro = gettext.translation("deluge", localedir=translations_path, languages=[lang]) - ro.install() - except IOError as ex: - log.warn("IOError when loading translations: %s", ex) - - -# Initialize gettext -def setup_translations(setup_gettext=True, setup_pygtk=False): - translations_path = get_translations_path() - domain = "deluge" - log.info("Setting up translations from %s", translations_path) - - if setup_pygtk: - try: - log.info("Setting up GTK translations from %s", translations_path) - - if windows_check(): - import ctypes - libintl = ctypes.cdll.intl - libintl.bindtextdomain(domain, translations_path.encode(sys.getfilesystemencoding())) - libintl.textdomain(domain) - libintl.bind_textdomain_codeset(domain, "UTF-8") - libintl.gettext.restype = ctypes.c_char_p - - # Use glade for plugins that still uses it - import gtk - import gtk.glade - gtk.glade.bindtextdomain(domain, translations_path) - gtk.glade.textdomain(domain) - except Exception as ex: - log.error("Unable to initialize glade translation!") - log.exception(ex) - if setup_gettext: - try: - if hasattr(locale, "bindtextdomain"): - locale.bindtextdomain(domain, translations_path) - if hasattr(locale, "textdomain"): - locale.textdomain(domain) - - gettext.bindtextdomain(domain, translations_path) - gettext.bind_textdomain_codeset(domain, 'UTF-8') - gettext.textdomain(domain) - gettext.install(domain, translations_path, unicode=True) - except Exception as ex: - log.error("Unable to initialize gettext/locale!") - log.exception(ex) - import __builtin__ - __builtin__.__dict__["_"] = lambda x: x - - translate_size_units() - - def unicode_argv(): """ Gets sys.argv as list of unicode objects on any platform.""" if windows_check(): diff --git a/deluge/core/daemon_entry.py b/deluge/core/daemon_entry.py index 5b6c756a8..e7d550161 100644 --- a/deluge/core/daemon_entry.py +++ b/deluge/core/daemon_entry.py @@ -17,6 +17,7 @@ import deluge.common import deluge.configmanager import deluge.error from deluge.ui.baseargparser import BaseArgParser +from deluge.ui.util import lang def add_daemon_options(parser): @@ -55,7 +56,7 @@ def start_daemon(skip_start=False): deluge.core.daemon.Daemon: A new daemon object """ - deluge.common.setup_translations() + lang.set_dummy_trans() # Setup the argument parser parser = BaseArgParser() diff --git a/deluge/tests/common.py b/deluge/tests/common.py index df411848f..7ca084258 100644 --- a/deluge/tests/common.py +++ b/deluge/tests/common.py @@ -6,11 +6,11 @@ from twisted.internet import defer, protocol, reactor from twisted.internet.defer import Deferred from twisted.internet.error import CannotListenError -import deluge.common import deluge.configmanager import deluge.core.preferencesmanager import deluge.log from deluge.error import DelugeError +from deluge.ui.util import lang deluge.log.setup_logger("none") @@ -44,7 +44,7 @@ def rpath(*args): return os.path.join(os.path.dirname(__file__), *args) # Initialize gettext -deluge.common.setup_translations() +lang.setup_translations() class ProcessOutputHandler(protocol.ProcessProtocol): diff --git a/deluge/tests/test_common.py b/deluge/tests/test_common.py index fc6e97971..7201a7e4c 100644 --- a/deluge/tests/test_common.py +++ b/deluge/tests/test_common.py @@ -3,12 +3,13 @@ import os from twisted.trial import unittest from deluge.common import (VersionSplit, fdate, fpcnt, fpeer, fsize, fspeed, ftime, get_path_size, is_ip, is_magnet, - is_url, setup_translations) + is_url) +from deluge.ui.util import lang class CommonTestCase(unittest.TestCase): def setUp(self): # NOQA - setup_translations() + lang.setup_translations() def tearDown(self): # NOQA pass diff --git a/deluge/tests/test_files_tab.py b/deluge/tests/test_files_tab.py index 17cc5928f..e27e376ef 100644 --- a/deluge/tests/test_files_tab.py +++ b/deluge/tests/test_files_tab.py @@ -1,9 +1,9 @@ import pytest from twisted.trial import unittest -import deluge.common import deluge.component as component from deluge.configmanager import ConfigManager +from deluge.ui.util import lang from . import common from .basetest import BaseTestCase @@ -20,7 +20,7 @@ except ImportError as err: import traceback traceback.print_exc() -deluge.common.setup_translations() +lang.setup_translations() @pytest.mark.gtkui diff --git a/deluge/tests/test_torrentview.py b/deluge/tests/test_torrentview.py index 72400119e..8db0c12e0 100644 --- a/deluge/tests/test_torrentview.py +++ b/deluge/tests/test_torrentview.py @@ -1,9 +1,9 @@ import pytest from twisted.trial import unittest -import deluge.common import deluge.component as component from deluge.configmanager import ConfigManager +from deluge.ui.util import lang from . import common from .basetest import BaseTestCase @@ -23,7 +23,7 @@ except ImportError as err: import traceback traceback.print_exc() -deluge.common.setup_translations() +lang.setup_translations() @pytest.mark.gtkui diff --git a/deluge/ui/gtkui/gtkui.py b/deluge/ui/gtkui/gtkui.py index 84972ac16..3d4edfd92 100644 --- a/deluge/ui/gtkui/gtkui.py +++ b/deluge/ui/gtkui/gtkui.py @@ -55,6 +55,7 @@ from deluge.ui.gtkui.torrentview import TorrentView from deluge.ui.sessionproxy import SessionProxy from deluge.ui.tracker_icons import TrackerIcons from deluge.ui.ui import UI +from deluge.ui.util import lang gobject.set_prgname("deluge") @@ -163,7 +164,7 @@ class Gtk(UI): class GtkUI(object): def __init__(self, args): # Setup gtkbuilder/glade translation - deluge.common.setup_translations(setup_gettext=False, setup_pygtk=True) + lang.setup_translations(setup_gettext=False, setup_pygtk=True) # Setup signals def on_die(*args): @@ -201,7 +202,7 @@ class GtkUI(object): # Set language if self.config["language"] is not None: - deluge.common.set_language(self.config["language"]) + lang.set_language(self.config["language"]) # Start the IPC Interface before anything else.. Just in case we are # already running. diff --git a/deluge/ui/gtkui/preferences.py b/deluge/ui/gtkui/preferences.py index ddda2f5f0..83677a10c 100644 --- a/deluge/ui/gtkui/preferences.py +++ b/deluge/ui/gtkui/preferences.py @@ -23,6 +23,7 @@ from deluge.ui.client import client from deluge.ui.gtkui.common import associate_magnet_links, get_deluge_icon from deluge.ui.gtkui.dialogs import AccountDialog, ErrorDialog, InformationDialog, YesNoDialog from deluge.ui.gtkui.path_chooser import PathChooser +from deluge.ui.util import lang pygtk.require('2.0') @@ -203,21 +204,13 @@ class Preferences(component.Component): self.copy_torrents_to_hbox.show_all() def load_languages(self): - from deluge.ui import languages # Import here so that gettext has been setup first - translations_path = deluge.common.get_translations_path() - for root, dirs, files in os.walk(translations_path): - # Get the dirs - lang_dirs = dirs - break self.language_combo = self.builder.get_object("combobox_language") self.language_checkbox = self.builder.get_object("checkbutton_language") lang_model = self.language_combo.get_model() - + langs = lang.get_languages() index = -1 - for i, lang_code in enumerate(sorted(lang_dirs)): - name = "%s (Language name missing)" % lang_code - if lang_code in languages.LANGUAGES: - name = languages.LANGUAGES[lang_code] + for i, l in enumerate(langs): + lang_code, name = l lang_model.append([lang_code, name]) if self.gtkui_config["language"] == lang_code: index = i @@ -225,8 +218,10 @@ class Preferences(component.Component): if self.gtkui_config["language"] is None: self.language_checkbox.set_active(True) self.language_combo.set_visible(False) - elif index != -1: - self.language_combo.set_active(index) + else: + self.language_combo.set_visible(True) + if index != -1: + self.language_combo.set_active(index) def __del__(self): del self.gtkui_config diff --git a/deluge/ui/ui.py b/deluge/ui/ui.py index aee10574d..8bc06fe1c 100644 --- a/deluge/ui/ui.py +++ b/deluge/ui/ui.py @@ -13,6 +13,7 @@ import deluge.common import deluge.configmanager import deluge.log from deluge.ui.baseargparser import BaseArgParser +from deluge.ui.util import lang log = logging.getLogger(__name__) @@ -27,7 +28,7 @@ class UI(object): def __init__(self, name="gtk", parser=None): self.__name = name - deluge.common.setup_translations(setup_pygtk=(name == "gtk")) + lang.setup_translations(setup_pygtk=(name == "gtk")) self.__parser = parser if parser else BaseArgParser() def parse_args(self, args=None): diff --git a/deluge/ui/ui_entry.py b/deluge/ui/ui_entry.py index 1630c5ae9..c18f1f7fe 100644 --- a/deluge/ui/ui_entry.py +++ b/deluge/ui/ui_entry.py @@ -13,14 +13,15 @@ """Main starting point for Deluge""" +import logging import sys -from logging import getLogger import pkg_resources import deluge.common import deluge.configmanager from deluge.ui.baseargparser import BaseArgParser +from deluge.ui.util import lang DEFAULT_PREFS = { "default_ui": "gtk" @@ -29,7 +30,7 @@ DEFAULT_PREFS = { def start_ui(): """Entry point for ui script""" - deluge.common.setup_translations() + lang.setup_translations() # Setup the argument parser parser = BaseArgParser() @@ -56,7 +57,7 @@ def start_ui(): options = parser.parse_args(deluge.common.unicode_argv()[1:]) config = deluge.configmanager.ConfigManager("ui.conf", DEFAULT_PREFS) - log = getLogger(__name__) + log = logging.getLogger(__name__) if options.default_ui: config["default_ui"] = options.default_ui diff --git a/deluge/ui/util/__init__.py b/deluge/ui/util/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/deluge/ui/util/lang.py b/deluge/ui/util/lang.py new file mode 100644 index 000000000..0c2bfccaa --- /dev/null +++ b/deluge/ui/util/lang.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2007,2008 Andrew Resch +# +# This file is part of Deluge and is licensed under GNU General Public License 3.0, or later, with +# the additional special exception to link portions of this program with the OpenSSL library. +# See LICENSE for more details. +# + +import gettext +import locale +import logging +import os +import sys + +import deluge.common + +log = logging.getLogger(__name__) +log.addHandler(logging.NullHandler()) # Silence: No handlers could be found for logger "deluge.util.lang" + + +def set_dummy_trans(warn_msg=None): + import __builtin__ + + def _func(txt): + if warn_msg: + log.warn("'%s' has been marked for translation, but translation is unavailable.", txt) + return txt + __builtin__.__dict__["_"] = _func + + +def get_translations_path(): + """Get the absolute path to the directory containing translation files""" + return deluge.common.resource_filename("deluge", "i18n") + + +def get_languages(): + from deluge.ui import languages # Import here so that gettext has been setup first + translations_path = get_translations_path() + for root, dirs, files in os.walk(translations_path): + # Get the dirs + lang_dirs = dirs + break + + lang = [] + for i, lang_code in enumerate(sorted(lang_dirs)): + name = "%s (Language name missing)" % lang_code + if lang_code in languages.LANGUAGES: + name = languages.LANGUAGES[lang_code] + lang.append([lang_code, name]) + return lang + + +def set_language(lang): + """ + Set the language to use. + + gettext and GtkBuilder will load the translations from the specified + language. + + :param lang: the language, e.g. "en", "de" or "en_GB" + :type lang: str + """ + lang = str(lang) + # Necessary to set these environment variables for GtkBuilder + deluge.common.set_env_variable('LANGUAGE', lang) # Windows/Linux + deluge.common.set_env_variable('LANG', lang) # For OSX + + translations_path = get_translations_path() + try: + ro = gettext.translation("deluge", localedir=translations_path, languages=[lang]) + ro.install() + except IOError as ex: + log.warn("IOError when loading translations: %s", ex) + + +# Initialize gettext +def setup_translations(setup_gettext=True, setup_pygtk=False): + translations_path = get_translations_path() + domain = "deluge" + log.info("Setting up translations from %s", translations_path) + + if setup_pygtk: + try: + log.info("Setting up GTK translations from %s", translations_path) + + if deluge.common.windows_check(): + import ctypes + libintl = ctypes.cdll.intl + libintl.bindtextdomain(domain, translations_path.encode(sys.getfilesystemencoding())) + libintl.textdomain(domain) + libintl.bind_textdomain_codeset(domain, "UTF-8") + libintl.gettext.restype = ctypes.c_char_p + + # Use glade for plugins that still uses it + import gtk + import gtk.glade + gtk.glade.bindtextdomain(domain, translations_path) + gtk.glade.textdomain(domain) + except Exception as ex: + log.error("Unable to initialize glade translation!") + log.exception(ex) + if setup_gettext: + try: + if hasattr(locale, "bindtextdomain"): + locale.bindtextdomain(domain, translations_path) + if hasattr(locale, "textdomain"): + locale.textdomain(domain) + + gettext.bindtextdomain(domain, translations_path) + gettext.bind_textdomain_codeset(domain, 'UTF-8') + gettext.textdomain(domain) + gettext.install(domain, translations_path, unicode=True) + except Exception as ex: + log.error("Unable to initialize gettext/locale!") + log.exception(ex) + set_dummy_trans() + + deluge.common.translate_size_units() diff --git a/deluge/ui/web/js/deluge-all/preferences/InterfacePage.js b/deluge/ui/web/js/deluge-all/preferences/InterfacePage.js index 9c09fb0a3..11d4eb239 100644 --- a/deluge/ui/web/js/deluge-all/preferences/InterfacePage.js +++ b/deluge/ui/web/js/deluge-all/preferences/InterfacePage.js @@ -36,25 +36,48 @@ Deluge.preferences.Interface = Ext.extend(Ext.form.FormPanel, { }); om.bind('show_session_speed', fieldset.add({ name: 'show_session_speed', - height: 22, + height: 17, fieldLabel: '', labelSeparator: '', boxLabel: _('Show session speed in titlebar') })); om.bind('sidebar_show_zero', fieldset.add({ name: 'sidebar_show_zero', - height: 22, + height: 17, fieldLabel: '', labelSeparator: '', boxLabel: _('Show filters with zero torrents') })); om.bind('sidebar_multiple_filters', fieldset.add({ name: 'sidebar_multiple_filters', - height: 22, + height: 17, fieldLabel: '', labelSeparator: '', boxLabel: _('Allow the use of multiple filters at once') })); + var languagePanel = this.add({ + xtype: 'fieldset', + border: false, + title: _('Language'), + style: 'margin-bottom: 0px; padding-bottom: 5px; padding-top: 8px', + autoHeight: true, + labelWidth: 1, + defaultType: 'checkbox' + }); + this.language = om.bind('language', languagePanel.add({ + xtype: 'combo', + labelSeparator: '', + name: 'language', + mode: 'local', + width: 200, + store: new Ext.data.ArrayStore({ + fields: ['id', 'text'] + }), + editable: false, + triggerAction: 'all', + valueField: 'id', + displayField: 'text' + })); fieldset = this.add({ xtype: 'fieldset', @@ -73,16 +96,19 @@ Deluge.preferences.Interface = Ext.extend(Ext.form.FormPanel, { this.oldPassword = fieldset.add({ name: 'old_password', fieldLabel: _('Old Password:'), + height: 20, labelSeparator: '' }); this.newPassword = fieldset.add({ name: 'new_password', fieldLabel: _('New Password:'), + height: 20, labelSeparator: '' }); this.confirmPassword = fieldset.add({ name: 'confirm_password', fieldLabel: _('Confirm Password:'), + height: 20, labelSeparator: '' }); @@ -137,7 +163,7 @@ Deluge.preferences.Interface = Ext.extend(Ext.form.FormPanel, { name: 'https', hideLabel: true, width: 280, - height: 22, + height: 17, boxLabel: _('Use SSL (paths relative to Deluge config folder)') })); this.httpsField.on('check', this.onSSLCheck, this); @@ -177,6 +203,12 @@ Deluge.preferences.Interface = Ext.extend(Ext.form.FormPanel, { this.optionsManager.set(config); }, + onGotLanguages: function(info, obj, response, request) { + info.unshift(['', _('System Default')]) + this.language.store.loadData(info); + this.language.setValue(this.optionsManager.get('language')); + }, + onPasswordChange: function() { var newPassword = this.newPassword.getValue(); if (newPassword != this.confirmPassword.getValue()) { @@ -230,7 +262,11 @@ Deluge.preferences.Interface = Ext.extend(Ext.form.FormPanel, { deluge.client.web.get_config({ success: this.onGotConfig, scope: this - }) + }); + deluge.client.webutils.get_languages({ + success: this.onGotLanguages, + scope: this, + }); }, onSSLCheck: function(e, checked) { diff --git a/deluge/ui/web/js/gettext.js b/deluge/ui/web/js/gettext.js index 268ac3a60..f04b16f51 100644 --- a/deluge/ui/web/js/gettext.js +++ b/deluge/ui/web/js/gettext.js @@ -1,5 +1,6 @@ GetText={maps:{}, add:function(string,translation) {this.maps[string]=translation}, get:function(string) {if (this.maps[string]) {string=this.maps[string]} return string}} - function _(string) {return GetText.get(string)} GetText.add('10 KiB/s','${escape(_("10 KiB/s"))}') + function _(string) {return GetText.get(string)} +GetText.add('10 KiB/s','${escape(_("10 KiB/s"))}') GetText.add('30 KiB/s','${escape(_("30 KiB/s"))}') GetText.add('300 KiB/s','${escape(_("300 KiB/s"))}') GetText.add('5 KiB/s','${escape(_("5 KiB/s"))}') @@ -130,6 +131,7 @@ GetText.add('Invalid Password','${escape(_("Invalid Password"))}') GetText.add('KiB/s','${escape(_("KiB/s"))}') GetText.add('LSD','${escape(_("LSD"))}') GetText.add('Labels','${escape(_("Labels"))}') +GetText.add('Language:','${escape(_("Language:"))}') GetText.add('Level:','${escape(_("Level:"))}') GetText.add('Loading','${escape(_("Loading"))}') GetText.add('Login','${escape(_("Login"))}') diff --git a/deluge/ui/web/json_api.py b/deluge/ui/web/json_api.py index 38872cdd8..7ee3e43a4 100644 --- a/deluge/ui/web/json_api.py +++ b/deluge/ui/web/json_api.py @@ -27,6 +27,7 @@ from deluge.ui import common as uicommon from deluge.ui.client import Client, client from deluge.ui.coreconfig import CoreConfig from deluge.ui.sessionproxy import SessionProxy +from deluge.ui.util import lang from deluge.ui.web.common import _, compress log = logging.getLogger(__name__) @@ -957,3 +958,21 @@ class WebApi(JSONComponent): Retrieve the pending events for the session. """ return self.event_queue.get_events(__request__.session_id) + + +class WebUtils(JSONComponent): + """ + + """ + def __init__(self): + super(WebUtils, self).__init__("WebUtils") + + @export + def get_languages(self): + """ + Get the available translated languages + + Returns: + list: of tuples [(lang-id, language-name), ...] + """ + return lang.get_languages() diff --git a/deluge/ui/web/server.py b/deluge/ui/web/server.py index 591eeec9b..5ad2936f2 100644 --- a/deluge/ui/web/server.py +++ b/deluge/ui/web/server.py @@ -23,9 +23,10 @@ from twisted.web import http, resource, server, static from deluge import common, component, configmanager from deluge.core.rpcserver import check_ssl_keys from deluge.ui.tracker_icons import TrackerIcons +from deluge.ui.util import lang from deluge.ui.web.auth import Auth from deluge.ui.web.common import Template, compress -from deluge.ui.web.json_api import JSON, WebApi +from deluge.ui.web.json_api import JSON, WebApi, WebUtils from deluge.ui.web.pluginmanager import PluginManager log = logging.getLogger(__name__) @@ -48,6 +49,7 @@ CONFIG_DEFAULTS = { "show_sidebar": True, "theme": "gray", "first_login": True, + "language": "", # Server Settings "base": "/", @@ -533,6 +535,8 @@ class DelugeWeb(component.Component): def __init__(self, options=None): super(DelugeWeb, self).__init__("DelugeWeb") self.config = configmanager.ConfigManager("web.conf", CONFIG_DEFAULTS) + self.config.run_converter((0, 1), 2, self._migrate_config_1_to_2) + self.config.register_set_function("language", self._on_language_changed) self.socket = None self.top_level = TopLevel() @@ -556,13 +560,21 @@ class DelugeWeb(component.Component): # Strip away slashes and serve on the base path as well as root path self.top_level.putChild(self.base.strip('/'), self.top_level) + lang.setup_translations(setup_gettext=True, setup_pygtk=False) + self.site = server.Site(self.top_level) self.web_api = WebApi() + self.web_utils = WebUtils() + self.auth = Auth(self.config) self.standalone = True # Initalize the plugins self.plugins = PluginManager() + def _on_language_changed(self, key, value): + log.debug("Setting UI language '%s'", value) + lang.set_language(value) + def install_signal_handlers(self): # Since twisted assigns itself all the signals may as well make # use of it. @@ -648,6 +660,10 @@ class DelugeWeb(component.Component): if self.standalone and reactor.running: reactor.stop() + def _migrate_config_1_to_2(self, config): + config["language"] = CONFIG_DEFAULTS["language"] + return config + if __name__ == "__builtin__": deluge_web = DelugeWeb()