Skip to content

Commit fe0642e

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

5 files changed

Lines changed: 181 additions & 8 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/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from transifex.native.core import TxNative
22

3-
43
tx = TxNative()
54
t = tx.translate

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

0 commit comments

Comments
 (0)