diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8239350..94d03fe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 4 matrix: - python-version: [3.8, 3.9, "3.10"] + python-version: ["3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v1 diff --git a/CHANGES b/CHANGES index ba8e238..0b561af 100644 --- a/CHANGES +++ b/CHANGES @@ -1,14 +1,23 @@ Version History =============== -Version 0.9.9 (unreleased) --------------------------- +Version 0.10.0 +-------------- +* Fix link to polib. (#277, thanks @gamboz) +* Deepl: use the PRO API endpoint when using a PRO API key. (#278, thanks @nullcode) +* Limit supported versions to Django 4.2 and 5.0, using Python 3.9, 3.10, 3.11 and 3.12 + + +Version 0.9.9 +------------- +* Use packaged jQuery instead of the CDN version. (#274, thanks @stephanema1) * Test test_47_azure_ajax_translation: avoid DNS lookup for better isolation. Should fix #233 * Adds Chinese (Simplified) translation. (#266 thanks @chenluyong) -* Test against Django 4.1a +* Test against Django 4.1 and 4.2 * Limit supported versions to Django 3.2 and later, using Python 3.8, 3.9 and 3.10 * Proxy Deepl translations suggestions through the back-end to avoid CORS issues. (#271 thanks @rafaelromon, @biermeester and @matthiask) * Format code with pre-commit +* Replace case sensitivity check with setting (#273 thanks @patroqueeet) Version 0.9.8 diff --git a/LICENSE b/LICENSE index 48ad830..71cca5a 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2008-2010 Marco Bonetti +Copyright (c) 2008-2023 Marco Bonetti Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation diff --git a/README.rst b/README.rst index 0204e96..83dd5ad 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,12 @@ Rosetta is a `Django `_ application that facilita Because it doesn't export any models, Rosetta doesn't create any tables in your project's database. Rosetta can be installed and uninstalled by simply adding and removing a single entry in your project's `INSTALLED_APPS` and a single line in your main ``urls.py`` file. -Note: as of version 0.9.0, django-rosetta requires Django 1.11 or later, as of version 0.9.6, django-rosetta requires Django 2.2 or later, and as of version 0.9.9 django-rosetta supports Django 3.2 or later. +Note: + +* As of version 0.9.0, django-rosetta requires Django 1.11 or later +* As of version 0.9.6, django-rosetta requires Django 2.2 or later +* As of version 0.9.9 django-rosetta supports Django 3.2 or later +* As of version 0.10.0, django-rosetta requires Django 4.2 or later ******** Features diff --git a/docs/conf.py b/docs/conf.py index 0c78b22..c91058c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -60,7 +60,7 @@ master_doc = "index" # General information about the project. project = "Django Rosetta" -copyright = "2008 – 2021 Marco Bonetti and contributors" +copyright = "2008 – 2023 Marco Bonetti and contributors" author = "Marco Bonetti" @@ -73,7 +73,7 @@ release = get_version() # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: diff --git a/docs/settings.rst b/docs/settings.rst index 55d5d08..a26780d 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -27,6 +27,7 @@ Rosetta can be configured via the following parameters, to be defined in your pr * ``ROSETTA_LOGIN_URL``: Use this if you want to override the login URL for rosetta. Defaults to ``settings.LOGIN_URL``. * ``ROSETTA_LANGUAGES``: List of languages that Rosetta will offer to translate. This is useful when you wish to translate a language that is not yet defined in ``settings.LANGUAGES``. Defaults to ``settings.LANGUAGES``. * ``ROSETTA_SHOW_OCCURRENCES``: Determines whether occurrences (where the original text appears) should be shown next to the translations for context. Defaults to ``True``. +* ``ROSETTA_CASE_SENSITIVE_FILESYSTEM``: Overrides auto-detection of case sensitive OS. Defaults to ``None`` which enables auto-detection. Useful when running case sensitive OS (e.g. Ubuntu) in docker on case insensitive OS (e.g. MacOS). diff --git a/docs/usage.rst b/docs/usage.rst index 5d6068e..518d289 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -38,4 +38,4 @@ By default Rosetta hides its own catalog files in the file selection interface ( Acknowledgments *************** -* Rosetta uses the excellent `polib `_ library to parse and handle gettext files. +* Rosetta uses the excellent `polib `_ library to parse and handle gettext files. diff --git a/pyproject.toml b/pyproject.toml index 9a9c3de..7e03953 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ order_by_type = true known_django = "django" known_django_third_party = "django_*" known_first_party = "apps,rosetta" -sections = "FUTURE,STDLIB,THIRDPARTY,DJANGO,DJANGO_THIRD_PARTY,REST_FRAMEWORK,FIRSTPARTY,LOCALFOLDER" +sections = "FUTURE,STDLIB,THIRDPARTY,DJANGO,DJANGO_THIRD_PARTY,FIRSTPARTY,LOCALFOLDER" skip_glob= "**/migrations/**" diff --git a/rosetta/__init__.py b/rosetta/__init__.py index ff477d9..ab09801 100644 --- a/rosetta/__init__.py +++ b/rosetta/__init__.py @@ -1,12 +1,4 @@ -try: - import django - - if django.VERSION[:3] <= (3, 2, 0): - default_app_config = "rosetta.apps.RosettaAppConfig" -except ImportError: - pass - -VERSION = (0, 9, 9) +VERSION = (0, 10, 0) def get_version(limit=3): diff --git a/rosetta/poutil.py b/rosetta/poutil.py index da9d28b..011e4b7 100644 --- a/rosetta/poutil.py +++ b/rosetta/poutil.py @@ -71,11 +71,18 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False) ) ) - case_sensitive_file_system = True - tmphandle, tmppath = tempfile.mkstemp() - if os.path.exists(tmppath.upper()): - # Case insensitive file system. - case_sensitive_file_system = False + # is OS case sensitive? settings preferred over auto detection + case_sensitive_file_system = getattr( + settings, "ROSETTA_CASE_SENSITIVE_FILESYSTEM", None + ) + + # in case of no settings, attempt auto detection + if case_sensitive_file_system is None: + case_sensitive_file_system = True + tmphandle, tmppath = tempfile.mkstemp() + if os.path.exists(tmppath.upper()): + # Case insensitive file system. + case_sensitive_file_system = False # django/locale if django_apps: diff --git a/rosetta/templates/rosetta/base.html b/rosetta/templates/rosetta/base.html index 48611dd..ad9cdfe 100644 --- a/rosetta/templates/rosetta/base.html +++ b/rosetta/templates/rosetta/base.html @@ -11,7 +11,7 @@ {% include 'rosetta/css/rosetta.css' %} {% block extra_styles %}{% endblock %} - + diff --git a/rosetta/templates/rosetta/js/rosetta.js b/rosetta/templates/rosetta/js/rosetta.js index b2f560f..a3e4ab2 100644 --- a/rosetta/templates/rosetta/js/rosetta.js +++ b/rosetta/templates/rosetta/js/rosetta.js @@ -93,7 +93,7 @@ $(document).ready(function() { if($(this).val()) { $('.alert', $(this).parents('tr')).remove(); var RX = /%(?:\([^\s\)]*\))?[sdf]|\{[\w\d_]+?\}/g, - origs=$(this).parents('tr').find('.original>.message').html().match(RX), + origs=$(this).parents('tr').find('.original span').html().match(RX), trads=$(this).val().match(RX), error = $('Unmatched variables'); diff --git a/rosetta/tests/tests.py b/rosetta/tests/tests.py index 9015ad1..ece2fc6 100644 --- a/rosetta/tests/tests.py +++ b/rosetta/tests/tests.py @@ -2,6 +2,7 @@ import filecmp import hashlib import os import shutil +from unittest import mock from urllib.parse import urlencode import vcr @@ -17,6 +18,7 @@ from django.urls import resolve, reverse from django.utils.encoding import force_bytes from rosetta import views +from rosetta.poutil import find_pos from rosetta.signals import entry_changed, post_save from rosetta.storage import get_storage @@ -382,14 +384,16 @@ class RosettaTestCase(TestCase): # Post a translation, it should have properly wrapped lines data = { - "m_bb9d8fe6159187b9ea494c1b313d23d4": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean " - "commodo ligula eget dolor. Aenean massa. Cum sociis natoque " - "penatibus et magnis dis parturient montes, nascetur ridiculus " - "mus. Donec quam felis, ultricies nec, pellentesque eu, pretium " - "quis, sem. Nulla consequat massa quis enim. Donec pede justo, " - "fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, " - "rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum " - "felis eu pede mollis pretium." + "m_bb9d8fe6159187b9ea494c1b313d23d4": ( + "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean " + "commodo ligula eget dolor. Aenean massa. Cum sociis natoque " + "penatibus et magnis dis parturient montes, nascetur ridiculus " + "mus. Donec quam felis, ultricies nec, pellentesque eu, pretium " + "quis, sem. Nulla consequat massa quis enim. Donec pede justo, " + "fringilla vel, aliquet nec, vulputate eget, arcu. In enim justo, " + "rhoncus ut, imperdiet a, venenatis vitae, justo. Nullam dictum " + "felis eu pede mollis pretium." + ) } r = self.client.post(self.xx_form_url, data) with open(self.dest_file, "r") as po_file: @@ -762,8 +766,9 @@ class RosettaTestCase(TestCase): msg_hashes = message_hashes() data = {msg_hashes["String 1"]: "Translation 1"} self.client.post(self.xx_form_url, data) - po_file_hash_before, mo_file_hash_before = file_hash(po_file), file_hash( - mo_file + po_file_hash_before, mo_file_hash_before = ( + file_hash(po_file), + file_hash(mo_file), ) # Make a change to the translations @@ -1053,6 +1058,23 @@ class RosettaTestCase(TestCase): resp = self.client.get(reverse("admin:index")) self.assertNotContains(resp, "rosetta-content-main") + @mock.patch("rosetta.poutil.os.path.exists") + def test_273_override_case_sensitivity(self, path_mock): + path_mock.exists.return_value = False + # no setting + find_pos("en") + path_mock.assert_called_with(mock.ANY) + + path_mock.reset_mock() + with override_settings(ROSETTA_CASE_SENSITIVE_FILESYSTEM=False): + find_pos("en") + path_mock.isfile.assert_not_called() + + path_mock.reset_mock() + with override_settings(ROSETTA_CASE_SENSITIVE_FILESYSTEM=True): + find_pos("en") + path_mock.isfile.assert_not_called() + # Stubbed access control function def no_access(user): diff --git a/rosetta/translate_utils.py b/rosetta/translate_utils.py index 28032c0..b4f2b52 100644 --- a/rosetta/translate_utils.py +++ b/rosetta/translate_utils.py @@ -48,8 +48,13 @@ def translate(text, from_language, to_language): def translate_by_deepl(text, to_language, auth_key): + if auth_key.lower().endswith(":fx"): + endpoint = "https://api-free.deepl.com" + else: + endpoint = "https://api.deepl.com" + r = requests.post( - "https://api-free.deepl.com/v2/translate", + f"{endpoint}/v2/translate", headers={"Authorization": f"DeepL-Auth-Key {auth_key}"}, data={ "target_lang": to_language.upper(), diff --git a/rosetta/urls.py b/rosetta/urls.py index 2843a42..6c53856 100644 --- a/rosetta/urls.py +++ b/rosetta/urls.py @@ -27,12 +27,12 @@ urlpatterns = [ name="rosetta-file-list", ), re_path( - r"^files/(?P[\w-]+)/(?P[\w\-_\.]+)/(?P\d+)/$", + r"^files/(?P[\w-]+)/(?P[\w\-_\.@]+)/(?P\d+)/$", views.TranslationFormView.as_view(), name="rosetta-form", ), re_path( - r"^files/(?P[\w-]+)/(?P[\w\-_\.]+)/(?P\d+)/download/$", + r"^files/(?P[\w-]+)/(?P[\w\-_\.@]+)/(?P\d+)/download/$", views.TranslationFileDownload.as_view(), name="rosetta-download-file", ), diff --git a/setup.py b/setup.py index e8a1cf4..ec01032 100644 --- a/setup.py +++ b/setup.py @@ -52,19 +52,16 @@ setup( "Topic :: Software Development :: Localization", "Topic :: Software Development :: Internationalization", "Framework :: Django", - "Framework :: Django :: 3.0", - "Framework :: Django :: 3.1", - "Framework :: Django :: 3.2", - "Framework :: Django :: 4.0", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", ], include_package_data=True, zip_safe=False, - install_requires=["Django >= 2.2", "requests >= 2.1.0", "polib >= 1.1.0"], + install_requires=["Django >= 4.2", "requests >= 2.30.0", "polib >= 1.1.0"], tests_require=["tox", "vcrpy"], cmdclass={"test": Tox}, ) diff --git a/testproject/fixtures/vcr_cassettes/test_47_2_deeps_ajax_translation.yaml b/testproject/fixtures/vcr_cassettes/test_47_2_deeps_ajax_translation.yaml index c633f57..bab63a5 100644 --- a/testproject/fixtures/vcr_cassettes/test_47_2_deeps_ajax_translation.yaml +++ b/testproject/fixtures/vcr_cassettes/test_47_2_deeps_ajax_translation.yaml @@ -17,7 +17,7 @@ interactions: User-Agent: - python-requests/2.26.0 method: POST - uri: https://api-free.deepl.com/v2/translate + uri: https://api.deepl.com/v2/translate response: body: {string: '{"translations": [{"detected_source_language": "EN", "text": "Salut tout le monde"}]}'} headers: diff --git a/testproject/settings.py b/testproject/settings.py index 0276dd3..c45aa7a 100644 --- a/testproject/settings.py +++ b/testproject/settings.py @@ -1,8 +1,6 @@ import os import sys -import django - SITE_ID = 1 @@ -17,22 +15,13 @@ DATABASES = { } } -if django.VERSION[:3] >= (3, 2, 0): - CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", - "LOCATION": "127.0.0.1:11211", - "KEY_PREFIX": "ROSETTA_TEST", - } - } -else: - CACHES = { - "default": { - "BACKEND": "django.core.cache.backends.memcached.MemcachedCache", - "LOCATION": "127.0.0.1:11211", - "KEY_PREFIX": "ROSETTA_TEST", - } +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.memcached.PyMemcacheCache", + "LOCATION": "127.0.0.1:11211", + "KEY_PREFIX": "ROSETTA_TEST", } +} # CACHES = {'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}} @@ -52,11 +41,9 @@ INSTALLED_APPS = [ "django.contrib.sites", "django.contrib.messages", "rosetta", + "rosetta.tests.test_app.apps.TestAppConfig", ] -if django.VERSION[0:2] >= (1, 7): - INSTALLED_APPS.append("rosetta.tests.test_app.apps.TestAppConfig") - LANGUAGE_CODE = "en" MIDDLEWARE = ( @@ -84,8 +71,6 @@ SILENCED_SYSTEM_CHECKS = ["translation.E002"] LOCALE_PATHS = [os.path.join(PROJECT_PATH, "locale")] -SOUTH_TESTS_MIGRATE = False - FIXTURE_DIRS = (os.path.join(PROJECT_PATH, "fixtures"),) STATIC_URL = "/static/" ROOT_URLCONF = "testproject.urls" @@ -112,6 +97,8 @@ TEMPLATES = [ } ] +USE_TZ = True + STATIC_URL = "/static/" # SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies" diff --git a/tox.ini b/tox.ini index 8bacb0d..dd04665 100644 --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,18 @@ [tox] envlist = flake8, - py{38,39,310}-django{32,40,41}, + py{38,39,310}-django42, + py{310,311,312}-django50, gettext, docs [gh-actions] python = - 3.10: py310-django32, py310-django40, py310-django41 - 3.9: py39-django32, py39-django40, py39-django41 - 3.8: py38-django32, py38-django40, py38-django41 + 3.12: py312-django50 + 3.11: py311-django42, py311-django50 + 3.10: py310-django42, py310-django50 + 3.9: py39-django42 skipsdist = True @@ -27,9 +29,8 @@ setenv = PYTHONDONTWRITEBYTECODE=1 deps = - django32: Django>=3.2,<=3.2.99 - django40: Django>=4.0,<4.1 - django41: Django>=4.1a,<4.2 + django42: Django>=4.2a,<4.3 + django50: Django>=5.0,<5.1 pymemcache requests