diff --git a/tests/native/conftest.py b/tests/native/conftest.py
deleted file mode 100644
index 5a34816..0000000
--- a/tests/native/conftest.py
+++ /dev/null
@@ -1,8 +0,0 @@
-import pytest
-
-
-@pytest.fixture
-def reset_tx():
- """Reset "global" TxNative instance so that it can be reinitialized."""
- from transifex.native import tx
- tx.initialized = False
diff --git a/tests/native/core/test_cache.py b/tests/native/core/test_cache.py
index ae99582..539a385 100644
--- a/tests/native/core/test_cache.py
+++ b/tests/native/core/test_cache.py
@@ -37,3 +37,12 @@ def test_returns_entry_if_exists(self):
assert cache.get('chair', 'el') == u'Μια καρέκλα'
assert cache.get('invalid', 'en') is None
assert cache.get('invalid', 'el') is None
+
+ def test_contains(self):
+ cache = MemoryCache()
+ cache.update({
+ 'lang1': (True, {'source1': {'string': "translation1"},
+ 'source2': {'string': "translation2"}}),
+ })
+ assert 'lang1' in cache
+ assert 'lang2' not in cache
diff --git a/tests/native/core/test_cds.py b/tests/native/core/test_cds.py
index a9455db..1cb70f9 100644
--- a/tests/native/core/test_cds.py
+++ b/tests/native/core/test_cds.py
@@ -23,11 +23,9 @@ def _lang_lists_equal(self, list_1, list_2):
@patch('transifex.native.cds.logger')
def test_fetch_languages(self, patched_logger):
cds_host = 'https://some.host'
- cds_handler = CDSHandler(
- ['el', 'en'],
- 'some_token',
- host=cds_host
- )
+ cds_handler = CDSHandler(configured_languages=['el', 'en'],
+ token='some_token',
+ host=cds_host)
# correct response
responses.add(
@@ -140,11 +138,9 @@ def test_fetch_languages(self, patched_logger):
@patch('transifex.native.cds.logger')
def test_fetch_translations(self, patched_logger):
cds_host = 'https://some.host'
- cds_handler = CDSHandler(
- ['el', 'en', 'fr'],
- 'some_token',
- host=cds_host
- )
+ cds_handler = CDSHandler(configured_languages=['el', 'en', 'fr'],
+ token='some_token',
+ host=cds_host)
# add response for languages
responses.add(
@@ -206,7 +202,7 @@ def test_fetch_translations(self, patched_logger):
responses.GET, cds_host + '/content/fr', status=404
)
- resp = cds_handler.fetch_translations()
+ resp = cds_handler.fetch_translations('el')
assert resp == {
'el': (True, {
'key1': {
@@ -216,6 +212,9 @@ def test_fetch_translations(self, patched_logger):
'string': 'key2_el'
},
}),
+ }
+ resp = cds_handler.fetch_translations('en')
+ assert resp == {
'en': (True, {
'key1': {
'string': 'key1_en'
@@ -224,58 +223,16 @@ def test_fetch_translations(self, patched_logger):
'string': 'key2_en'
},
}),
- 'fr': (False, {}) # that is due to the error status in response
}
-
- responses.reset()
-
- # test fetch_languages fails with connection error
- responses.add(responses.GET, cds_host + '/languages', status=500)
- resp = cds_handler.fetch_translations()
- assert resp == {}
-
- patched_logger.error.assert_called_with(
- 'Error retrieving languages from CDS: UnknownError '
- '(`500 Server Error: Internal Server Error for url: '
- 'https://some.host/languages`)'
- )
- responses.reset()
- patched_logger.reset_mock()
-
- # test language code
- responses.add(
- responses.GET, cds_host + '/content/el',
- json={
- 'data': {
- 'key1': {
- 'string': 'key1_el'
- },
- 'key2': {
- 'string': 'key2_el'
- },
- },
- 'meta': {
- "some_key": "some_value"
- }
- }, status=200
- )
-
- resp = cds_handler.fetch_translations(language_code='el')
+ resp = cds_handler.fetch_translations('fr')
assert resp == {
- 'el': (True, {
- 'key1': {
- 'string': 'key1_el'
- },
- 'key2': {
- 'string': 'key2_el'
- },
- })
+ 'fr': (False, {}) # that is due to the error status in response
}
+
responses.reset()
- assert patched_logger.error.call_count == 0
# test connection_error
- resp = cds_handler.fetch_translations(language_code='el')
+ resp = cds_handler.fetch_translations('el')
patched_logger.error.assert_called_with(
'Error retrieving translations from CDS: ConnectionError'
)
@@ -286,11 +243,9 @@ def test_fetch_translations(self, patched_logger):
def test_fetch_translations_etags_management(self, patched_logger):
cds_host = 'https://some.host'
- cds_handler = CDSHandler(
- ['el', 'en'],
- 'some_token',
- host=cds_host
- )
+ cds_handler = CDSHandler(configured_languages=['el', 'en'],
+ token='some_token',
+ host=cds_host)
# add response for languages
responses.add(
@@ -337,7 +292,7 @@ def test_fetch_translations_etags_management(self, patched_logger):
status=304
)
- resp = cds_handler.fetch_translations()
+ resp = cds_handler.fetch_translations('el')
assert resp == {
'el': (True, {
'key1': {
@@ -347,15 +302,12 @@ def test_fetch_translations_etags_management(self, patched_logger):
'string': 'key2_el'
},
}),
- 'en': (False, {})
}
assert cds_handler.etags.get('el') == 'some_unique_tag_is_here'
def test_push_source_strings_no_secret(self):
- cds_handler = CDSHandler(
- ['el', 'en'],
- 'some_token',
- )
+ cds_handler = CDSHandler(configured_languages=['el', 'en'],
+ token='some_token')
with pytest.raises(Exception):
cds_handler.push_source_strings([], False)
@@ -363,12 +315,10 @@ def test_push_source_strings_no_secret(self):
@patch('transifex.native.cds.logger')
def test_push_source_strings(self, patched_logger):
cds_host = 'https://some.host'
- cds_handler = CDSHandler(
- ['el', 'en'],
- 'some_token',
- secret='some_secret',
- host=cds_host
- )
+ cds_handler = CDSHandler(configured_languages=['el', 'en'],
+ token='some_token',
+ secret='some_secret',
+ host=cds_host)
# test push no correct
responses.add(
@@ -426,12 +376,10 @@ def test_push_source_strings(self, patched_logger):
def test_get_headers(self):
cds_host = 'https://some.host'
- cds_handler = CDSHandler(
- ['el', 'en'],
- 'some_token',
- secret='some_secret',
- host=cds_host
- )
+ cds_handler = CDSHandler(configured_languages=['el', 'en'],
+ token='some_token',
+ secret='some_secret',
+ host=cds_host)
assert cds_handler._get_headers() == {
'Authorization': 'Bearer some_token',
'Accept-Encoding': 'gzip',
@@ -456,11 +404,9 @@ def test_get_headers(self):
@responses.activate
def test_retry_fetch_languages(self):
cds_host = 'https://some.host'
- cds_handler = CDSHandler(
- ['el', 'en'],
- 'some_token',
- host=cds_host,
- )
+ cds_handler = CDSHandler(configured_languages=['el', 'en'],
+ token='some_token',
+ host=cds_host)
responses.add(responses.GET, cds_host + '/languages', status=202)
responses.add(responses.GET, cds_host + '/languages', status=202)
responses.add(responses.GET, cds_host + '/languages',
@@ -477,11 +423,9 @@ def test_retry_fetch_languages(self):
@responses.activate
def test_retry_fetch_translations(self):
cds_host = 'https://some.host'
- cds_handler = CDSHandler(
- ['el', 'en'],
- 'some_token',
- host=cds_host,
- )
+ cds_handler = CDSHandler(configured_languages=['el', 'en'],
+ token='some_token',
+ host=cds_host)
responses.add(responses.GET, cds_host + '/content/el', status=202)
responses.add(responses.GET, cds_host + '/content/el', status=202)
responses.add(responses.GET,
diff --git a/tests/native/core/test_core.py b/tests/native/core/test_core.py
index 174017b..9e2623a 100644
--- a/tests/native/core/test_core.py
+++ b/tests/native/core/test_core.py
@@ -5,7 +5,7 @@
from transifex.common.utils import generate_key
from transifex.native.cache import MemoryCache
from transifex.native.cds import TRANSIFEX_CDS_HOST
-from transifex.native.core import NotInitializedError, TxNative
+from transifex.native.core import TxNative
from transifex.native.parsing import SourceString
from transifex.native.rendering import (PseudoTranslationPolicy,
SourceStringPolicy, parse_error_policy)
@@ -52,41 +52,14 @@ class TestNative(object):
def _get_tx(self, **kwargs):
mytx = TxNative()
- mytx.init(['en', 'el'], 'cds_token', **kwargs)
+ mytx.setup(source_language='en', languages=['en', 'el'],
+ token='cds_token', **kwargs)
return mytx
- def test_uninitialized(self):
- mytx = TxNative()
- with pytest.raises(NotInitializedError):
- mytx.translate('string', 'en')
- with pytest.raises(NotInitializedError):
- mytx.fetch_translations()
- with pytest.raises(NotInitializedError):
- mytx.push_source_strings([], False)
-
- def test_default_init(self):
- mytx = self._get_tx()
- assert mytx.initialized is True
- assert mytx._languages == ['en', 'el']
- assert isinstance(mytx._missing_policy, SourceStringPolicy)
- assert isinstance(mytx._cache, MemoryCache)
- assert mytx._cds_handler.token == 'cds_token'
- assert mytx._cds_handler.host == TRANSIFEX_CDS_HOST
-
- def test_custom_init(self):
- missing_policy = PseudoTranslationPolicy()
- mytx = self._get_tx(cds_host='myhost', missing_policy=missing_policy)
- assert mytx.initialized is True
- assert mytx._languages == ['en', 'el']
- assert mytx._missing_policy == missing_policy
- assert isinstance(mytx._cache, MemoryCache)
- assert mytx._cds_handler.token == 'cds_token'
- assert mytx._cds_handler.host == 'myhost'
-
@patch('transifex.native.core.StringRenderer.render')
def test_translate_source_language_reaches_renderer(self, mock_render):
mytx = self._get_tx()
- mytx.translate('My String', 'en', is_source=True)
+ mytx.translate('My String', 'en')
mock_render.assert_called_once_with(
source_string='My String',
string_to_render='My String',
@@ -102,13 +75,13 @@ def test_translate_target_language_missing_reaches_renderer(self, mock_render,
mock_cache):
mock_cache.return_value = None
mytx = self._get_tx()
- mytx.translate('My String', 'en', is_source=False)
+ mytx.translate('My String', 'foo')
mock_cache.assert_called_once_with(
- generate_key(string='My String'), 'en')
+ generate_key(string='My String'), 'foo')
mock_render.assert_called_once_with(
source_string='My String',
string_to_render=None,
- language_code='en',
+ language_code='foo',
escape=True,
missing_policy=mytx._missing_policy,
params={},
@@ -117,7 +90,7 @@ def test_translate_target_language_missing_reaches_renderer(self, mock_render,
def test_translate_target_language_missing_reaches_missing_policy(self):
missing_policy = MagicMock()
mytx = self._get_tx(missing_policy=missing_policy)
- mytx.translate('My String', 'en', is_source=False)
+ mytx.translate('My String', 'foo')
missing_policy.get.assert_called_once_with('My String')
@patch('transifex.native.core.StringRenderer')
@@ -125,10 +98,10 @@ def test_translate_error_reaches_error_policy(self, mock_renderer):
error_policy = MagicMock()
mock_renderer.render.side_effect = Exception
mytx = self._get_tx(error_policy=error_policy)
- mytx.translate('My String', 'en', is_source=False)
+ mytx.translate('My String', 'en')
error_policy.get.assert_called_once_with(
- source_string='My String', translation=None, language_code='en',
- escape=True, params={},
+ source_string='My String', translation="My String",
+ language_code='en', escape=True, params={},
)
def test_translate_error_reaches_source_string_error_policy(
@@ -139,7 +112,7 @@ def test_translate_error_reaches_source_string_error_policy(
mock_missing_policy = MagicMock()
mock_missing_policy.get.side_effect = Exception
mytx = self._get_tx(missing_policy=mock_missing_policy)
- result = mytx.translate('My String', 'en', is_source=False)
+ result = mytx.translate('My String', 'en')
assert result == 'My String'
@patch('transifex.native.core.StringRenderer')
@@ -158,7 +131,7 @@ def test_source_string_policy_custom_text(
mytx = self._get_tx(
error_policy=error_policy
)
- result = mytx.translate('My String', 'en', is_source=False)
+ result = mytx.translate('My String', 'en')
assert result == 'my-default-text'
def test_translate_source_language_renders_icu(self):
@@ -166,7 +139,6 @@ def test_translate_source_language_renders_icu(self):
translation = mytx.translate(
u'{cnt, plural, one {{cnt} duck} other {{cnt} ducks}}',
'en',
- is_source=True,
params={'cnt': 1}
)
assert translation == '1 duck'
@@ -177,8 +149,7 @@ def test_translate_target_language_renders_icu(self, mock_cache):
mytx = self._get_tx()
translation = mytx.translate(
u'{cnt, plural, one {{cnt} duck} other {{cnt} ducks}}',
- 'en',
- is_source=False,
+ 'el',
params={'cnt': 1}
)
assert translation == u'1 παπί'
@@ -188,7 +159,6 @@ def test_translate_source_language_escape_html_true(self):
translation = mytx.translate(
u'',
'en',
- is_source=True,
escape=True,
params={'cnt': 1}
)
@@ -201,7 +171,6 @@ def test_translate_source_language_escape_html_false(self):
translation = mytx.translate(
u'',
'en',
- is_source=True,
escape=False,
params={'cnt': 1}
)
@@ -223,6 +192,7 @@ def test_push_strings_reaches_cds_handler(self, mock_push_strings):
def test_fetch_translations_reaches_cds_handler_and_cache(self, mock_cds,
mock_cache):
mytx = self._get_tx()
+ mytx.remote_languages = [{'code': "el"}]
mytx.fetch_translations()
assert mock_cds.call_count == 1
assert mock_cache.call_count > 0
@@ -239,3 +209,13 @@ def test_plural(self, cache_mock):
u"fr_FR",
params={'cnt': 2})
assert translation == u'OTHER'
+
+ @patch('transifex.native.core.CDSHandler.fetch_translations')
+ def test_set_current_language(self, mock_cds):
+ mock_cds.return_value = {'el': (True, {})}
+ tx = self._get_tx()
+ tx.remote_languages = [{'code': "el"}]
+ tx.set_current_language('el')
+
+ assert tx.current_language_code == 'el'
+ mock_cds.assert_called_once_with('el')
diff --git a/tests/native/core/test_daemon.py b/tests/native/core/test_daemon.py
index 2368b01..4f82eaf 100644
--- a/tests/native/core/test_daemon.py
+++ b/tests/native/core/test_daemon.py
@@ -10,10 +10,12 @@ class TestFetchingDaemon(object):
@patch('transifex.native.daemon.tx')
def test_daemon_starts(self, patched_tx):
tx = TxNative()
- tx.init(['en', 'el'], 'some:token', 'https://some.host')
+ tx.setup(languages=['en', 'el'],
+ token='some:token',
+ cds_host='https://some.host')
# the `interval` we will be using
- interval = 1
+ interval = .1
daemon = DaemonicThread()
@@ -41,11 +43,13 @@ def test_daemon_exception(self, patched_logger, patched_tx):
'Something went wrong')
tx = TxNative()
- tx.init(['en', 'el'], 'some:token', 'https://some.host')
+ tx.setup(languages=['en', 'el'],
+ token='some:token',
+ cds_host='https://some.host')
daemon = DaemonicThread()
- interval = 1
+ interval = .1
daemon.start_daemon(interval=1)
time.sleep(interval * 2)
assert daemon.is_daemon_running(log_errors=False)
diff --git a/tests/native/core/test_init.py b/tests/native/core/test_init.py
index e4c0920..ea86bc7 100644
--- a/tests/native/core/test_init.py
+++ b/tests/native/core/test_init.py
@@ -1,4 +1,3 @@
-from transifex import native
from transifex.native import tx as _tx
from transifex.native.rendering import PseudoTranslationPolicy
@@ -6,25 +5,12 @@
class TestModuleInit(object):
"""Test __init__.py of root native module."""
- def test_init_uses_given_params(self, reset_tx):
- native.init(
- 'mytoken', ['lang1', 'lang2'], cds_host='myhost',
- missing_policy=PseudoTranslationPolicy(),
- )
- assert _tx.initialized is True
+ def test_init_uses_given_params(self):
+ _tx.setup(token='mytoken',
+ languages=['lang1', 'lang2'],
+ cds_host='myhost',
+ missing_policy=PseudoTranslationPolicy())
assert _tx._cds_handler.token == 'mytoken'
assert _tx._cds_handler.host == 'myhost'
- assert _tx._languages == ['lang1', 'lang2']
- assert isinstance(_tx._missing_policy, PseudoTranslationPolicy)
-
- def test_initialized_only_once(self, reset_tx):
- # Even if native.init() is called multiple times, only the first one matters
- native.init(
- 'mytoken', ['lang1', 'lang2'], cds_host='myhost',
- missing_policy=PseudoTranslationPolicy(),
- )
- native.init('another_token', ['lang3', 'lang4'], 'another_host')
- assert _tx._cds_handler.token == 'mytoken'
- assert _tx._cds_handler.host == 'myhost'
- assert _tx._languages == ['lang1', 'lang2']
+ assert _tx.hardcoded_language_codes == ['lang1', 'lang2']
assert isinstance(_tx._missing_policy, PseudoTranslationPolicy)
diff --git a/tests/native/django/test_templatetag.py b/tests/native/django/test_templatetag.py
index 1bb6ee9..a678ad5 100644
--- a/tests/native/django/test_templatetag.py
+++ b/tests/native/django/test_templatetag.py
@@ -6,6 +6,8 @@
from transifex.native import tx
from transifex.native.rendering import SourceStringPolicy
+tx.setup(source_language="en_US")
+
def do_test(template_str, context_dict=None, autoescape=True,
lang_code="en-us"):
diff --git a/transifex/native/__init__.py b/transifex/native/__init__.py
index fe3d649..04c1f5e 100644
--- a/transifex/native/__init__.py
+++ b/transifex/native/__init__.py
@@ -1,33 +1,4 @@
from transifex.native.core import TxNative
-
-def init(
- token, languages, secret=None,
- cds_host=None, missing_policy=None,
- error_policy=None
-):
- """Initialize the framework.
-
- :param list languages: A list of language codes for the languages
- the application is localized into
- :param str token: the API token to use for connecting to the CDS
- :param str secret: the additional secret required for pushing translations
- :param str cds_host: an optional host for the Content Delivery Service,
- defaults to the host provided by Transifex
- :param AbstractRenderingPolicy missing_policy: an optional policy to use
- for returning strings when a translation is missing
- :param AbstractErrorPolicy error_policy: an optional policy to use
- for defining how to handle translation rendering errors
- """
- if not tx.initialized:
- tx.init(
- languages,
- token,
- secret=secret,
- cds_host=cds_host,
- missing_policy=missing_policy,
- error_policy=error_policy
- )
-
-
tx = TxNative()
+t = tx.translate
diff --git a/transifex/native/cache.py b/transifex/native/cache.py
index a6f96e0..6ef6f1c 100644
--- a/transifex/native/cache.py
+++ b/transifex/native/cache.py
@@ -51,6 +51,12 @@ def update(self, data):
"""
pass
+ def __contains__(self, language_code):
+ """ Check whether language_code has already been added to the cache.
+ """
+
+ pass
+
class MemoryCache(AbstractCache):
"""A cache that stores translations in memory."""
@@ -81,3 +87,6 @@ def get(self, key, language_code):
except (ValueError, AttributeError):
pass
return retrieved_translation
+
+ def __contains__(self, language_code):
+ return language_code in self._translations_by_lang
diff --git a/transifex/native/cds.py b/transifex/native/cds.py
index 6deceb3..e7f8c6c 100644
--- a/transifex/native/cds.py
+++ b/transifex/native/cds.py
@@ -47,8 +47,7 @@ def get(self, key):
class CDSHandler(object):
"""Handles communication with the Content Delivery Service."""
- def __init__(self, configured_languages, token, secret=None,
- host=TRANSIFEX_CDS_HOST):
+ def __init__(self, **kwargs):
"""Constructor.
:param list configured_languages: a list of language codes for the
@@ -56,12 +55,25 @@ def __init__(self, configured_languages, token, secret=None,
:param str token: the API token to use for connecting to the CDS
:param str host: the host of the Content Delivery Service
"""
- self.configured_language_codes = configured_languages
- self.token = token
- self.secret = secret
- self.host = host or TRANSIFEX_CDS_HOST
+ self.configured_language_codes = None
+ self.token = None
+ self.secret = None
+ self.host = TRANSIFEX_CDS_HOST
self.etags = EtagStore()
+ self.setup(**kwargs)
+
+ def setup(self, configured_languages=None, token=None, secret=None,
+ host=None):
+ if configured_languages is not None:
+ self.configured_language_codes = configured_languages
+ if token is not None:
+ self.token = token
+ if secret is not None:
+ self.secret = secret
+ if host is not None:
+ self.host = host
+
def fetch_languages(self):
"""Fetch the languages defined in the CDS for the specific project.
@@ -106,7 +118,7 @@ def fetch_languages(self):
return languages
- def fetch_translations(self, language_code=None):
+ def fetch_translations(self, language_code):
"""Fetch all translations for the given organization/project/(resource)
associated with the current token. Returns a tuple of refresh flag and
a dictionary of the fetched translations per language.
@@ -121,61 +133,53 @@ def fetch_translations(self, language_code=None):
translations = {}
- if not language_code:
- languages = [lang['code'] for lang in self.fetch_languages()]
- else:
- languages = [language_code]
-
- for language_code in set(languages) & \
- set(self.configured_language_codes):
-
- try:
- last_response_status = 202
- while last_response_status == 202:
- response = requests.get(
- (self.host +
- cds_url.format(language_code=language_code)),
- headers=self._get_headers(
- etag=self.etags.get(language_code)
- )
+ try:
+ last_response_status = 202
+ while last_response_status == 202:
+ response = requests.get(
+ (self.host +
+ cds_url.format(language_code=language_code)),
+ headers=self._get_headers(
+ etag=self.etags.get(language_code)
)
- last_response_status = response.status_code
+ )
+ last_response_status = response.status_code
- if not response.ok:
- logger.error(
- 'Error retrieving translations from CDS: `{}`'.format(
- response.reason
- )
- )
- response.raise_for_status()
-
- # etags indicate that no translation have been updated
- if response.status_code == 304:
- translations[language_code] = (False, {})
- else:
- self.etags.set(
- language_code, response.headers.get('ETag', ''))
- json_content = response.json()
- translations[language_code] = (
- True, json_content['data']
+ if not response.ok:
+ logger.error(
+ 'Error retrieving translations from CDS: `{}`'.format(
+ response.reason
)
+ )
+ response.raise_for_status()
- except (KeyError, ValueError):
- # Compatibility with python2.7 where `JSONDecodeError` doesn't
- # exist
- logger.error('Error retrieving translations from CDS: '
- 'Malformed response') # pragma no cover
- translations[language_code] = (False, {}) # pragma no cover
- except requests.ConnectionError:
- logger.error(
- 'Error retrieving translations from CDS: ConnectionError')
- translations[language_code] = (False, {})
- except Exception as e:
- logger.error(
- 'Error retrieving translations from CDS: UnknownError '
- '(`{}`)'.format(str(e))
- ) # pragma no cover
+ # etags indicate that no translation have been updated
+ if response.status_code == 304:
translations[language_code] = (False, {})
+ else:
+ self.etags.set(
+ language_code, response.headers.get('ETag', ''))
+ json_content = response.json()
+ translations[language_code] = (
+ True, json_content['data']
+ )
+
+ except (KeyError, ValueError):
+ # Compatibility with python2.7 where `JSONDecodeError` doesn't
+ # exist
+ logger.error('Error retrieving translations from CDS: '
+ 'Malformed response') # pragma no cover
+ translations[language_code] = (False, {}) # pragma no cover
+ except requests.ConnectionError:
+ logger.error(
+ 'Error retrieving translations from CDS: ConnectionError')
+ translations[language_code] = (False, {})
+ except Exception as e:
+ logger.error(
+ 'Error retrieving translations from CDS: UnknownError '
+ '(`{}`)'.format(str(e))
+ ) # pragma no cover
+ translations[language_code] = (False, {})
return translations
diff --git a/transifex/native/core.py b/transifex/native/core.py
index 83446fc..57ffbad 100644
--- a/transifex/native/core.py
+++ b/transifex/native/core.py
@@ -6,39 +6,33 @@
from transifex.common.utils import generate_key, parse_plurals
from transifex.native.cache import MemoryCache
from transifex.native.cds import CDSHandler
+from transifex.native.events import EventDispatcher
from transifex.native.rendering import (SourceStringErrorPolicy,
SourceStringPolicy, StringRenderer)
-class NotInitializedError(Exception):
- """Raised when a method of a TxNative instance is called but the class
- hasn't been initialized.
-
- Allows for better debugging when developers neglect to call init().
- """
- pass
-
-
class TxNative(object):
"""The main class of the framework, responsible for orchestrating all
behavior."""
- def __init__(self):
- # The class uses an untypical initialization scheme, defining
- # an init() method, instead of initializing inside the constructor
- # This is necessary for allowing it to be initialized by its clients
- # with proper arguments, while at the same time being very easy
- # to import and use a single "global" instance
- self._cache = None
- self._languages = []
- self._missing_policy = None
- self._cds_handler = None
- self.initialized = False
-
- def init(
- self, languages, token, secret=None, cds_host=None,
- missing_policy=None, error_policy=None
- ):
+ def __init__(self, **kwargs):
+ self.source_language_code = None
+ self.current_language_code = None
+ self.hardcoded_language_codes = None
+ self.remote_languages = None
+
+ self._event_dispatcher = EventDispatcher()
+ self._missing_policy = SourceStringPolicy()
+ self._cds_handler = CDSHandler()
+ self._cache = MemoryCache()
+ self._error_policy = SourceStringErrorPolicy()
+
+ self.setup(**kwargs)
+
+ def setup(self,
+ source_language=None, current_language=None, languages=None,
+ token=None, secret=None, cds_host=None,
+ missing_policy=None, error_policy=None):
"""Create an instance of the core framework class.
Also warms up the cache by fetching the translations from the CDS.
@@ -55,26 +49,87 @@ def init(
:param AbstractErrorPolicy error_policy: an optional policy
to determine how to handle rendering errors
"""
- self._languages = languages
- self._cache = MemoryCache()
- self._missing_policy = missing_policy or SourceStringPolicy()
- self._error_policy = error_policy or SourceStringErrorPolicy()
- self._cds_handler = CDSHandler(
- self._languages, token, secret=secret, host=cds_host
- )
- self.initialized = True
-
- def translate(
- self, source_string, language_code, is_source=False,
- _context=None, escape=True, params=None
- ):
+ if source_language is not None:
+ self.source_language_code = source_language
+ if languages is not None:
+ self.hardcoded_language_codes = languages
+ if missing_policy is not None:
+ self._missing_policy = missing_policy
+ if error_policy is not None:
+ self._error_policy = error_policy
+
+ self._cds_handler.setup(token=token,
+ secret=secret,
+ host=cds_host)
+
+ if current_language is not None:
+ self.set_current_language(current_language)
+ elif source_language is not None:
+ self.set_current_language(source_language)
+
+ def fetch_languages(self, force=False):
+ if self.remote_languages is None or force:
+ self._event_dispatcher.trigger('FETCHING_LOCALES')
+ try:
+ self.remote_languages = self._cds_handler.fetch_languages()
+ except Exception:
+ self._event_dispatcher.trigger('LOCALES_FETCH_FAILED')
+ raise
+ else:
+ self._event_dispatcher.trigger('LOCALES_FETCHED')
+
+ if self.hardcoded_language_codes is not None:
+ return [language
+ for language in self.remote_languages
+ if language['code'] in self.hardcoded_language_codes]
+ else:
+ return self.remote_languages
+
+ def set_current_language(self, language_code, force=False):
+ if language_code not in (language['code']
+ for language in self.fetch_languages()):
+ raise ValueError("Language {} is not supported by the application".
+ format(language_code))
+ if language_code not in self._cache or force:
+ self.fetch_translations(language_code=language_code, force=True)
+ prev = self.current_language_code
+ self.current_language_code = language_code
+ self._event_dispatcher.trigger('LOCALE_CHANGED', prev, language_code)
+
+ def fetch_translations(self, language_code=None, force=False):
+ """Fetch fresh content from the CDS."""
+ if language_code is None:
+ for language in self.fetch_languages():
+ self.fetch_translations(language['code'], force=force)
+ else:
+ if language_code not in [language['code']
+ for language in self.fetch_languages()]:
+ raise ValueError(
+ "Language {} is not supported by the application".
+ format(language_code)
+ )
+ self._event_dispatcher.trigger('FETCHING_TRANSLATIONS',
+ language_code)
+ try:
+ if language_code not in self._cache or force:
+ translations = self._cds_handler.\
+ fetch_translations(language_code)
+ self._cache.update(translations)
+ except Exception:
+ self._event_dispatcher.trigger('TRANSLATIONS_FETCH_FAILED',
+ language_code)
+ raise
+ else:
+ self._event_dispatcher.trigger('TRANSLATIONS_FETCHED',
+ language_code)
+
+ def translate(self, source_string, language_code=None, _context=None,
+ escape=True, params=None):
"""Translate the given string to the provided language.
:param unicode source_string: the source string to get the translation
for e.g. 'Order: {num, plural, one {A table} other {{num} tables}}'
:param str language_code: the language to translate to
- :param bool is_source: a boolean indicating whether `translate`
- is being used for the source language
:param unicode _context: an optional context that accompanies
the string
:param bool escape: if True, the returned string will be HTML-escaped,
@@ -88,12 +143,12 @@ def translate(
if params is None:
params = {}
- self._check_initialization()
+ if language_code is None:
+ language_code = self.current_language_code
translation_template = self.get_translation(source_string,
language_code,
- _context,
- is_source)
+ _context)
return self.render_translation(translation_template,
params,
@@ -101,8 +156,7 @@ def translate(
language_code,
escape)
- def get_translation(self, source_string, language_code, _context,
- is_source=False):
+ def get_translation(self, source_string, language_code, _context):
""" Try to retrieve the translation.
A translation is a serialized source_string with ICU format
@@ -110,7 +164,7 @@ def get_translation(self, source_string, language_code, _context,
'{num, plural, one {Ένα τραπέζι} other {{num} τραπέζια}}'
"""
- if is_source:
+ if language_code == self.source_language_code:
translation_template = source_string
else:
pluralized, plurals = parse_plurals(source_string)
@@ -146,11 +200,6 @@ def render_translation(self, translation_template, params, source_string,
escape=escape, params=params,
)
- def fetch_translations(self):
- """Fetch fresh content from the CDS."""
- self._check_initialization()
- self._cache.update(self._cds_handler.fetch_translations())
-
def push_source_strings(self, strings, purge=False):
"""Push the given source strings to the CDS.
@@ -163,16 +212,12 @@ def push_source_strings(self, strings, purge=False):
response
:rtype: tuple
"""
- self._check_initialization()
response = self._cds_handler.push_source_strings(strings, purge)
return response.status_code, json.loads(response.content)
- def _check_initialization(self):
- """Raise an exception if the class has not been initialized.
+ # Events
+ def on(self, label, callback):
+ self._event_dispatcher.on(label, callback)
- :raise NotInitializedError: if the class hasn't been initialized
- """
- if not self.initialized:
- raise NotInitializedError(
- 'TxNative is not initialized, make sure you call init() first.'
- )
+ def off(self, label, callback):
+ self._event_dispatcher.off(label, callback)
diff --git a/transifex/native/django/apps.py b/transifex/native/django/apps.py
index 2a257e2..0685572 100644
--- a/transifex/native/django/apps.py
+++ b/transifex/native/django/apps.py
@@ -5,7 +5,7 @@
from django.apps import AppConfig
from django.core.signals import request_finished
from django.utils.translation import to_locale
-from transifex.native import init, tx
+from transifex.native import tx
from transifex.native.daemon import daemon
from transifex.native.django import settings as native_settings
from transifex.native.rendering import (parse_error_policy,
@@ -77,14 +77,12 @@ def ready(self):
error_policy = parse_error_policy(
native_settings.TRANSIFEX_ERROR_POLICY
)
- init(
- native_settings.TRANSIFEX_TOKEN,
- languages,
- secret=native_settings.TRANSIFEX_SECRET,
- cds_host=native_settings.TRANSIFEX_CDS_HOST,
- missing_policy=missing_policy,
- error_policy=error_policy
- )
+ tx.setup(token=native_settings.TRANSIFEX_TOKEN,
+ languages=languages,
+ secret=native_settings.TRANSIFEX_SECRET,
+ cds_host=native_settings.TRANSIFEX_CDS_HOST,
+ missing_policy=missing_policy,
+ error_policy=error_policy)
if fetch_translations:
logger.info(
diff --git a/transifex/native/django/templatetags/transifex.py b/transifex/native/django/templatetags/transifex.py
index fb87e75..5e31147 100644
--- a/transifex/native/django/templatetags/transifex.py
+++ b/transifex/native/django/templatetags/transifex.py
@@ -2,7 +2,6 @@
from copy import copy
-from django.conf import settings
from django.template import Library, Node, TemplateSyntaxError
from django.template.base import (BLOCK_TAG_END, BLOCK_TAG_START,
COMMENT_TAG_END, COMMENT_TAG_START,
@@ -207,11 +206,9 @@ def render(self, context):
# ICU template. Then we perform ICU rendering against 'params'.
# Inbetween the two steps, if the tag used was 't' and not 'ut', we
# peform escaping on the ICU template.
- is_source = get_language() == settings.LANGUAGE_CODE
locale = to_locale(get_language()) # e.g. from en-us to en_US
translation_icu_template = tx.get_translation(
source_icu_template, locale, params.get('_context', None),
- is_source,
)
if self.tag_name == "t":
source_icu_template = escape_html(source_icu_template)
diff --git a/transifex/native/django/utils/__init__.py b/transifex/native/django/utils/__init__.py
index dfe522f..3d1346e 100644
--- a/transifex/native/django/utils/__init__.py
+++ b/transifex/native/django/utils/__init__.py
@@ -1,4 +1,3 @@
-from django.conf import settings
from django.utils.translation import get_language, to_locale
from transifex.common.strings import LazyString
from transifex.native import tx
@@ -19,13 +18,11 @@ def translate(_string, _context=None, _escape=True, **params):
:return: the final translation in the current language
:rtype: unicode
"""
- is_source = get_language() == settings.LANGUAGE_CODE
locale = to_locale(get_language()) # e.g. from en-us to en_US
return tx.translate(
_string,
locale,
_context=_context,
- is_source=is_source,
escape=_escape,
params=params,
)
diff --git a/transifex/native/events.py b/transifex/native/events.py
new file mode 100644
index 0000000..763edb6
--- /dev/null
+++ b/transifex/native/events.py
@@ -0,0 +1,25 @@
+class EventDispatcher(object):
+ LABELS = ['FETCHING_TRANSLATIONS', 'TRANSLATIONS_FETCHED',
+ 'TRANSLATIONS_FETCH_FAILED', 'LOCALE_CHANGED',
+ 'FETCHING_LOCALES', 'LOCALES_FETCHED', 'LOCALES_FETCH_FAILED']
+
+ def __init__(self):
+ self.callbacks = {}
+
+ def on(self, label, callback):
+ self._require_label(label)
+ self.callbacks.setdefault(label, set()).add(callback)
+
+ def off(self, label, callback):
+ self._require_label(label)
+ # Can raise KeyError if callback is not there
+ self.callbacks.get(label, set()).remove(callback)
+
+ def trigger(self, label, *args, **kwargs):
+ self._require_label(label)
+ for callback in self.callbacks.get(label, []):
+ callback(*args, **kwargs)
+
+ def _require_label(self, label):
+ if label not in self.LABELS:
+ raise ValueError("Label '{}' is not supported".format(label))
diff --git a/transifex/native/urwid/__init__.py b/transifex/native/urwid/__init__.py
new file mode 100644
index 0000000..253a693
--- /dev/null
+++ b/transifex/native/urwid/__init__.py
@@ -0,0 +1,119 @@
+""" Utilities for integrating urwid applications with Transifex Native. """
+
+import urwid
+from transifex.native import t, tx
+
+
+class Variable(object):
+ """ Holds a value and will trigger events on change.
+
+ >>> v = Variable(1)
+ >>> v.on_change(lambda: print("New value: " + v.get()))
+ >>> v.set(v.get() + 1)
+ <<< # New value: 2
+ """
+
+ def __init__(self, value):
+ self._value = value
+ self._callbacks = set()
+
+ def get(self):
+ return self._value
+
+ def set(self, value):
+ if value != self._value:
+ self._value = value
+ for callback in self._callbacks:
+ callback(value)
+
+ def on_change(self, callback):
+ self._callbacks.add(callback)
+
+ def off_change(self, callback):
+ self._callbacks.remove(callback)
+
+
+class T(urwid.Text):
+ """ Usage:
+
+ Render the string in the current language. Will rerender on language
+ change:
+
+ >>> T("Hello world")
+
+ Render using the variable as template parameter, will rerender on
+ language change and if the parameter changes value:
+
+ >>> variable = Variable("Bob")
+ >>> T("Hello {username}", {'username': variable})
+ >>> variable.set("Jill")
+
+ Render inside an untranslatable wrapper template:
+
+ >>> T("Hello world", wrapper="Translation: {}")
+ """
+
+ def __init__(self, source_string, params=None, wrapper=None, _context=None,
+ _charlimit=None, _comment=None, _occurrences=None, _tags=None,
+ *args, **kwargs):
+ if params is None:
+ params = {}
+
+ self._source_string = source_string
+ self._params = params
+ self._wrapper = wrapper
+
+ tx.on("LOCALE_CHANGED", self.rerender)
+ for key, value in self._params.items():
+ try:
+ value.on_change(self.rerender)
+ except AttributeError:
+ pass
+
+ super().__init__("", *args, **kwargs)
+ self.rerender()
+
+ def rerender(self, *args, **kwargs):
+ params = {}
+ for key, value in self._params.items():
+ try:
+ params[key] = value.get()
+ except AttributeError:
+ params[key] = value
+
+ translation = t(self._source_string, params=params)
+
+ if self._wrapper is not None:
+ self.set_text(self._wrapper.format(translation))
+ else:
+ self.set_text(translation)
+
+
+def language_picker(source_language=None):
+ """ Returns an array of radio buttons for language selection.
+
+ The 'source_language' must be a dictionary describing the source
+ language, with at least the 'name' and 'code' fields. If unset,
+ `{'name': "English", 'code': "en"}` will be used
+ """
+
+ if source_language is None:
+ source_language = {'name': "English", 'code': "en"}
+
+ languages = tx.fetch_languages()
+ if not any((language['code'] == source_language['code']
+ for language in languages)):
+ languages = [source_language] + languages
+
+ language_group, language_radio = [], []
+ for language in languages:
+ button = urwid.RadioButton(language_group, language['name'])
+ urwid.connect_signal(button, 'change', _on_language_select,
+ language['code'])
+ language_radio.append(button)
+ return language_radio
+
+
+def _on_language_select(radio_button, new_state, language_code):
+ if new_state:
+ tx.set_current_language(language_code)