├── .gitignore ├── CONTRIBUTING.md ├── MANIFEST.in ├── README.md ├── djangotransifex ├── __init__.py ├── api.py ├── app_settings.py ├── exceptions.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ └── tx.py ├── models.py ├── version.txt └── views.py ├── requirements-dev.txt ├── requirements.txt ├── setup.cfg ├── setup.py ├── test_project ├── __init__.py ├── manage.py ├── settings.py └── urls.py ├── tests ├── __init__.py ├── conftest.py └── test_api.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | djangotransifex.egg-info/ 3 | .tox/ 4 | 5 | # PyCharm editor files 6 | .idea 7 | 8 | # virtualenv 9 | env/ 10 | 11 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Testing 2 | 3 | To run the tests, clone the repository, and then: 4 | 5 | # Setup the virtual environment 6 | virtualenv env 7 | env/bin/activate 8 | # windows users: env\Scripts\activate.bat 9 | pip install -r requirements.txt 10 | pip install -r requirements-dev.txt 11 | 12 | # Run the tests 13 | py.test 14 | 15 | You can also use the excellent [`tox`][tox] testing tool to run the tests against all supported versions of Python and Django. Install `tox` globally, and then simply run: 16 | 17 | tox 18 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include requirements.txt 3 | include djangotransifex/version.txt 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Djangotransifex 2 | =============== 3 | 4 | This app can help you push/pull *project level* translations into Transifex. 5 | 6 | Settings 7 | ======== 8 | You must set the following in your settings file 9 | 10 | # The username for the Transifex server 11 | TRANSIFEX_USERNAME = 'username' 12 | 13 | # The password for the Transifex server 14 | TRANSIFEX_PASSWORD = 'password' 15 | 16 | 17 | The following are optional settings 18 | 19 | 20 | # The Transifex host to use 21 | # default: `https://www.Transifex.net/` 22 | TRANSIFEX_HOST 23 | 24 | # The source language for your strings. 25 | # default: The Django LANGUAGE_CODE setting 26 | SOURCE_LANGUAGE_CODE 27 | 28 | # What prefix to give the resources 29 | # default: No prefix 30 | RESOURCE_PREFIX 31 | 32 | # The slug for the project 33 | # default: `MyProject` 34 | PROJECT_SLUG 35 | 36 | # A set of key/value mappings, where the key is the 37 | # Transifex language code, and the value is the 38 | # Django language code 39 | # default: No mappings 40 | LANGUAGE_MAPPING 41 | 42 | 43 | CHANGELOG 44 | ========= 45 | 46 | 0.1.10 47 | ------ 48 | * Relax requirements versions 49 | 50 | 0.1.9 51 | ----- 52 | Fix pypi version 53 | 54 | 0.1.8 55 | ----- 56 | * Swtich to py.test and tox.ini 57 | 58 | 0.1.7 59 | ----- 60 | * Make `PROJECT_PATH` setting overridable from `settings.py` 61 | * Convert `README.rst` to be markdown 62 | 63 | 0.1.6 64 | ----- 65 | * Make pull_translations and create_project commands print to the command line 66 | 67 | 0.1.5 68 | ----- 69 | * Output the name of the command being run before the command executes 70 | * Make the user confirm on update_source_translations, as it is a destructive action 71 | 72 | 0.1.4 73 | ----- 74 | * Make the user confirm their intentions when they are pushing translations which could destroy work done 75 | on the Transifex server 76 | 77 | 0.1.3 78 | ----- 79 | * Add output on commandline when commands execute sucessfully 80 | * Add example settings into readme 81 | 82 | -------------------------------------------------------------------------------- /djangotransifex/__init__.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | _version_path = os.path.join(os.path.dirname(__file__), 'version.txt') 5 | 6 | VERSION = open(_version_path).read().rstrip() #rstrip() removes newlines 7 | 8 | 9 | try: 10 | import semver 11 | except ImportError: 12 | pass 13 | else: 14 | VERSION_INFO = semver.parse(VERSION) 15 | 16 | -------------------------------------------------------------------------------- /djangotransifex/api.py: -------------------------------------------------------------------------------- 1 | from transifex.api import TransifexAPI 2 | import os 3 | import app_settings 4 | import glob 5 | from transifex.exceptions import TransifexAPIException 6 | import requests 7 | from djangotransifex.exceptions import LanguageCodeNotAllowed, NoPoFilesFound, \ 8 | ProjectNotFound, ResourceNotFound 9 | from django.conf import settings 10 | 11 | class DjangoTransifexAPI(TransifexAPI): 12 | 13 | def upload_source_translations(self, project_slug): 14 | """ 15 | Uploads the current translations as a new source translations. 16 | 17 | This command will create resources if they don't already exist in the 18 | Transifex project 19 | 20 | @param project_slug 21 | the project slug 22 | 23 | @return 24 | """ 25 | 26 | 27 | source_folder = os.path.join( 28 | app_settings.PROJECT_PATH, 'locale', 29 | app_settings.SOURCE_LANGUAGE_CODE, 'LC_MESSAGES' 30 | ) 31 | files = [filepath for filepath in glob.glob(source_folder + '/*.po')] 32 | if len(files) == 0: 33 | # No source files 34 | raise NoPoFilesFound( 35 | 'Could not find any .po files in %r' % (source_folder) 36 | ) 37 | 38 | # First check that the project exists on transifex 39 | if not self.project_exists(project_slug): 40 | self.new_project(slug=project_slug) 41 | 42 | # Second: upload the individual source .po files to resources in that 43 | # project 44 | for filepath in files: 45 | __, filename = os.path.split(filepath) 46 | base_filename = filename.replace('.po', '') 47 | resource_slug = app_settings.RESOURCE_PREFIX + base_filename 48 | 49 | try: 50 | self.update_source_translation( 51 | project_slug=project_slug, resource_slug=resource_slug, 52 | path_to_pofile=filepath 53 | ) 54 | except TransifexAPIException, ex: 55 | if hasattr(ex, 'response') and ex.response.status_code == \ 56 | requests.codes['NOT_FOUND']: 57 | # Resource doesn't exist 58 | # Create a new resource from scratch instead 59 | self.new_resource( 60 | project_slug=project_slug, path_to_pofile=filepath, 61 | resource_slug=resource_slug, 62 | ) 63 | else: 64 | # Unknown exception 65 | raise 66 | 67 | def upload_translations(self, project_slug, language_code): 68 | """ 69 | Uploads the current translations for the given language to Transifex. 70 | 71 | This command will overwrite translations made on the Transifex server 72 | 73 | 74 | @param project_slug 75 | The project slug 76 | @param language_code 77 | The language code to upload. This should be the language code used 78 | in the Django project, not the code used on Transifex. 79 | 80 | @return 81 | None 82 | 83 | @raises LanguageCodeNotAllowed 84 | If the language code given is not listed in settings.LANGUAGES 85 | @raises NoPoFilesFound 86 | If there are no po files in the language folder for the given 87 | language 88 | @raises ProjectNotFound 89 | If the project does not exist on the Transifex server 90 | @raises NoSourceLanguage 91 | If the translation being uploaded doesn't have a source language 92 | translation 93 | """ 94 | if language_code not in [code for code, __ in settings.LANGUAGES]: 95 | raise LanguageCodeNotAllowed(language_code) 96 | 97 | folder = os.path.join( 98 | app_settings.PROJECT_PATH, 'locale', language_code, 'LC_MESSAGES' 99 | ) 100 | files = [filepath for filepath in glob.glob(folder + '/*.po')] 101 | if len(files) == 0: 102 | # No source files 103 | raise NoPoFilesFound( 104 | 'Could not find any .po files in %r' % (folder) 105 | ) 106 | 107 | # First check that the project exists on transifex 108 | if not self.project_exists(project_slug): 109 | raise ProjectNotFound(project_slug) 110 | 111 | # Second: upload the individual .po files to resources in that 112 | # project 113 | for filepath in files: 114 | __, filename = os.path.split(filepath) 115 | base_filename = filename.replace('.po', '') 116 | resource_slug = app_settings.RESOURCE_PREFIX + base_filename 117 | 118 | try: 119 | # Transifex will automatically convert from our language codes 120 | # to theirs 121 | self.new_translation( 122 | project_slug=project_slug, resource_slug=resource_slug, 123 | path_to_pofile=filepath, language_code=language_code 124 | ) 125 | except TransifexAPIException, ex: 126 | if hasattr(ex, 'response') and ex.response.status_code == \ 127 | requests.codes['NOT_FOUND']: 128 | # Resource doesn't exist 129 | raise ResourceNotFound(resource_slug) 130 | else: 131 | # Unknown exception 132 | raise 133 | 134 | def pull_translations(self, project_slug, source_language): 135 | """ 136 | Pull all translations from the remote Transifex server to the local 137 | machine, creating the folders where needed 138 | 139 | @param project_slug 140 | The project slug 141 | @param source_language 142 | The source language code. 143 | This should be the *Transifex* language code 144 | 145 | @return None 146 | 147 | @raises ProjectNotFound 148 | If the project does not exist on the Transifex server 149 | """ 150 | # First check that the project exists on transifex 151 | if not self.project_exists(project_slug): 152 | raise ProjectNotFound(project_slug) 153 | 154 | resources = self.list_resources(project_slug) 155 | for resource in resources: 156 | resource_slug = resource['slug'] 157 | languages = self.list_languages(project_slug, resource_slug) 158 | 159 | # remove the source language code 160 | languages = [ 161 | language for language in languages 162 | if language != source_language 163 | ] 164 | 165 | for transifex_language_code in languages: 166 | local_language_code = self._convert_to_local_language_code( 167 | transifex_language_code 168 | ) 169 | pofile_dir = os.path.join( 170 | app_settings.PROJECT_PATH, 'locale', local_language_code, 171 | 'LC_MESSAGES' 172 | ) 173 | if not os.path.exists(pofile_dir): 174 | os.makedirs(pofile_dir) 175 | path_to_pofile = os.path.join(pofile_dir, resource_slug + '.po') 176 | self.get_translation( 177 | project_slug, resource_slug, transifex_language_code, 178 | path_to_pofile 179 | ) 180 | 181 | def _convert_to_local_language_code(self, transifex_language_code): 182 | """ 183 | Converts a transifex language code to a local one 184 | """ 185 | return app_settings.LANGUAGE_MAPPING.get( 186 | transifex_language_code, transifex_language_code 187 | ) -------------------------------------------------------------------------------- /djangotransifex/app_settings.py: -------------------------------------------------------------------------------- 1 | from django.conf import settings 2 | from django.core.exceptions import ImproperlyConfigured 3 | from django.utils.functional import lazy 4 | 5 | TRANSIFEX_USERNAME = getattr(settings, 'TRANSIFEX_USERNAME', None) 6 | if TRANSIFEX_USERNAME is None: 7 | raise ImproperlyConfigured('You must set setting %r' % ( 8 | 'TRANSIFEX_USERNAME' 9 | )) 10 | 11 | TRANSIFEX_PASSWORD = getattr(settings, 'TRANSIFEX_PASSWORD', None) 12 | if TRANSIFEX_PASSWORD is None: 13 | raise ImproperlyConfigured('You must set setting %r' % ( 14 | 'TRANSIFEX_PASSWORD' 15 | )) 16 | 17 | 18 | TRANSIFEX_HOST = getattr(settings, 'TRANSIFEX_HOST', 'https://www.transifex.net/') 19 | SOURCE_LANGUAGE_CODE = getattr(settings, 'TRANSIFEX_SOURCE_LANGUAGE', settings.LANGUAGE_CODE) 20 | RESOURCE_PREFIX = getattr(settings, 'TRANSIFEX_RESOURCE_PREFIX', '') 21 | PROJECT_SLUG = getattr(settings, 'TRANSIFEX_PROJECT_SLUG', 'MyProject') 22 | 23 | 24 | # Transifex might use different language codes to that used in the Django app. 25 | # This dictionary will convert between them. The key is the transifex code, 26 | # the value should be a string corresponding to the Django code 27 | LANGUAGE_MAPPING = getattr(settings, 'TRANSIFEX_LANGUAGE_MAPPING', {}) 28 | 29 | def _get_project_path(): 30 | from django.utils.importlib import import_module 31 | import os 32 | parts = settings.SETTINGS_MODULE.split('.') 33 | project = import_module(parts[0]) 34 | projectpath = os.path.abspath(os.path.dirname(project.__file__)) 35 | return projectpath 36 | 37 | # Not strictly a setting, but this is a good place to keep it 38 | PROJECT_PATH = getattr(settings, 'TRANSIFEX_PROJECT_PATH', 39 | lazy(_get_project_path)) 40 | 41 | -------------------------------------------------------------------------------- /djangotransifex/exceptions.py: -------------------------------------------------------------------------------- 1 | class DjangoTransifexException(Exception): 2 | pass 3 | 4 | class DjangoTransifexAPIException(DjangoTransifexException): 5 | pass 6 | 7 | class LanguageCodeNotAllowed(DjangoTransifexException): 8 | pass 9 | 10 | class NoPoFilesFound(DjangoTransifexException): 11 | pass 12 | 13 | class ProjectNotFound(DjangoTransifexException): 14 | pass 15 | 16 | class ResourceNotFound(DjangoTransifexException): 17 | pass 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /djangotransifex/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakul/django-transifex/e05cb04924dd6c3acf61c3b1e605f85164185417/djangotransifex/management/__init__.py -------------------------------------------------------------------------------- /djangotransifex/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakul/django-transifex/e05cb04924dd6c3acf61c3b1e605f85164185417/djangotransifex/management/commands/__init__.py -------------------------------------------------------------------------------- /djangotransifex/management/commands/tx.py: -------------------------------------------------------------------------------- 1 | """ 2 | txpush.py 3 | 4 | Push source translations to transifex. 5 | """ 6 | 7 | from django.core.management.base import NoArgsCommand, BaseCommand, CommandError 8 | from djangotransifex import app_settings 9 | from djangotransifex.api import DjangoTransifexAPI 10 | import random 11 | import sys 12 | from textwrap import dedent 13 | import inspect 14 | 15 | 16 | class Command(BaseCommand): 17 | ## Settings ## 18 | project_slug = app_settings.PROJECT_SLUG 19 | resource_prefix = app_settings.RESOURCE_PREFIX 20 | source_language = app_settings.SOURCE_LANGUAGE_CODE 21 | username = app_settings.TRANSIFEX_USERNAME 22 | password = app_settings.TRANSIFEX_PASSWORD 23 | host = app_settings.TRANSIFEX_HOST 24 | 25 | @property 26 | def help(self): 27 | """ 28 | Dynamically generate the help based on the available methods of this 29 | object 30 | """ 31 | help_string = getattr(self, '__help', None) 32 | if help_string is None: 33 | help_string = dedent(""" 34 | Available Commands: 35 | """) 36 | 37 | transifex_methods = [ 38 | method for method in dir(self) 39 | if method.startswith('transifex_') 40 | and callable(getattr(self, method)) 41 | ] 42 | for method_name in transifex_methods: 43 | reduced_method_name = method_name.replace('transifex_', '') 44 | help_string += ' * %s\n' % (reduced_method_name) 45 | 46 | # Check for a docstring 47 | method = getattr(self, method_name) 48 | doc = inspect.getdoc(method) 49 | for line in doc.split('\n'): 50 | help_string += ' %s\n' % (line) 51 | 52 | self.__help = help_string 53 | return help_string 54 | 55 | def usage(self, subcommand): 56 | """ 57 | Return a brief description of how to use this command, by 58 | default from the attribute ``self.help``. 59 | """ 60 | usage = '%%prog %s command [options] %s' % (subcommand, self.args) 61 | if self.help: 62 | return '%s\n\n%s' % (usage, self.help) 63 | else: 64 | return usage 65 | 66 | def handle(self, *args, **options): 67 | if len(args) == 0: 68 | raise CommandError('You must give the name of an action to perform') 69 | command = args[0] 70 | command_func = getattr(self, 'transifex_%s' % (command), None) 71 | if command_func is None: 72 | raise CommandError('Unknown command %r' % (command)) 73 | 74 | print('Executing {0} on project {1}'.format(command, self.project_slug)) 75 | command_func(*args[1:], **options) 76 | 77 | @property 78 | def api(self): 79 | """ 80 | Create an api instance 81 | """ 82 | if not hasattr(self, '_api'): 83 | self._api = DjangoTransifexAPI( 84 | username=self.username, password=self.password, host=self.host 85 | ) 86 | #TODO: Do a ping here 87 | return self._api 88 | 89 | def transifex_upload_source_translations(self, *args, **options): 90 | """ 91 | Usage: ./manage.py tx upload_source_translation [options] 92 | Upload the source translation to Transifex. 93 | """ 94 | self.confirm_command(app_settings.SOURCE_LANGUAGE_CODE) 95 | self.api.upload_source_translations(project_slug=self.project_slug) 96 | 97 | def transifex_upload_translations(self, *args, **options): 98 | """ 99 | Usage: ./manage.py tx upload_translations language_code [options] 100 | Upload the translations for the given language to Transifex. 101 | This will overwrite any translations made on the Transifex server 102 | """ 103 | if len(args) == 0: 104 | raise CommandError('Please provide the language code to upload') 105 | language_code = args[0] 106 | self.confirm_command(language_code) 107 | 108 | self.api.upload_translations( 109 | project_slug=self.project_slug, language_code=language_code 110 | ) 111 | 112 | def _choose_word(self): 113 | with open('/usr/share/dict/words') as dict_file: 114 | dict_words = dict_file.readlines() 115 | 116 | word = None 117 | while word is None: 118 | word = random.choice(dict_words).replace('\n', '') 119 | if len(word) < 5: 120 | word = None 121 | 122 | return word 123 | 124 | def confirm_command(self, language_code): 125 | safety_word = self._choose_word() 126 | 127 | print(( 128 | 'This command will delete the existing "{0}" translations in the ' 129 | '"{1}" project').format(language_code, self.project_slug) 130 | ) 131 | print( 132 | 'THIS IS NOT REVERSIBLE. YOU MAY LOSE DATA WHICH CANNOT BE ' 133 | 'REGENERATED' 134 | ) 135 | print( 136 | 'To continue, please type this word exactly as it appears - {0}'\ 137 | .format(safety_word) 138 | ) 139 | typed_word = raw_input('> ') 140 | 141 | if typed_word == safety_word: 142 | return 143 | else: 144 | print('Word entered incorrectly.') 145 | print('No upload performed.') 146 | sys.exit(1) 147 | 148 | def transifex_pull_translations(self, *args, **kwargs): 149 | """ 150 | Usage: ./manage.py tx pull_translations [options] 151 | Pull all translations from the Transifex server to the local machine. 152 | This will overwrite any translations made locally 153 | """ 154 | print( 155 | 'Pulling translations for "{0}" project, in "{1}" source language'\ 156 | .format(self.project_slug, self.source_language) 157 | ) 158 | self.api.pull_translations( 159 | project_slug=self.project_slug, source_language=self.source_language 160 | ) 161 | print('Translations pulled') 162 | 163 | def transifex_ping(self, *args, **kwargs): 164 | """ 165 | Ping the server to verify connection details and auth details 166 | """ 167 | print(self.api.ping()) 168 | 169 | def transifex_create_project(self, *args, **kwargs): 170 | """ 171 | Usage: ./manage.py tx create_project 172 | Create the project on Transifex. 173 | """ 174 | print( 175 | 'Creating project "{0}" with source language "{1}"'\ 176 | .format(self.project_slug, self.source_language) 177 | ) 178 | self.api.new_project( 179 | slug=self.project_slug, name=self.project_slug, 180 | source_language_code=self.source_language 181 | ) 182 | print('Project created') 183 | 184 | -------------------------------------------------------------------------------- /djangotransifex/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /djangotransifex/version.txt: -------------------------------------------------------------------------------- 1 | 0.1.10 2 | -------------------------------------------------------------------------------- /djangotransifex/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /requirements-dev.txt: -------------------------------------------------------------------------------- 1 | # Test requirements 2 | mock==1.0.1 3 | pytest==2.6.4 4 | pytest-django==2.7 5 | 6 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | django 2 | python-transifex>=0.1.7 -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import sys 3 | import os 4 | 5 | from djangotransifex import VERSION 6 | 7 | 8 | if sys.argv[-1] == 'publish-to-pypi': 9 | os.system("python setup.py sdist upload -r pypi") 10 | os.system("git tag -a %s -m 'version %s'" % (VERSION, VERSION)) 11 | os.system("git push --tags") 12 | sys.exit() 13 | 14 | 15 | if sys.argv[-1] == 'publish-to-pypitest': 16 | os.system("python setup.py sdist upload -r pypitest") 17 | sys.exit() 18 | 19 | 20 | if sys.argv[-1] == 'install-from-pypitest': 21 | os.system("pip install -i https://testpypi.python.org/pypi djangotransifex") 22 | sys.exit() 23 | 24 | 25 | if sys.argv[-1] == 'check-import': 26 | os.system("pip install --upgrade djangotransifex && python -c \"import djangotransifex; print(djangotransifex.VERSION)\"") 27 | sys.exit() 28 | 29 | 30 | setup( 31 | name='djangotransifex', 32 | version=VERSION, 33 | description='A Django api to Transifex', 34 | author='Craig Blaszczyk', 35 | author_email='masterjakul@gmail.com', 36 | url='https://github.com/jakul/django-transifex', 37 | license='BSD', 38 | packages=find_packages(), 39 | tests_require=[ 40 | ], 41 | install_requires=open('requirements.txt').read(), 42 | zip_safe=False, 43 | include_package_data=True, 44 | classifiers=[ 45 | 'Environment :: Web Environment', 46 | 'Framework :: Django', 47 | 'Intended Audience :: Developers', 48 | 'License :: OSI Approved :: BSD License', 49 | 'Operating System :: OS Independent', 50 | 'Programming Language :: Python', 51 | 'Topic :: Software Development :: Libraries :: Python Modules', 52 | ], 53 | ) 54 | -------------------------------------------------------------------------------- /test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakul/django-transifex/e05cb04924dd6c3acf61c3b1e605f85164185417/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | import imp 4 | try: 5 | imp.find_module('settings') # Assumed to be in the same directory. 6 | except ImportError: 7 | import sys 8 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n" % __file__) 9 | sys.exit(1) 10 | 11 | import settings 12 | 13 | if __name__ == "__main__": 14 | execute_manager(settings) 15 | -------------------------------------------------------------------------------- /test_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for mysite project. 2 | import sys 3 | sys.path.insert(0,'/p/python-transifex') 4 | DEBUG = True 5 | TEMPLATE_DEBUG = DEBUG 6 | 7 | ADMINS = ( 8 | # ('Your Name', 'your_email@example.com'), 9 | ) 10 | 11 | MANAGERS = ADMINS 12 | 13 | DATABASES = { 14 | 'default': { 15 | 'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 16 | 'NAME': '', # Or path to database file if using sqlite3. 17 | 'USER': '', # Not used with sqlite3. 18 | 'PASSWORD': '', # Not used with sqlite3. 19 | 'HOST': '', # Set to empty string for localhost. Not used with sqlite3. 20 | 'PORT': '', # Set to empty string for default. Not used with sqlite3. 21 | } 22 | } 23 | 24 | # Local time zone for this installation. Choices can be found here: 25 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 26 | # although not all choices may be available on all operating systems. 27 | # On Unix systems, a value of None will cause Django to use the same 28 | # timezone as the operating system. 29 | # If running in a Windows environment this must be set to the same as your 30 | # system time zone. 31 | TIME_ZONE = 'Europe/London' 32 | 33 | # Language code for this installation. All choices can be found here: 34 | # http://www.i18nguy.com/unicode/language-identifiers.html 35 | LANGUAGE_CODE = 'en-gb' 36 | LANGUAGES = (('en-gb', 'English'), ('it', 'Italian')) 37 | 38 | SITE_ID = 1 39 | 40 | # If you set this to False, Django will make some optimizations so as not 41 | # to load the internationalization machinery. 42 | USE_I18N = True 43 | 44 | # If you set this to False, Django will not format dates, numbers and 45 | # calendars according to the current locale 46 | USE_L10N = True 47 | 48 | # Absolute filesystem path to the directory that will hold user-uploaded files. 49 | # Example: "/home/media/media.lawrence.com/media/" 50 | MEDIA_ROOT = '' 51 | 52 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 53 | # trailing slash. 54 | # Examples: "http://media.lawrence.com/media/", "http://example.com/media/" 55 | MEDIA_URL = '' 56 | 57 | # Absolute path to the directory static files should be collected to. 58 | # Don't put anything in this directory yourself; store your static files 59 | # in apps' "static/" subdirectories and in STATICFILES_DIRS. 60 | # Example: "/home/media/media.lawrence.com/static/" 61 | STATIC_ROOT = '' 62 | 63 | # URL prefix for static files. 64 | # Example: "http://media.lawrence.com/static/" 65 | STATIC_URL = '/static/' 66 | 67 | # URL prefix for admin static files -- CSS, JavaScript and images. 68 | # Make sure to use a trailing slash. 69 | # Examples: "http://foo.com/static/admin/", "/static/admin/". 70 | ADMIN_MEDIA_PREFIX = '/static/admin/' 71 | 72 | # Additional locations of static files 73 | STATICFILES_DIRS = ( 74 | # Put strings here, like "/home/html/static" or "C:/www/django/static". 75 | # Always use forward slashes, even on Windows. 76 | # Don't forget to use absolute paths, not relative paths. 77 | ) 78 | 79 | # List of finder classes that know how to find static files in 80 | # various locations. 81 | STATICFILES_FINDERS = ( 82 | 'django.contrib.staticfiles.finders.FileSystemFinder', 83 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 84 | # 'django.contrib.staticfiles.finders.DefaultStorageFinder', 85 | ) 86 | 87 | # Make this unique, and don't share it with anybody. 88 | SECRET_KEY = '3754wmvo((qk3o_+j+&=qhkp4q3*gber^isq3zv^8xaak@@t2p' 89 | 90 | # List of callables that know how to import templates from various sources. 91 | TEMPLATE_LOADERS = ( 92 | 'django.template.loaders.filesystem.Loader', 93 | 'django.template.loaders.app_directories.Loader', 94 | # 'django.template.loaders.eggs.Loader', 95 | ) 96 | 97 | MIDDLEWARE_CLASSES = ( 98 | 'django.middleware.common.CommonMiddleware', 99 | 'django.contrib.sessions.middleware.SessionMiddleware', 100 | 'django.middleware.csrf.CsrfViewMiddleware', 101 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 102 | 'django.contrib.messages.middleware.MessageMiddleware', 103 | ) 104 | 105 | ROOT_URLCONF = 'urls' 106 | 107 | TEMPLATE_DIRS = ( 108 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 109 | # Always use forward slashes, even on Windows. 110 | # Don't forget to use absolute paths, not relative paths. 111 | ) 112 | 113 | INSTALLED_APPS = ( 114 | 'django.contrib.auth', 115 | 'django.contrib.contenttypes', 116 | 'django.contrib.sessions', 117 | 'django.contrib.sites', 118 | 'django.contrib.messages', 119 | 'django.contrib.staticfiles', 120 | # Uncomment the next line to enable the admin: 121 | # 'django.contrib.admin', 122 | # Uncomment the next line to enable admin documentation: 123 | # 'django.contrib.admindocs', 124 | 'djangotransifex', 125 | ) 126 | 127 | # A sample logging configuration. The only tangible logging 128 | # performed by this configuration is to send an email to 129 | # the site admins on every HTTP 500 error. 130 | # See http://docs.djangoproject.com/en/dev/topics/logging for 131 | # more details on how to customize your logging configuration. 132 | LOGGING = { 133 | 'version': 1, 134 | 'disable_existing_loggers': False, 135 | 'handlers': { 136 | 'mail_admins': { 137 | 'level': 'ERROR', 138 | 'class': 'django.utils.log.AdminEmailHandler' 139 | } 140 | }, 141 | 'loggers': { 142 | 'django.request': { 143 | 'handlers': ['mail_admins'], 144 | 'level': 'ERROR', 145 | 'propagate': True, 146 | }, 147 | } 148 | } 149 | 150 | 151 | 152 | 153 | TRANSIFEX_USERNAME = 'abcde' 154 | TRANSIFEX_PASSWORD = 'abcde' 155 | TRANSIFEX_LANGUAGE_MAPPING = {'en_GB': 'en-gb', 'it_IT': 'it'} -------------------------------------------------------------------------------- /test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import patterns, include, url 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | # Examples: 9 | # url(r'^$', 'mysite.views.home', name='home'), 10 | # url(r'^mysite/', include('mysite.foo.urls')), 11 | 12 | # Uncomment the admin/doc line below to enable admin documentation: 13 | # url(r'^admin/doc/', include('django.contrib.admindocs.urls')), 14 | 15 | # Uncomment the next line to enable the admin: 16 | # url(r'^admin/', include(admin.site.urls)), 17 | ) 18 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jakul/django-transifex/e05cb04924dd6c3acf61c3b1e605f85164185417/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | def pytest_configure(): 2 | from django.conf import settings 3 | 4 | settings.configure( 5 | DEBUG_PROPAGATE_EXCEPTIONS=True, 6 | DATABASES={'default': {'ENGINE': 'django.db.backends.sqlite3', 7 | 'NAME': ':memory:'}}, 8 | SITE_ID=1, 9 | SECRET_KEY='not very secret in tests', 10 | USE_I18N=True, 11 | USE_L10N=True, 12 | ROOT_URLCONF='tests.urls', 13 | INSTALLED_APPS=( 14 | 'django.contrib.auth', 15 | 'django.contrib.contenttypes', 16 | 'django.contrib.sessions', 17 | 'django.contrib.sites', 18 | 'django.contrib.messages', 19 | 'django.contrib.staticfiles', 20 | 21 | 'djangotransifex', 22 | 'tests', 23 | ), 24 | TRANSIFEX_USERNAME = 'abcde', 25 | TRANSIFEX_PASSWORD = 'abcde', 26 | TRANSIFEX_LANGUAGE_MAPPING = {'en_GB': 'en-gb', 'it_IT': 'it'}, 27 | LANGUAGE_CODE = 'en-gb', 28 | LANGUAGES = (('en-gb', 'English'), ('it', 'Italian')), 29 | TRANSIFEX_PROJECT_PATH = '.' 30 | ) 31 | 32 | try: 33 | import django 34 | django.setup() 35 | except AttributeError: 36 | pass 37 | -------------------------------------------------------------------------------- /tests/test_api.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from mock import patch, MagicMock, Mock 3 | from djangotransifex.api import DjangoTransifexAPI 4 | from django.utils.unittest.case import skip 5 | from djangotransifex.exceptions import NoPoFilesFound, LanguageCodeNotAllowed,\ 6 | ProjectNotFound, ResourceNotFound 7 | import json 8 | 9 | class _BaseDjangoTransifexAPITest(TestCase): 10 | 11 | def setUp(self): 12 | data = { 13 | 'username': 'aaa', 'password': 'aaa', 14 | 'host': 'http://www.areallynonexistentdomaiihope.com' 15 | } 16 | self.api = DjangoTransifexAPI(**data) 17 | 18 | def _mock_response(self, status_code, content=None): 19 | response = Mock() 20 | response.status_code = status_code 21 | if content: 22 | response.content = json.dumps(content) 23 | return response 24 | 25 | class DjangoTransifexAPITest(_BaseDjangoTransifexAPITest): 26 | 27 | def setUp(self): 28 | super(DjangoTransifexAPITest, self).setUp() 29 | 30 | self.patchers = {} 31 | self.patchers['mock_open'] = patch('__builtin__.open', create=True 32 | ) 33 | self.patchers['mock_glob'] = patch('glob.glob') 34 | self.patchers['mock_requests_put'] = patch('requests.put') 35 | self.patchers['mock_requests_post'] = patch('requests.post') 36 | self.patchers['mock_project_exists'] = patch( 37 | 'djangotransifex.api.DjangoTransifexAPI.project_exists' 38 | ) 39 | self.patchers['mock_new_project'] = patch( 40 | 'djangotransifex.api.DjangoTransifexAPI.new_project' 41 | ) 42 | 43 | for name, patcher in self.patchers.items(): 44 | setattr(self, name, patcher.start()) 45 | 46 | self.mock_glob.return_value = [ 47 | '/a/b/c/locale/en-gb/LC_MESSAGES/django.po', 48 | '/a/b/c/locale/en-gb/LC_MESSAGES/djangojs.po', 49 | '/a/b/c/locale/en-gb/LC_MESSAGES/another.po', 50 | ] 51 | file_contents = 'aaaaaa\nggggg' 52 | self.mock_open.return_value = MagicMock(spec=file) 53 | self.mock_open.return_value.read = lambda: file_contents 54 | 55 | def tearDown(self): 56 | for patcher in self.patchers.values(): 57 | patcher.stop() 58 | 59 | def test_upload_source_translations(self): 60 | """ 61 | Ensure that `upload_source_translations` works 62 | """ 63 | self.mock_requests_put.side_effect = lambda *args,**kwargs: \ 64 | self._mock_response(200, {'a':1}) 65 | self.mock_project_exists.return_value = True 66 | 67 | self.api.upload_source_translations(project_slug='aaa') 68 | 69 | self.assertEqual(self.mock_requests_put.call_count, 3) 70 | 71 | def test_upload_source_translations_no_files_found(self): 72 | """ 73 | Ensure that `upload_source_translations` works when there are no 74 | localisation files in the source language 75 | """ 76 | self.mock_glob.return_value = [] 77 | 78 | self.assertRaises( 79 | NoPoFilesFound, self.api.upload_source_translations, 80 | project_slug='aaa' 81 | ) 82 | 83 | def test_upload_source_translations_no_project(self): 84 | """ 85 | Ensure that `upload_source_translations` works when there project 86 | has not been created 87 | """ 88 | self.mock_requests_put.side_effect = lambda *args,**kwargs: \ 89 | self._mock_response(200, {'a':1}) 90 | self.mock_project_exists.return_value = False 91 | self.mock_new_project.return_value = None 92 | 93 | self.api.upload_source_translations(project_slug='aaa') 94 | 95 | self.assertEqual(self.mock_requests_put.call_count, 3) 96 | self.assertTrue(self.mock_new_project.called) 97 | 98 | def test_upload_source_translations_no_resources(self): 99 | """ 100 | Ensure that `upload_source_translations` works when the resources 101 | being uploaded haven't been created 102 | """ 103 | self.mock_requests_put.side_effect = lambda *args,**kwargs: \ 104 | self._mock_response(404) 105 | self.mock_requests_post.side_effect = lambda *args,**kwargs: \ 106 | self._mock_response(201) 107 | self.mock_project_exists.return_value = True 108 | 109 | self.api.upload_source_translations(project_slug='aaa') 110 | self.assertEqual(self.mock_requests_post.call_count, 3) 111 | 112 | @patch('djangotransifex.api.DjangoTransifexAPI.new_translation') 113 | def test_upload_translations(self, mock_new_translation): 114 | """ 115 | Ensure that the `upload_translations` function works 116 | """ 117 | self.mock_project_exists.return_value = True 118 | 119 | self.api.upload_translations(project_slug='aaa', language_code='it') 120 | 121 | self.assertTrue(mock_new_translation.called) 122 | 123 | 124 | def test_upload_translations_bad_language_code(self): 125 | """ 126 | Ensure that the `upload_translations` function works when a bad language 127 | code is provided 128 | """ 129 | self.assertRaises( 130 | LanguageCodeNotAllowed, self.api.upload_translations, 131 | project_slug='aaa', language_code='abc' 132 | ) 133 | 134 | def test_upload_translations_no_po_files(self): 135 | """ 136 | Ensure that the `upload_translations` function works when there are no 137 | source po files 138 | """ 139 | self.mock_glob.return_value = [] 140 | 141 | self.assertRaises( 142 | NoPoFilesFound, self.api.upload_translations, 143 | project_slug='aaa', language_code='it' 144 | ) 145 | 146 | def test_upload_translations_project_not_found(self): 147 | """ 148 | Ensure that the `upload_translations` function works when Transifex 149 | cannot find the project 150 | """ 151 | self.mock_project_exists.return_value = False 152 | 153 | self.assertRaises( 154 | ProjectNotFound, self.api.upload_translations, 155 | project_slug='aaa', language_code='it' 156 | ) 157 | 158 | def test_upload_translations_resource_not_found(self): 159 | """ 160 | Ensure that the `upload_translations` function works when the resource 161 | is not found 162 | """ 163 | self.mock_requests_put.side_effect = lambda *args,**kwargs: \ 164 | self._mock_response(404) 165 | 166 | self.assertRaises( 167 | ResourceNotFound, self.api.upload_translations, 168 | project_slug='aaa', language_code='it' 169 | ) 170 | 171 | 172 | class DjangoTransifexAPIPullTranslationsTest(_BaseDjangoTransifexAPITest): 173 | """ 174 | Test the `pull_translations` function of the Django Transifex API 175 | """ 176 | 177 | def setUp(self): 178 | super(DjangoTransifexAPIPullTranslationsTest, self).setUp() 179 | 180 | #setup patches 181 | self.patchers = {} 182 | self.patchers['mock_list_resources'] = patch( 183 | 'djangotransifex.api.DjangoTransifexAPI.list_resources' 184 | ) 185 | self.patchers['mock_list_languages'] = patch( 186 | 'djangotransifex.api.DjangoTransifexAPI.list_languages' 187 | ) 188 | self.patchers['mock_exists'] = patch('os.path.exists') 189 | self.patchers['mock_makedirs'] = patch('os.makedirs') 190 | self.patchers['mock_get_translation'] = patch( 191 | 'djangotransifex.api.DjangoTransifexAPI.get_translation' 192 | ) 193 | self.patchers['mock_project_exists'] = patch( 194 | 'djangotransifex.api.DjangoTransifexAPI.project_exists' 195 | ) 196 | 197 | for name, patcher in self.patchers.items(): 198 | setattr(self, name, patcher.start()) 199 | 200 | def tearDown(self): 201 | for patcher in self.patchers.values(): 202 | patcher.stop() 203 | 204 | 205 | def test_pull_translations(self): 206 | """ 207 | Ensure that the `pull_translations` function works 208 | """ 209 | self.mock_list_resources.return_value = [ 210 | {'slug': 'abc'}, {'slug': 'def'} 211 | ] 212 | self.mock_list_languages.return_value = ['en_GB', 'it'] 213 | self.mock_exists.return_value = True 214 | 215 | self.api.pull_translations(project_slug='abc', source_language='en-gb') 216 | 217 | self.assertEqual(self.mock_get_translation.call_count, 4) 218 | 219 | def test_pull_translations_no_project(self): 220 | """ 221 | Ensure that the `pull_translations` function works when Transifex 222 | cannot find the project 223 | """ 224 | self.mock_project_exists.return_value = False 225 | 226 | self.assertRaises( 227 | ProjectNotFound, self.api.pull_translations, 228 | project_slug='aaa', source_language='it' 229 | ) 230 | 231 | def test_pull_translations_no_folders(self): 232 | 233 | """ 234 | Ensure that the `pull_translations` function works when there are no 235 | locale folders on the local machine 236 | """ 237 | self.mock_project_exists.return_value = True 238 | self.mock_list_resources.return_value = [ 239 | {'slug': 'abc'}, {'slug': 'def'} 240 | ] 241 | self.mock_list_languages.return_value = ['en_GB', 'it'] 242 | self.mock_exists.return_value = False 243 | 244 | self.api.pull_translations(project_slug='abc', source_language='en-gb') 245 | 246 | self.assertEqual(self.mock_get_translation.call_count, 4) 247 | self.assertEqual(self.mock_makedirs.call_count, 4) 248 | 249 | def test_pull_translations_different_local_language_code(self): 250 | """ 251 | Ensure that the `pull_translations` function works for a language which 252 | has a different local language code to transifex one 253 | """ 254 | self.mock_project_exists.return_value = True 255 | self.mock_list_resources.return_value = [{'slug': 'abc'}] 256 | self.mock_list_languages.return_value = ['en_GB','it_IT'] 257 | self.mock_exists.return_value = True 258 | 259 | self.api.pull_translations(project_slug='abc', source_language='en_GB') 260 | 261 | self.assertEqual(self.mock_get_translation.call_count, 1) 262 | saved_pofile_path = self.mock_get_translation.call_args[0][3] 263 | self.assertTrue(saved_pofile_path.endswith('it/LC_MESSAGES/abc.po')) 264 | 265 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | {py26,py27}-django{14,15,16} 4 | 5 | [testenv] 6 | commands = py.test 7 | setenv = 8 | PYTHONDONTWRITEBYTECODE=1 9 | deps = 10 | django14: Django==1.4.17 11 | django15: Django==1.5.12 12 | django16: Django==1.6.9 13 | django17: Django==1.7.2 14 | mock==1.0.1 15 | python-transifex==0.1.7 16 | pytest==2.6.4 17 | pytest-django==2.7 18 | --------------------------------------------------------------------------------