[#1959] [WebUI] Allow user selectable GUI language

This commit is contained in:
bendikro 2016-04-23 22:18:55 +02:00 committed by Calum Lind
parent 74f2f45fc0
commit 857e2fd46e
16 changed files with 227 additions and 110 deletions

View File

@ -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():

View File

@ -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()

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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):

View File

@ -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

View File

119
deluge/ui/util/lang.py Normal file
View File

@ -0,0 +1,119 @@
# -*- coding: utf-8 -*-
#
# Copyright (C) 2007,2008 Andrew Resch <andrewresch@gmail.com>
#
# 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()

View File

@ -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) {

View File

@ -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"))}')

View File

@ -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()

View File

@ -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()