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)