Skip to content

Commit 069273b

Browse files
author
Konstantinos Bairaktaris
committed
Event dispatcher and urwid SDK
1 parent 3fe0982 commit 069273b

4 files changed

Lines changed: 182 additions & 7 deletions

File tree

tests/native/core/test_daemon.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def test_daemon_starts(self, patched_tx):
1515
cds_host='https://some.host')
1616

1717
# the `interval` we will be using
18-
interval = 1
18+
interval = .1
1919

2020
daemon = DaemonicThread()
2121

@@ -49,7 +49,7 @@ def test_daemon_exception(self, patched_logger, patched_tx):
4949

5050
daemon = DaemonicThread()
5151

52-
interval = 1
52+
interval = .1
5353
daemon.start_daemon(interval=1)
5454
time.sleep(interval * 2)
5555
assert daemon.is_daemon_running(log_errors=False)

transifex/native/core.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from transifex.common.utils import generate_key, parse_plurals
77
from transifex.native.cache import MemoryCache
88
from transifex.native.cds import CDSHandler
9+
from transifex.native.events import EventDispatcher
910
from transifex.native.rendering import (SourceStringErrorPolicy,
1011
SourceStringPolicy, StringRenderer)
1112

@@ -20,6 +21,7 @@ def __init__(self, **kwargs):
2021
self.hardcoded_language_codes = None
2122
self.remote_languages = None
2223

24+
self._event_dispatcher = EventDispatcher()
2325
self._missing_policy = SourceStringPolicy()
2426
self._cds_handler = CDSHandler()
2527
self._cache = MemoryCache()
@@ -62,10 +64,19 @@ def setup(self,
6264

6365
if current_language is not None:
6466
self.set_current_language(current_language)
67+
elif source_language is not None:
68+
self.set_current_language(source_language)
6569

6670
def fetch_languages(self, force=False):
6771
if self.remote_languages is None or force:
68-
self.remote_languages = self._cds_handler.fetch_languages()
72+
self._event_dispatcher.trigger('FETCHING_LOCALES')
73+
try:
74+
self.remote_languages = self._cds_handler.fetch_languages()
75+
except Exception:
76+
self._event_dispatcher.trigger('LOCALES_FETCH_FAILED')
77+
raise
78+
else:
79+
self._event_dispatcher.trigger('LOCALES_FETCHED')
6980

7081
if self.hardcoded_language_codes is not None:
7182
return [language
@@ -81,7 +92,9 @@ def set_current_language(self, language_code, force=False):
8192
format(language_code))
8293
if language_code not in self._cache or force:
8394
self.fetch_translations(language_code=language_code, force=True)
95+
prev = self.current_language_code
8496
self.current_language_code = language_code
97+
self._event_dispatcher.trigger('LOCALE_CHANGED', prev, language_code)
8598

8699
def fetch_translations(self, language_code=None, force=False):
87100
"""Fetch fresh content from the CDS."""
@@ -95,10 +108,20 @@ def fetch_translations(self, language_code=None, force=False):
95108
"Language {} is not supported by the application".
96109
format(language_code)
97110
)
98-
if language_code not in self._cache or force:
99-
translations = self._cds_handler.\
100-
fetch_translations(language_code)
101-
self._cache.update(translations)
111+
self._event_dispatcher.trigger('FETCHING_TRANSLATIONS',
112+
language_code)
113+
try:
114+
if language_code not in self._cache or force:
115+
translations = self._cds_handler.\
116+
fetch_translations(language_code)
117+
self._cache.update(translations)
118+
except Exception:
119+
self._event_dispatcher.trigger('TRANSLATIONS_FETCH_FAILED',
120+
language_code)
121+
raise
122+
else:
123+
self._event_dispatcher.trigger('TRANSLATIONS_FETCHED',
124+
language_code)
102125

103126
def translate(self, source_string, language_code=None, _context=None,
104127
escape=True, params=None):
@@ -191,3 +214,10 @@ def push_source_strings(self, strings, purge=False):
191214
"""
192215
response = self._cds_handler.push_source_strings(strings, purge)
193216
return response.status_code, json.loads(response.content)
217+
218+
# Events
219+
def on(self, label, callback):
220+
self._event_dispatcher.on(label, callback)
221+
222+
def off(self, label, callback):
223+
self._event_dispatcher.off(label, callback)

transifex/native/events.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
class EventDispatcher(object):
2+
LABELS = ['FETCHING_TRANSLATIONS', 'TRANSLATIONS_FETCHED',
3+
'TRANSLATIONS_FETCH_FAILED', 'LOCALE_CHANGED',
4+
'FETCHING_LOCALES', 'LOCALES_FETCHED', 'LOCALES_FETCH_FAILED']
5+
6+
def __init__(self):
7+
self.callbacks = {}
8+
9+
def on(self, label, callback):
10+
self._require_label(label)
11+
self.callbacks.setdefault(label, set()).add(callback)
12+
13+
def off(self, label, callback):
14+
self._require_label(label)
15+
# Can raise KeyError if callback is not there
16+
self.callbacks.get(label, set()).remove(callback)
17+
18+
def trigger(self, label, *args, **kwargs):
19+
self._require_label(label)
20+
for callback in self.callbacks.get(label, []):
21+
callback(*args, **kwargs)
22+
23+
def _require_label(self, label):
24+
if label not in self.LABELS:
25+
raise ValueError("Label '{}' is not supported".format(label))

transifex/native/urwid/__init__.py

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
""" Utilities for integrating urwid applications with Transifex Native. """
2+
3+
import urwid
4+
5+
from transifex.native import t, tx
6+
7+
8+
class Variable(object):
9+
""" Holds a value and will trigger events on change.
10+
11+
>>> v = Variable(1)
12+
>>> v.on_change(lambda: print("New value: " + v.get()))
13+
>>> v.set(v.get() + 1)
14+
<<< # New value: 2
15+
"""
16+
17+
def __init__(self, value):
18+
self._value = value
19+
self._callbacks = set()
20+
21+
def get(self):
22+
return self._value
23+
24+
def set(self, value):
25+
if value != self._value:
26+
self._value = value
27+
for callback in self._callbacks:
28+
callback(value)
29+
30+
def on_change(self, callback):
31+
self._callbacks.add(callback)
32+
33+
def off_change(self, callback):
34+
self._callbacks.remove(callback)
35+
36+
37+
class T(urwid.Text):
38+
""" Usage:
39+
40+
Render the string in the current language. Will rerender on language
41+
change:
42+
43+
>>> T("Hello world")
44+
45+
Render using the variable as template parameter, will rerender on
46+
language change and if the parameter changes value:
47+
48+
>>> variable = Variable("Bob")
49+
>>> T("Hello {username}", {'username': variable})
50+
>>> variable.set("Jill")
51+
52+
Render inside an untranslatable wrapper template:
53+
54+
>>> T("Hello world", wrapper="Translation: {}")
55+
"""
56+
57+
def __init__(self, source_string, params=None, wrapper=None, _context=None,
58+
_charlimit=None, _comment=None, _occurrences=None, _tags=None,
59+
*args, **kwargs):
60+
if params is None:
61+
params = {}
62+
63+
self._source_string = source_string
64+
self._params = params
65+
self._wrapper = wrapper
66+
67+
tx.on("LOCALE_CHANGED", self.rerender)
68+
for key, value in self._params.items():
69+
try:
70+
value.on_change(self.rerender)
71+
except AttributeError:
72+
pass
73+
74+
super().__init__("", *args, **kwargs)
75+
self.rerender()
76+
77+
def rerender(self, *args, **kwargs):
78+
params = {}
79+
for key, value in self._params.items():
80+
try:
81+
params[key] = value.get()
82+
except AttributeError:
83+
params[key] = value
84+
85+
translation = t(self._source_string, params=params)
86+
87+
if self._wrapper is not None:
88+
self.set_text(self._wrapper.format(translation))
89+
else:
90+
self.set_text(translation)
91+
92+
93+
def language_picker(source_language=None):
94+
""" Returns an array of radio buttons for language selection.
95+
96+
The 'source_language' must be a dictionary describing the source
97+
language, with at least the 'name' and 'code' fields. If unset,
98+
`{'name': "English", 'code': "en"}` will be used
99+
"""
100+
101+
if source_language is None:
102+
source_language = {'name': "English", 'code': "en"}
103+
104+
languages = tx.fetch_languages()
105+
if not any((language['code'] == source_language['code']
106+
for language in languages)):
107+
languages = [source_language] + languages
108+
109+
language_group, language_radio = [], []
110+
for language in languages:
111+
button = urwid.RadioButton(language_group, language['name'])
112+
urwid.connect_signal(button, 'change', _on_language_select,
113+
language['code'])
114+
language_radio.append(button)
115+
return language_radio
116+
117+
118+
def _on_language_select(radio_button, new_state, language_code):
119+
if new_state:
120+
tx.set_current_language(language_code)

0 commit comments

Comments
 (0)