proxy deeply through the backend. Fixes #271

This commit is contained in:
Marco Bonetti 2023-01-01 14:17:08 +01:00
parent 8d91136d56
commit 020903d4cc
38 changed files with 801 additions and 678 deletions

View file

@ -1,3 +1,3 @@
- Which version of Django are you using?:
- Which version of Django are you using?:
- Which version of django-rosetta are you using?:
- Have you looked trough [recent issues](https://github.com/mbi/django-rosetta/issues?utf8=%E2%9C%93&q=is%3Aissue) and checked this isn't a duplicate?

29
.pre-commit-config.yaml Normal file
View file

@ -0,0 +1,29 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.0.1
hooks:
- id: fix-encoding-pragma
args: ['--remove']
- id: debug-statements
- id: check-merge-conflict
- id: check-ast
- id: end-of-file-fixer
- id: trailing-whitespace
- repo: https://github.com/timothycrosley/isort
rev: 5.7.0
hooks:
- id: isort
additional_dependencies: [toml]
- repo: https://github.com/psf/black
rev: 2d0c14989dca41676fc83fb36f2d652cf93fad58
hooks:
- id: black
- repo: https://gitlab.com/pycqa/flake8
rev: 3.8.4
hooks:
- id: flake8
args: ['--config', '.flake8']
exclude: '.*/migrations/.*|conf'

View file

@ -7,6 +7,8 @@ Version 0.9.9 (unreleased)
* Adds Chinese (Simplified) translation. (#266 thanks @chenluyong)
* Test against Django 4.1a
* 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 and @biermeester)
* Format code with pre-commit
Version 0.9.8

View file

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
#
# Django Rosetta documentation build configuration file, created by
# sphinx-quickstart on Thu Apr 2 15:19:37 2015.
@ -25,7 +24,7 @@ import sys
def get_version():
sys.path.insert(0, os.path.abspath('..'))
sys.path.insert(0, os.path.abspath(".."))
from rosetta import get_version as get_version_
return get_version_()
@ -46,23 +45,23 @@ def get_version():
extensions = []
# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]
# The suffix(es) of source filenames.
# You can specify multiple suffix as a list of string:
# source_suffix = ['.rst', '.md']
source_suffix = '.rst'
source_suffix = ".rst"
# The encoding of source files.
# source_encoding = 'utf-8-sig'
# The master toctree document.
master_doc = 'index'
master_doc = "index"
# General information about the project.
project = u'Django Rosetta'
copyright = u'2008 2021 Marco Bonetti and contributors'
author = u'Marco Bonetti'
project = "Django Rosetta"
copyright = "2008 2021 Marco Bonetti and contributors"
author = "Marco Bonetti"
version = get_version()
@ -84,7 +83,7 @@ language = None
# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ['_build']
exclude_patterns = ["_build"]
# The reST default role (used for this markup: `text`) to use for all
# documents.
@ -102,7 +101,7 @@ exclude_patterns = ['_build']
# show_authors = False
# The name of the Pygments (syntax highlighting) style to use.
pygments_style = 'sphinx'
pygments_style = "sphinx"
# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []
@ -147,7 +146,7 @@ todo_include_todos = False
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]
# Add any extra paths that contain custom files (such as robots.txt or
# .htaccess) here, relative to this directory. These files are copied
@ -210,7 +209,7 @@ html_static_path = ['_static']
# html_search_scorer = 'scorer.js'
# Output file base name for HTML help builder.
htmlhelp_basename = 'DjangoRosettadoc'
htmlhelp_basename = "DjangoRosettadoc"
# -- Options for LaTeX output ---------------------------------------------
@ -231,10 +230,10 @@ latex_elements = {
latex_documents = [
(
master_doc,
'DjangoRosetta.tex',
u'Django Rosetta Documentation',
u'Marco Bonetti',
'manual',
"DjangoRosetta.tex",
"Django Rosetta Documentation",
"Marco Bonetti",
"manual",
)
]
@ -263,7 +262,7 @@ latex_documents = [
# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [(master_doc, 'djangorosetta', u'Django Rosetta Documentation', [author], 1)]
man_pages = [(master_doc, "djangorosetta", "Django Rosetta Documentation", [author], 1)]
# If true, show URL addresses after external links.
# man_show_urls = False
@ -277,12 +276,12 @@ man_pages = [(master_doc, 'djangorosetta', u'Django Rosetta Documentation', [aut
texinfo_documents = [
(
master_doc,
'DjangoRosetta',
u'Django Rosetta Documentation',
"DjangoRosetta",
"Django Rosetta Documentation",
author,
'DjangoRosetta',
'One line description of project.',
'Miscellaneous',
"DjangoRosetta",
"One line description of project.",
"Miscellaneous",
)
]

35
pyproject.toml Normal file
View file

@ -0,0 +1,35 @@
[tool.isort]
profile = "black"
lines_after_imports = 2
multi_line_output = 3
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"
skip_glob= "**/migrations/**"
[tool.black]
line-length = 88
target-version = ['py38']
exclude = '''
(
/(
\.eggs # exclude a few common directories in the
| \.git # root of the project
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| node_modules
)/
| foo.py # also separately exclude a file named foo.py in
# the root of the project
)
'''

View file

@ -15,12 +15,12 @@ def get_access_control_function():
Return a predicate for determining if a user can
access the Rosetta views
"""
access_function = getattr(settings, 'ROSETTA_ACCESS_CONTROL_FUNCTION', None)
access_function = getattr(settings, "ROSETTA_ACCESS_CONTROL_FUNCTION", None)
if access_function is None:
return is_superuser_staff_or_in_translators_group
elif isinstance(access_function, str):
# Dynamically load a permissions function
perm_module, perm_func = access_function.rsplit('.', 1)
perm_module, perm_func = access_function.rsplit(".", 1)
perm_module = importlib.import_module(perm_module)
return getattr(perm_module, perm_func)
elif callable(access_function):
@ -31,7 +31,7 @@ def get_access_control_function():
# Default access control test
def is_superuser_staff_or_in_translators_group(user):
if not getattr(settings, 'ROSETTA_REQUIRES_AUTH', True):
if not getattr(settings, "ROSETTA_REQUIRES_AUTH", True):
return True
try:
if not user.is_authenticated:
@ -39,10 +39,16 @@ def is_superuser_staff_or_in_translators_group(user):
elif user.is_superuser and user.is_staff:
return True
else:
return user.groups.filter(name='translators').exists()
return user.groups.filter(name="translators").exists()
except AttributeError:
if not hasattr(user, 'is_authenticated') or not hasattr(user, 'is_superuser') or not hasattr(user, 'groups'):
raise ImproperlyConfigured('If you are using custom User Models you must implement a custom authentication method for Rosetta. See ROSETTA_ACCESS_CONTROL_FUNCTION here: https://django-rosetta.readthedocs.org/en/latest/settings.html')
if (
not hasattr(user, "is_authenticated")
or not hasattr(user, "is_superuser")
or not hasattr(user, "groups")
):
raise ImproperlyConfigured(
"If you are using custom User Models you must implement a custom authentication method for Rosetta. See ROSETTA_ACCESS_CONTROL_FUNCTION here: https://django-rosetta.readthedocs.org/en/latest/settings.html"
)
raise
@ -55,9 +61,15 @@ def can_translate_language(user, langid):
elif user.is_superuser and user.is_staff:
return True
else:
return user.groups.filter(name='translators-%s' % langid).exists()
return user.groups.filter(name="translators-%s" % langid).exists()
except AttributeError:
if not hasattr(user, 'is_authenticated') or not hasattr(user, 'is_superuser') or not hasattr(user, 'groups'):
raise ImproperlyConfigured('If you are using custom User Models you must implement a custom authentication method for Rosetta. See ROSETTA_ACCESS_CONTROL_FUNCTION here: https://django-rosetta.readthedocs.org/en/latest/settings.html')
if (
not hasattr(user, "is_authenticated")
or not hasattr(user, "is_superuser")
or not hasattr(user, "groups")
):
raise ImproperlyConfigured(
"If you are using custom User Models you must implement a custom authentication method for Rosetta. See ROSETTA_ACCESS_CONTROL_FUNCTION here: https://django-rosetta.readthedocs.org/en/latest/settings.html"
)
raise

View file

@ -4,10 +4,10 @@ from .conf import settings as rosetta_settings
class RosettaAppConfig(AppConfig):
name = 'rosetta'
name = "rosetta"
def ready(self):
from django.contrib import admin
if rosetta_settings.SHOW_AT_ADMIN_PANEL:
admin.site.index_template = 'rosetta/admin_index.html'
admin.site.index_template = "rosetta/admin_index.html"

View file

@ -6,7 +6,7 @@ from django.conf import settings as dj_settings
from django.core.signals import setting_changed
__all__ = ['settings']
__all__ = ["settings"]
class RosettaSettings(object):
@ -18,75 +18,87 @@ class RosettaSettings(object):
"""
SETTINGS = {
'ROSETTA_MESSAGES_PER_PAGE': ('MESSAGES_PER_PAGE', 10),
'ROSETTA_ENABLE_TRANSLATION_SUGGESTIONS': (
'ENABLE_TRANSLATION_SUGGESTIONS',
"ROSETTA_MESSAGES_PER_PAGE": ("MESSAGES_PER_PAGE", 10),
"ROSETTA_ENABLE_TRANSLATION_SUGGESTIONS": (
"ENABLE_TRANSLATION_SUGGESTIONS",
False,
),
'YANDEX_TRANSLATE_KEY': ('YANDEX_TRANSLATE_KEY', None),
'AZURE_CLIENT_SECRET': ('AZURE_CLIENT_SECRET', None),
'GOOGLE_APPLICATION_CREDENTIALS_PATH': (
'GOOGLE_APPLICATION_CREDENTIALS_PATH',
"YANDEX_TRANSLATE_KEY": ("YANDEX_TRANSLATE_KEY", None),
"AZURE_CLIENT_SECRET": ("AZURE_CLIENT_SECRET", None),
"GOOGLE_APPLICATION_CREDENTIALS_PATH": (
"GOOGLE_APPLICATION_CREDENTIALS_PATH",
None,
),
'GOOGLE_PROJECT_ID': ('GOOGLE_PROJECT_ID', None),
'DEEPL_AUTH_KEY': ('DEEPL_AUTH_KEY', None),
'ROSETTA_MAIN_LANGUAGE': ('MAIN_LANGUAGE', None),
'ROSETTA_MESSAGES_SOURCE_LANGUAGE_CODE': ('MESSAGES_SOURCE_LANGUAGE_CODE', 'en'),
'ROSETTA_MESSAGES_SOURCE_LANGUAGE_NAME': (
'MESSAGES_SOURCE_LANGUAGE_NAME',
'English',
"GOOGLE_PROJECT_ID": ("GOOGLE_PROJECT_ID", None),
"DEEPL_AUTH_KEY": ("DEEPL_AUTH_KEY", None),
"ROSETTA_MAIN_LANGUAGE": ("MAIN_LANGUAGE", None),
"ROSETTA_MESSAGES_SOURCE_LANGUAGE_CODE": (
"MESSAGES_SOURCE_LANGUAGE_CODE",
"en",
),
'ROSETTA_ACCESS_CONTROL_FUNCTION': ('ACCESS_CONTROL_FUNCTION', None),
'ROSETTA_WSGI_AUTO_RELOAD': ('WSGI_AUTO_RELOAD', False),
'ROSETTA_UWSGI_AUTO_RELOAD': ('UWSGI_AUTO_RELOAD', False),
'ROSETTA_EXCLUDED_APPLICATIONS': ('EXCLUDED_APPLICATIONS', ()),
'ROSETTA_POFILE_WRAP_WIDTH': ('POFILE_WRAP_WIDTH', 78),
'ROSETTA_STORAGE_CLASS': ('STORAGE_CLASS', 'rosetta.storage.CacheRosettaStorage'),
'ROSETTA_ENABLE_REFLANG': ('ENABLE_REFLANG', False),
'ROSETTA_POFILENAMES': ('POFILENAMES', ('django.po', 'djangojs.po')),
'ROSETTA_CACHE_NAME': (
'ROSETTA_CACHE_NAME',
'rosetta' if 'rosetta' in dj_settings.CACHES else 'default',
"ROSETTA_MESSAGES_SOURCE_LANGUAGE_NAME": (
"MESSAGES_SOURCE_LANGUAGE_NAME",
"English",
),
'ROSETTA_REQUIRES_AUTH': ('ROSETTA_REQUIRES_AUTH', True),
'ROSETTA_EXCLUDED_PATHS': ('ROSETTA_EXCLUDED_PATHS', ()),
'ROSETTA_LANGUAGE_GROUPS': ('ROSETTA_LANGUAGE_GROUPS', False),
'ROSETTA_AUTO_COMPILE': ('AUTO_COMPILE', True),
'ROSETTA_SHOW_AT_ADMIN_PANEL': ('SHOW_AT_ADMIN_PANEL', False),
'ROSETTA_LOGIN_URL': ('LOGIN_URL', dj_settings.LOGIN_URL),
'ROSETTA_LANGUAGES': ('ROSETTA_LANGUAGES', dj_settings.LANGUAGES),
'ROSETTA_SHOW_OCCURRENCES': ('SHOW_OCCURRENCES', True),
"ROSETTA_ACCESS_CONTROL_FUNCTION": ("ACCESS_CONTROL_FUNCTION", None),
"ROSETTA_WSGI_AUTO_RELOAD": ("WSGI_AUTO_RELOAD", False),
"ROSETTA_UWSGI_AUTO_RELOAD": ("UWSGI_AUTO_RELOAD", False),
"ROSETTA_EXCLUDED_APPLICATIONS": ("EXCLUDED_APPLICATIONS", ()),
"ROSETTA_POFILE_WRAP_WIDTH": ("POFILE_WRAP_WIDTH", 78),
"ROSETTA_STORAGE_CLASS": (
"STORAGE_CLASS",
"rosetta.storage.CacheRosettaStorage",
),
"ROSETTA_ENABLE_REFLANG": ("ENABLE_REFLANG", False),
"ROSETTA_POFILENAMES": ("POFILENAMES", ("django.po", "djangojs.po")),
"ROSETTA_CACHE_NAME": (
"ROSETTA_CACHE_NAME",
"rosetta" if "rosetta" in dj_settings.CACHES else "default",
),
"ROSETTA_REQUIRES_AUTH": ("ROSETTA_REQUIRES_AUTH", True),
"ROSETTA_EXCLUDED_PATHS": ("ROSETTA_EXCLUDED_PATHS", ()),
"ROSETTA_LANGUAGE_GROUPS": ("ROSETTA_LANGUAGE_GROUPS", False),
"ROSETTA_AUTO_COMPILE": ("AUTO_COMPILE", True),
"ROSETTA_SHOW_AT_ADMIN_PANEL": ("SHOW_AT_ADMIN_PANEL", False),
"ROSETTA_LOGIN_URL": ("LOGIN_URL", dj_settings.LOGIN_URL),
"ROSETTA_LANGUAGES": ("ROSETTA_LANGUAGES", dj_settings.LANGUAGES),
"ROSETTA_SHOW_OCCURRENCES": ("SHOW_OCCURRENCES", True),
# Deepl API language codes are different then those of django, so if this is not set according to your desired languages,
# We use the first 2 letters of django language code.
# In which case it would work fine for most of the languages,
# But for 'en' if you want "EN-GB" for example, please set it in this dictionary.
# you can find the supported languages list of DeepL API here: https://www.deepl.com/docs-api/translating-text/request/
# ex: DEEPL_LANGUAGES = {"fr": "FR", "en": "EN-GB", "zh_Hans": "ZH"}
'DEEPL_LANGUAGES': ('DEEPL_LANGUAGES', {}),
"DEEPL_LANGUAGES": ("DEEPL_LANGUAGES", {}),
}
def __init__(self):
# make sure we don't assign self._settings directly here, to avoid
# recursion in __setattr__, we delegate to the parent instead
super(RosettaSettings, self).__setattr__('_settings', {})
super(RosettaSettings, self).__setattr__("_settings", {})
self.load()
def load(self):
for user_setting, (rosetta_setting, default) in self.SETTINGS.items():
self._settings[rosetta_setting] = getattr(dj_settings, user_setting, default)
self._settings[rosetta_setting] = getattr(
dj_settings, user_setting, default
)
def reload(self):
self.__init__()
def __getattr__(self, attr):
if attr not in self._settings:
raise AttributeError("'RosettaSettings' object has not attribute '%s'" % attr)
raise AttributeError(
"'RosettaSettings' object has not attribute '%s'" % attr
)
return self._settings[attr]
def __setattr__(self, attr, value):
if attr not in self._settings:
raise AttributeError("'RosettaSettings' object has not attribute '%s'" % attr)
raise AttributeError(
"'RosettaSettings' object has not attribute '%s'" % attr
)
self._settings[attr] = value
@ -96,7 +108,7 @@ settings = RosettaSettings()
# Signal handler to reload settings when needed
def reload_settings(*args, **kwargs):
val = kwargs.get('setting')
val = kwargs.get("setting")
if val in settings.SETTINGS:
settings.reload()

View file

@ -201,4 +201,3 @@ msgstr "در حال نمایش:"
msgid "%(hits)s/%(message_number)s message"
msgid_plural "%(hits)s/%(message_number)s messages"
msgstr[0] "%(hits)s از %(message_number)s پیام"

View file

@ -2,10 +2,10 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Soyuzbek Orozbek uulu <soyuzbek196.kg@gmail.com>, 2020.
#
#
# Translators:
# Soyuzbek Orozbek uulu <soyuzbek196.kg@gmail.com>, 2020
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"

View file

@ -31,7 +31,3 @@ msgstr ""
msgctxt "Context hint"
msgid "String 4"
msgstr ""

View file

@ -21,7 +21,7 @@ def timestamp_with_timezone(dt=None):
"""
dt = dt or datetime.now()
if timezone is None:
return dt.strftime('%Y-%m-%d %H:%M%z')
return dt.strftime("%Y-%m-%d %H:%M%z")
if not dt.tzinfo:
tz = timezone.get_current_timezone()
if not tz:
@ -41,19 +41,35 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False)
# project/locale
if settings.SETTINGS_MODULE:
parts = settings.SETTINGS_MODULE.split('.')
parts = settings.SETTINGS_MODULE.split(".")
else:
# if settings.SETTINGS_MODULE is None, we are probably in "test" mode
# and override_settings() was used
# see: https://code.djangoproject.com/ticket/25911
parts = os.environ.get(ENVIRONMENT_VARIABLE).split('.')
parts = os.environ.get(ENVIRONMENT_VARIABLE).split(".")
project = __import__(parts[0], {}, {}, [])
abs_project_path = os.path.normpath(os.path.abspath(os.path.dirname(project.__file__)))
abs_project_path = os.path.normpath(
os.path.abspath(os.path.dirname(project.__file__))
)
if project_apps:
if os.path.exists(os.path.abspath(os.path.join(os.path.dirname(project.__file__), 'locale'))):
paths.append(os.path.abspath(os.path.join(os.path.dirname(project.__file__), 'locale')))
if os.path.exists(os.path.abspath(os.path.join(os.path.dirname(project.__file__), '..', 'locale'))):
paths.append(os.path.abspath(os.path.join(os.path.dirname(project.__file__), '..', 'locale')))
if os.path.exists(
os.path.abspath(os.path.join(os.path.dirname(project.__file__), "locale"))
):
paths.append(
os.path.abspath(
os.path.join(os.path.dirname(project.__file__), "locale")
)
)
if os.path.exists(
os.path.abspath(
os.path.join(os.path.dirname(project.__file__), "..", "locale")
)
):
paths.append(
os.path.abspath(
os.path.join(os.path.dirname(project.__file__), "..", "locale")
)
)
case_sensitive_file_system = True
tmphandle, tmppath = tempfile.mkstemp()
@ -63,14 +79,16 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False)
# django/locale
if django_apps:
django_paths = cache.get('rosetta_django_paths')
django_paths = cache.get("rosetta_django_paths")
if django_paths is None:
django_paths = []
for root, dirnames, filename in os.walk(os.path.abspath(os.path.dirname(django.__file__))):
if 'locale' in dirnames:
django_paths.append(os.path.join(root, 'locale'))
for root, dirnames, filename in os.walk(
os.path.abspath(os.path.dirname(django.__file__))
):
if "locale" in dirnames:
django_paths.append(os.path.join(root, "locale"))
continue
cache.set('rosetta_django_paths', django_paths, 60 * 60)
cache.set("rosetta_django_paths", django_paths, 60 * 60)
paths = paths + django_paths
# settings
for localepath in settings.LOCALE_PATHS:
@ -79,12 +97,15 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False)
# project/app/locale
for app_ in apps.get_app_configs():
if rosetta_settings.EXCLUDED_APPLICATIONS and app_.name in rosetta_settings.EXCLUDED_APPLICATIONS:
if (
rosetta_settings.EXCLUDED_APPLICATIONS
and app_.name in rosetta_settings.EXCLUDED_APPLICATIONS
):
continue
app_path = app_.path
# django apps
if 'contrib' in app_path and 'django' in app_path and not django_apps:
if "contrib" in app_path and "django" in app_path and not django_apps:
continue
# third party external
@ -95,19 +116,27 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False)
if not project_apps and abs_project_path in app_path:
continue
if os.path.exists(os.path.abspath(os.path.join(app_path, 'locale'))):
paths.append(os.path.abspath(os.path.join(app_path, 'locale')))
if os.path.exists(os.path.abspath(os.path.join(app_path, '..', 'locale'))):
paths.append(os.path.abspath(os.path.join(app_path, '..', 'locale')))
if os.path.exists(os.path.abspath(os.path.join(app_path, "locale"))):
paths.append(os.path.abspath(os.path.join(app_path, "locale")))
if os.path.exists(os.path.abspath(os.path.join(app_path, "..", "locale"))):
paths.append(os.path.abspath(os.path.join(app_path, "..", "locale")))
ret = set()
langs = [lang]
if u'-' in lang:
_l, _c = map(lambda x: x.lower(), lang.split(u'-', 1))
langs += [u'%s_%s' % (_l, _c), u'%s_%s' % (_l, _c.upper()), u'%s_%s' % (_l, _c.capitalize())]
elif u'_' in lang:
_l, _c = map(lambda x: x.lower(), lang.split(u'_', 1))
langs += [u'%s-%s' % (_l, _c), u'%s-%s' % (_l, _c.upper()), u'%s_%s' % (_l, _c.capitalize())]
if "-" in lang:
_l, _c = map(lambda x: x.lower(), lang.split("-", 1))
langs += [
"%s_%s" % (_l, _c),
"%s_%s" % (_l, _c.upper()),
"%s_%s" % (_l, _c.capitalize()),
]
elif "_" in lang:
_l, _c = map(lambda x: x.lower(), lang.split("_", 1))
langs += [
"%s-%s" % (_l, _c),
"%s-%s" % (_l, _c.upper()),
"%s_%s" % (_l, _c.capitalize()),
]
paths = map(os.path.normpath, paths)
paths = list(set(paths))
@ -115,7 +144,7 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False)
# Exclude paths
if path not in rosetta_settings.ROSETTA_EXCLUDED_PATHS:
for lang_ in langs:
dirname = os.path.join(path, lang_, 'LC_MESSAGES')
dirname = os.path.join(path, lang_, "LC_MESSAGES")
for fn in rosetta_settings.POFILENAMES:
filename = os.path.join(dirname, fn)
abs_path = os.path.abspath(filename)
@ -159,7 +188,7 @@ def pagination_range(first, last, current):
for e in r[:]:
if prev + 1 < e:
try:
r.insert(r.index(e), '...')
r.insert(r.index(e), "...")
except ValueError:
pass
prev = e

View file

@ -8,6 +8,7 @@ from django.core.exceptions import ImproperlyConfigured
from .conf import settings as rosetta_settings
cache = caches[rosetta_settings.ROSETTA_CACHE_NAME]
@ -47,8 +48,8 @@ class SessionRosettaStorage(BaseRosettaStorage):
super(SessionRosettaStorage, self).__init__(request)
if (
'signed_cookies' in settings.SESSION_ENGINE
and 'pickle' not in settings.SESSION_SERIALIZER.lower()
"signed_cookies" in settings.SESSION_ENGINE
and "pickle" not in settings.SESSION_SERIALIZER.lower()
):
raise ImproperlyConfigured(
"Sorry, but django-rosetta doesn't support the `signed_cookies` SESSION_ENGINE, because rosetta specific session files cannot be serialized."
@ -75,23 +76,23 @@ class CacheRosettaStorage(BaseRosettaStorage):
def __init__(self, request):
super(CacheRosettaStorage, self).__init__(request)
if 'rosetta_cache_storage_key_prefix' in self.request.session:
self._key_prefix = self.request.session['rosetta_cache_storage_key_prefix']
if "rosetta_cache_storage_key_prefix" in self.request.session:
self._key_prefix = self.request.session["rosetta_cache_storage_key_prefix"]
else:
self._key_prefix = hashlib.new(
'sha1', str(time.time()).encode('utf8')
"sha1", str(time.time()).encode("utf8")
).hexdigest()
self.request.session['rosetta_cache_storage_key_prefix'] = self._key_prefix
self.request.session["rosetta_cache_storage_key_prefix"] = self._key_prefix
if self.request.session['rosetta_cache_storage_key_prefix'] != self._key_prefix:
if self.request.session["rosetta_cache_storage_key_prefix"] != self._key_prefix:
raise ImproperlyConfigured(
"You can't use the CacheRosettaStorage because your Django Session storage doesn't seem to be working. The CacheRosettaStorage relies on the Django Session storage to avoid conflicts."
)
# Make sure we're not using DummyCache
if (
'dummycache'
in settings.CACHES[rosetta_settings.ROSETTA_CACHE_NAME]['BACKEND'].lower()
"dummycache"
in settings.CACHES[rosetta_settings.ROSETTA_CACHE_NAME]["BACKEND"].lower()
):
raise ImproperlyConfigured(
"You can't use the CacheRosettaStorage if your cache isn't correctly set up (you are using the DummyCache cache backend)."
@ -99,13 +100,13 @@ class CacheRosettaStorage(BaseRosettaStorage):
# Make sure the cache actually works
try:
self.set('rosetta_cache_test', 'rosetta')
if not self.get('rosetta_cache_test') == 'rosetta':
self.set("rosetta_cache_test", "rosetta")
if not self.get("rosetta_cache_test") == "rosetta":
raise ImproperlyConfigured(
"You can't use the CacheRosettaStorage if your cache isn't correctly set up, please double check your Django DATABASES setting and that the cache server is responding."
)
finally:
self.delete('rosetta_cache_test')
self.delete("rosetta_cache_test")
def get(self, key, default=None):
# print ('get', self._key_prefix + key)
@ -127,6 +128,6 @@ class CacheRosettaStorage(BaseRosettaStorage):
def get_storage(request):
from rosetta.conf import settings
storage_module, storage_class = settings.STORAGE_CLASS.rsplit('.', 1)
storage_module, storage_class = settings.STORAGE_CLASS.rsplit(".", 1)
storage_module = importlib.import_module(storage_module)
return getattr(storage_module, storage_class)(request)

View file

@ -8,42 +8,8 @@ $(document).ready(function() {
$('.hide', $(this).parent()).hide();
});
{% if rosetta_settings.ENABLE_TRANSLATION_SUGGESTIONS %}
{% if rosetta_settings.DEEPL_AUTH_KEY %}
$('a.suggest').click(function(e){
e.preventDefault();
var a = $(this);
var str = a.html();
var orig = $('.original .message', a.parents('tr')).html();
var trans=$('textarea',a.parent());
var apiUrl = "https://api-free.deepl.com/v2/translate";
{% if deepl_language_code %}
var destLangRoot = '{{ deepl_language_code }}';
{% else %}
var destLangRoot = '{{ rosetta_i18n_lang_code_normalized }}'.substring(0, 2);
{% endif %}
var sourceLang = '{{ rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE }}'.substring(0, 2);
let authKey = '{{ rosetta_settings.DEEPL_AUTH_KEY }}:fx';
a.attr('class','suggesting').html('...');
fetch(apiUrl, {
method: 'POST',
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: `auth_key=${authKey}&text=${orig}&source_lang=${sourceLang}&target_lang=${destLangRoot}`
}).then(response => {
if(response.ok) {
return response.json();
}
}).then(data => {
trans.val(data.translations[0].text.replace(/<br>/g, '\n').replace(/<\/?code>/g, '').replace(/&lt;/g, '<').replace(/&gt;/g, '>'));
})
.catch(error => console.log(error));
});
{% elif rosetta_settings.AZURE_CLIENT_SECRET or rosetta_settings.GOOGLE_APPLICATION_CREDENTIALS_PATH %}
{% if rosetta_settings.DEEPL_AUTH_KEY or rosetta_settings.AZURE_CLIENT_SECRET or rosetta_settings.GOOGLE_APPLICATION_CREDENTIALS_PATH %}
$('a.suggest').click(function(e){
e.preventDefault();
var a = $(this);
@ -59,7 +25,7 @@ $(document).ready(function() {
$.getJSON("{% url 'rosetta.translate_text' %}", {
from: sourceLang,
to: destLang,
text: orig
text: orig,
},
function(data) {
if (data.success){

View file

@ -3,17 +3,19 @@ import re
from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe
from rosetta.access import can_translate
register = template.Library()
rx = re.compile(r'(%(\([^\s\)]*\))?[sd]|\{[\w\d_]+?\})')
rx = re.compile(r"(%(\([^\s\)]*\))?[sd]|\{[\w\d_]+?\})")
can_translate = register.filter(can_translate)
def format_message(message):
return mark_safe(
rx.sub('<code>\\1</code>', escape(message).replace(r'\n', '<br />\n'))
rx.sub("<code>\\1</code>", escape(message).replace(r"\n", "<br />\n"))
)
@ -21,7 +23,7 @@ format_message = register.filter(format_message)
def lines_count(message):
return 1 + sum([len(line) / 50 for line in message.split('\n')])
return 1 + sum([len(line) / 50 for line in message.split("\n")])
lines_count = register.filter(lines_count)
@ -59,14 +61,14 @@ def do_incr(parser, token):
if len(args) < 2:
raise SyntaxError("'incr' tag requires at least one argument")
name = args[1]
if not hasattr(parser, '_namedIncrNodes'):
if not hasattr(parser, "_namedIncrNodes"):
parser._namedIncrNodes = {}
if name not in parser._namedIncrNodes:
parser._namedIncrNodes[name] = IncrNode(0)
return parser._namedIncrNodes[name]
do_incr = register.tag('increment', do_incr)
do_incr = register.tag("increment", do_incr)
class IncrNode(template.Node):
@ -79,7 +81,7 @@ class IncrNode(template.Node):
def is_fuzzy(message):
return message and hasattr(message, 'flags') and 'fuzzy' in message.flags
return message and hasattr(message, "flags") and "fuzzy" in message.flags
is_fuzzy = register.filter(is_fuzzy)

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"
@ -366,6 +366,3 @@ msgstr ""
msgid "xeXu5ur6xtXV69d2-7u7Fz5eD6TpYXyNVcFd28vjsZ7fnYIrzTTMEn__E_5ykGYGm-aY_7JXpx9_fXD9K-75dlH1vTvOv2w2HsZPL9zu7MdvupP-qNh5xo8PjfCLkR1kO4QUmB8CZHeW2BcGw2nYTjt7I7NcBLDuNM9PpbvPQt3le1Pex String 50"
msgstr ""

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"
@ -31,7 +31,3 @@ msgstr ""
msgctxt "Context hint"
msgid "String 4"
msgstr ""

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"
@ -24,5 +24,3 @@ msgstr ""
msgid "String 2"
msgstr ""

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"
@ -22,6 +22,3 @@ msgstr ""
#~ msgid "String 2"
#~ msgstr ""

View file

@ -2,7 +2,7 @@
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#
msgid ""
msgstr ""
"Project-Id-Version: Rosetta\n"
@ -31,7 +31,3 @@ msgstr ""
msgctxt "Context hint"
msgid "String 4"
msgstr ""

View file

@ -22,6 +22,3 @@ msgstr ""
#~ msgid "String 2"
#~ msgstr ""

View file

@ -2,5 +2,5 @@ from django.apps import AppConfig
class TestAppConfig(AppConfig):
name = 'rosetta.tests.test_app'
verbose_name = 'Rosetta test app'
name = "rosetta.tests.test_app"
verbose_name = "Rosetta test app"

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,9 @@
from django.conf.urls import include, url
from .views import dummy
urlpatterns = [
url(r'^rosetta/', include('rosetta.urls')),
url(r'^admin/$', dummy, name='dummy-login')
url(r"^rosetta/", include("rosetta.urls")),
url(r"^admin/$", dummy, name="dummy-login"),
]

View file

@ -1,23 +1,39 @@
import json
import uuid
from django.conf import settings
import requests
from django.conf import settings
class TranslationException(Exception):
pass
def translate(text, from_language, to_language):
AZURE_CLIENT_SECRET = getattr(settings, 'AZURE_CLIENT_SECRET', None)
AZURE_CLIENT_SECRET = getattr(settings, "AZURE_CLIENT_SECRET", None)
GOOGLE_APPLICATION_CREDENTIALS_PATH = getattr(
settings, 'GOOGLE_APPLICATION_CREDENTIALS_PATH', None
settings, "GOOGLE_APPLICATION_CREDENTIALS_PATH", None
)
GOOGLE_PROJECT_ID = getattr(settings, 'GOOGLE_PROJECT_ID', None)
GOOGLE_PROJECT_ID = getattr(settings, "GOOGLE_PROJECT_ID", None)
DEEPL_AUTH_KEY = getattr(settings, "DEEPL_AUTH_KEY", None)
if AZURE_CLIENT_SECRET:
if DEEPL_AUTH_KEY:
deepl_language_code = None
DEEPL_LANGUAGES = getattr(settings, "DEEPL_LANGUAGES", None)
if type(DEEPL_LANGUAGES) is dict:
deepl_language_code = DEEPL_LANGUAGES.get(to_language, None)
if deepl_language_code is None:
deepl_language_code = to_language[:2].upper()
return translate_by_deepl(
text,
deepl_language_code.upper(),
DEEPL_AUTH_KEY,
)
elif AZURE_CLIENT_SECRET:
return translate_by_azure(text, from_language, to_language, AZURE_CLIENT_SECRET)
elif GOOGLE_APPLICATION_CREDENTIALS_PATH and GOOGLE_PROJECT_ID:
return translate_by_google(
@ -28,7 +44,19 @@ def translate(text, from_language, to_language):
GOOGLE_PROJECT_ID,
)
else:
raise TranslationException('No translation API service is configured.')
raise TranslationException("No translation API service is configured.")
def translate_by_deepl(text, to_language, auth_key):
r = requests.post(
"https://api-free.deepl.com/v2/translate",
headers={"Authorization": f"DeepL-Auth-Key {auth_key}"},
data={
"target_lang": to_language.upper(),
"text": text,
},
)
return r.json().get("translations")[0].get("text")
def translate_by_azure(text, from_language, to_language, subscription_key):
@ -43,13 +71,13 @@ def translate_by_azure(text, from_language, to_language, subscription_key):
https://docs.microsoft.com/en-us/azure/cognitive-services/translator/reference/v3-0-translate?tabs=curl
"""
AZURE_TRANSLATOR_HOST = 'https://api.cognitive.microsofttranslator.com'
AZURE_TRANSLATOR_PATH = '/translate?api-version=3.0'
AZURE_TRANSLATOR_HOST = "https://api.cognitive.microsofttranslator.com"
AZURE_TRANSLATOR_PATH = "/translate?api-version=3.0"
headers = {
'Ocp-Apim-Subscription-Key': subscription_key,
'Content-type': 'application/json',
'X-ClientTraceId': str(uuid.uuid4()),
"Ocp-Apim-Subscription-Key": subscription_key,
"Content-type": "application/json",
"X-ClientTraceId": str(uuid.uuid4()),
}
url_parameters = {"from": from_language, "to": to_language}
@ -112,18 +140,18 @@ def translate_by_google(
client = google_translate.TranslationServiceClient.from_service_account_json(
creadentials_path
)
parent = "projects/{}/locations/{}".format(project_id, 'global')
parent = "projects/{}/locations/{}".format(project_id, "global")
try:
api_response = client.translate_text(
request=dict(
parent=parent,
contents=[text],
mime_type='text/plain',
mime_type="text/plain",
source_language_code=input_language,
target_language_code=output_language.split('.', 1)[0],
target_language_code=output_language.split(".", 1)[0],
)
)
except Exception as e:
raise TranslationException('Google API error: {}'.format(e))
raise TranslationException("Google API error: {}".format(e))
else:
return str(api_response.translations[0].translated_text)

View file

@ -3,37 +3,38 @@ from django.views.generic.base import RedirectView
from . import views
urlpatterns = [
re_path(
r'^$',
r"^$",
RedirectView.as_view(
url=reverse_lazy('rosetta-file-list', kwargs={'po_filter': 'project'}),
url=reverse_lazy("rosetta-file-list", kwargs={"po_filter": "project"}),
permanent=False,
),
name='rosetta-old-home-redirect',
name="rosetta-old-home-redirect",
),
re_path(
r'^files/$',
r"^files/$",
RedirectView.as_view(
url=reverse_lazy('rosetta-file-list', kwargs={'po_filter': 'project'}),
url=reverse_lazy("rosetta-file-list", kwargs={"po_filter": "project"}),
permanent=False,
),
name='rosetta-file-list-redirect',
name="rosetta-file-list-redirect",
),
re_path(
r'^files/(?P<po_filter>[\w-]+)/$',
r"^files/(?P<po_filter>[\w-]+)/$",
views.TranslationFileListView.as_view(),
name='rosetta-file-list',
name="rosetta-file-list",
),
re_path(
r'^files/(?P<po_filter>[\w-]+)/(?P<lang_id>[\w\-_\.]+)/(?P<idx>\d+)/$',
r"^files/(?P<po_filter>[\w-]+)/(?P<lang_id>[\w\-_\.]+)/(?P<idx>\d+)/$",
views.TranslationFormView.as_view(),
name='rosetta-form',
name="rosetta-form",
),
re_path(
r'^files/(?P<po_filter>[\w-]+)/(?P<lang_id>[\w\-_\.]+)/(?P<idx>\d+)/download/$',
r"^files/(?P<po_filter>[\w-]+)/(?P<lang_id>[\w\-_\.]+)/(?P<idx>\d+)/download/$",
views.TranslationFileDownload.as_view(),
name='rosetta-download-file',
name="rosetta-download-file",
),
re_path(r'^translate/$', views.translate_text, name='rosetta.translate_text'),
re_path(r"^translate/$", views.translate_text, name="rosetta.translate_text"),
]

View file

@ -6,12 +6,13 @@ import zipfile
from io import BytesIO
from urllib.parse import urlencode
from polib import pofile
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test
from django.core.paginator import Paginator
from django.http import (Http404, HttpResponse, HttpResponseRedirect,
JsonResponse)
from django.http import Http404, HttpResponse, HttpResponseRedirect, JsonResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes
@ -19,7 +20,6 @@ from django.utils.functional import Promise, cached_property
from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache
from django.views.generic import TemplateView, View
from polib import pofile
from . import get_version as get_rosetta_version
from .access import can_translate, can_translate_language
@ -31,7 +31,7 @@ from .translate_utils import TranslationException, translate
def get_app_name(path):
return path.split('/locale')[0].split('/')[-1]
return path.split("/locale")[0].split("/")[-1]
class LoginURL(Promise):
@ -43,9 +43,9 @@ class LoginURL(Promise):
return rosetta_settings.LOGIN_URL
@method_decorator(never_cache, 'dispatch')
@method_decorator(never_cache, "dispatch")
@method_decorator(
user_passes_test(lambda user: can_translate(user), LoginURL()), 'dispatch'
user_passes_test(lambda user: can_translate(user), LoginURL()), "dispatch"
)
class RosettaBaseMixin(object):
"""A mixin class for Rosetta's class-based views. It provides:
@ -64,8 +64,8 @@ class RosettaBaseMixin(object):
If the filter isn't in this list, throw a 404.
"""
po_filter = self.kwargs.get('po_filter')
if po_filter not in {'all', 'django', 'third-party', 'project'}:
po_filter = self.kwargs.get("po_filter")
if po_filter not in {"all", "django", "third-party", "project"}:
raise Http404
return po_filter
@ -97,7 +97,7 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
(If either of the above fail, throw a 404.)
"""
# (Formerly known as "rosetta_i18n_lang_code")
lang_id = self.kwargs['lang_id']
lang_id = self.kwargs["lang_id"]
if lang_id not in {lang[0] for lang in rosetta_settings.ROSETTA_LANGUAGES}:
raise Http404
if not can_translate_language(self.request.user, lang_id):
@ -112,12 +112,12 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
Throw a 404 if a file isn't found.
"""
# This was formerly referred to as 'rosetta_i18n_fn'
idx = self.kwargs['idx']
idx = self.kwargs["idx"]
idx = int(idx) # idx matched url re expression; calling int() is safe
third_party_apps = self.po_filter in ('all', 'third-party')
django_apps = self.po_filter in ('all', 'django')
project_apps = self.po_filter in ('all', 'project')
third_party_apps = self.po_filter in ("all", "third-party")
django_apps = self.po_filter in ("all", "django")
project_apps = self.po_filter in ("all", "project")
po_paths = find_pos(
self.language_id,
@ -153,8 +153,8 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
# value of the meat of each entry on its side in an attribute
# called "md5hash".
str_to_hash = (
str(entry.msgid) + str(entry.msgstr) + str(entry.msgctxt or '')
).encode('utf8')
str(entry.msgid) + str(entry.msgstr) + str(entry.msgctxt or "")
).encode("utf8")
entry.md5hash = hashlib.md5(str_to_hash).hexdigest()
else:
storage = get_storage(self.request)
@ -167,9 +167,9 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
# a hashed value of the meat of each entry on its side in
# an attribute called "md5hash".
str_to_hash = (
str(entry.msgid) + str(entry.msgstr) + str(entry.msgctxt or '')
).encode('utf8')
entry.md5hash = hashlib.new('md5', str_to_hash).hexdigest()
str(entry.msgid) + str(entry.msgstr) + str(entry.msgctxt or "")
).encode("utf8")
entry.md5hash = hashlib.new("md5", str_to_hash).hexdigest()
storage.set(self.po_file_cache_key, po_file)
return po_file
@ -178,7 +178,7 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
"""Return the cache key used to save/access the .po file (when actually
persisted in cache).
"""
return 'po-file-%s' % self.po_file_path
return "po-file-%s" % self.po_file_path
@cached_property
def po_file_is_writable(self):
@ -194,15 +194,15 @@ class TranslationFileListView(RosettaBaseMixin, TemplateView):
and their translation progress for a filtered list of apps/projects.
"""
http_method_names = ['get']
template_name = 'rosetta/file-list.html'
http_method_names = ["get"]
template_name = "rosetta/file-list.html"
def get_context_data(self, **kwargs):
context = super(TranslationFileListView, self).get_context_data(**kwargs)
third_party_apps = self.po_filter in ('all', 'third-party')
django_apps = self.po_filter in ('all', 'django')
project_apps = self.po_filter in ('all', 'project')
third_party_apps = self.po_filter in ("all", "third-party")
django_apps = self.po_filter in ("all", "django")
project_apps = self.po_filter in ("all", "project")
languages = []
has_pos = False
@ -224,10 +224,10 @@ class TranslationFileListView(RosettaBaseMixin, TemplateView):
languages.append((language[0], _(language[1]), po_files))
has_pos = has_pos or bool(po_paths)
context['version'] = get_rosetta_version()
context['languages'] = languages
context['has_pos'] = has_pos
context['po_filter'] = self.po_filter
context["version"] = get_rosetta_version()
context["languages"] = languages
context["has_pos"] = has_pos
context["po_filter"] = self.po_filter
return context
@ -250,8 +250,8 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
# Note: due to the unorthodox nature of the form itself, we're not using
# Django's generic FormView as our base class.
http_method_names = ['get', 'post']
template_name = 'rosetta/form.html'
http_method_names = ["get", "post"]
template_name = "rosetta/form.html"
def fix_nls(self, in_, out_):
"""Fixes submitted translations by filtering carriage returns and pairing
@ -261,7 +261,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
return out_
if "\r" in out_ and "\r" not in in_:
out_ = out_.replace("\r", '')
out_ = out_.replace("\r", "")
if "\n" == in_[0] and "\n" != out_[0]:
out_ = "\n" + out_
@ -292,8 +292,8 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
# The message text inputs are captured as hashes of their initial
# contents, preceded by "m_". Messages with plurals end with their
# variation number.
single_text_input_regex = re.compile(r'^m_([0-9a-f]+)$')
plural_text_input_regex = re.compile(r'^m_([0-9a-f]+)_([0-9]+)$')
single_text_input_regex = re.compile(r"^m_([0-9a-f]+)$")
plural_text_input_regex = re.compile(r"^m_([0-9a-f]+)_([0-9]+)$")
file_change = False
for field_name, new_msgstr in request.POST.items():
md5hash = None
@ -315,7 +315,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
plural_id = None
if md5hash is not None: # Empty string should be processed!
entry = self.po_file.find(md5hash, 'md5hash')
entry = self.po_file.find(md5hash, "md5hash")
# If someone did a makemessage, some entries might
# have been removed, so we need to check.
if entry:
@ -327,13 +327,13 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
else:
entry.msgstr = self.fix_nls(entry.msgid, new_msgstr)
is_fuzzy = bool(self.request.POST.get('f_%s' % md5hash, False))
old_fuzzy = 'fuzzy' in entry.flags
is_fuzzy = bool(self.request.POST.get("f_%s" % md5hash, False))
old_fuzzy = "fuzzy" in entry.flags
if old_fuzzy and not is_fuzzy:
entry.flags.remove('fuzzy')
entry.flags.remove("fuzzy")
elif not old_fuzzy and is_fuzzy:
entry.flags.append('fuzzy')
entry.flags.append("fuzzy")
file_change = True
@ -358,15 +358,15 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
if file_change and self.po_file_is_writable:
try:
self.po_file.metadata['Last-Translator'] = "{} {} <{}>".format(
getattr(self.request.user, 'first_name', 'Anonymous'),
getattr(self.request.user, 'last_name', 'User'),
getattr(self.request.user, 'email', 'anonymous@user.tld'),
self.po_file.metadata["Last-Translator"] = "{} {} <{}>".format(
getattr(self.request.user, "first_name", "Anonymous"),
getattr(self.request.user, "last_name", "User"),
getattr(self.request.user, "email", "anonymous@user.tld"),
)
self.po_file.metadata['X-Translated-Using'] = u"django-rosetta %s" % (
self.po_file.metadata["X-Translated-Using"] = "django-rosetta %s" % (
get_rosetta_version()
)
self.po_file.metadata['PO-Revision-Date'] = timestamp_with_timezone()
self.po_file.metadata["PO-Revision-Date"] = timestamp_with_timezone()
except UnicodeDecodeError:
pass
@ -375,7 +375,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
po_filepath, ext = os.path.splitext(self.po_file_path)
if rosetta_settings.AUTO_COMPILE:
self.po_file.save_as_mofile(po_filepath + '.mo')
self.po_file.save_as_mofile(po_filepath + ".mo")
post_save.send(
sender=None, language_code=self.language_id, request=self.request
@ -383,14 +383,14 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
# Try auto-reloading via the WSGI daemon mode reload mechanism
should_try_wsgi_reload = (
rosetta_settings.WSGI_AUTO_RELOAD
and 'mod_wsgi.process_group' in self.request.environ
and self.request.environ.get('mod_wsgi.process_group', None)
and 'SCRIPT_FILENAME' in self.request.environ
and int(self.request.environ.get('mod_wsgi.script_reloading', 0))
and "mod_wsgi.process_group" in self.request.environ
and self.request.environ.get("mod_wsgi.process_group", None)
and "SCRIPT_FILENAME" in self.request.environ
and int(self.request.environ.get("mod_wsgi.script_reloading", 0))
)
if should_try_wsgi_reload:
try:
os.utime(self.request.environ.get('SCRIPT_FILENAME'), None)
os.utime(self.request.environ.get("SCRIPT_FILENAME"), None)
except OSError:
pass
# Try auto-reloading via uwsgi daemon reload mechanism
@ -413,7 +413,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
# page number can be incremented.
paginator = Paginator(self.get_entries(), rosetta_settings.MESSAGES_PER_PAGE)
try:
page = int(self._request_request('page', 1))
page = int(self._request_request("page", 1))
except ValueError:
page = 1 # fall back to page 1
else:
@ -422,16 +422,16 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
if page < paginator.num_pages:
page += 1
query_string_args = {
'msg_filter': self.msg_filter,
'query': self.query,
'ref_lang': self.ref_lang,
'page': page,
"msg_filter": self.msg_filter,
"query": self.query,
"ref_lang": self.ref_lang,
"page": page,
}
# Winnow down the query string args to non-blank ones
query_string_args = {k: v for k, v in query_string_args.items() if v}
return HttpResponseRedirect(
"{url}?{qs}".format(
url=reverse('rosetta-form', kwargs=self.kwargs),
url=reverse("rosetta-form", kwargs=self.kwargs),
qs=urlencode_safe(query_string_args),
)
)
@ -458,11 +458,11 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
# XXX: having "MSGID" at the end of the dropdown is really odd, no?
# Why not instead do this?
# LANGUAGES = [('', '----')] + list(settings.LANGUAGES)
LANGUAGES.append(('msgid', 'MSGID'))
LANGUAGES.append(("msgid", "MSGID"))
# Determine page number & how pagination links should be displayed
try:
page = int(self._request_request('page', 1))
page = int(self._request_request("page", 1))
except ValueError:
page = 1 # fall back to page 1
else:
@ -489,7 +489,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
break
if main_language:
main_lang_po_path = self.po_file_path.replace(
'/%s/' % self.language_id, '/%s/' % main_language_id
"/%s/" % self.language_id, "/%s/" % main_language_id
)
# XXX: brittle; what if this path doesn't exist? Isn't a .po file?
main_lang_po = pofile(main_lang_po_path)
@ -502,52 +502,47 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
dict(rosetta_settings.ROSETTA_LANGUAGES).get(self.language_id)
)
# "bidi" as in "bi-directional"
rosetta_i18n_lang_bidi = self.language_id.split('-')[0] in settings.LANGUAGES_BIDI
rosetta_i18n_lang_bidi = (
self.language_id.split("-")[0] in settings.LANGUAGES_BIDI
)
query_string_args = {}
if self.msg_filter:
query_string_args['msg_filter'] = self.msg_filter
query_string_args["msg_filter"] = self.msg_filter
if self.query:
query_string_args['query'] = self.query
query_string_args["query"] = self.query
if self.ref_lang:
query_string_args['ref_lang'] = self.ref_lang
query_string_args["ref_lang"] = self.ref_lang
# Base for pagination links; the page num itself is added in template
pagination_query_string_base = urlencode_safe(query_string_args)
# Base for msg filter links; it doesn't make sense to persist page
# numbers in these links. We just pass in ref_lang, if it's set.
filter_query_string_base = urlencode_safe(
{k: v for k, v in query_string_args.items() if k == 'ref_lang'}
{k: v for k, v in query_string_args.items() if k == "ref_lang"}
)
deepl_language_code = None
if rosetta_settings.DEEPL_LANGUAGES:
deepl_language_code = rosetta_settings.DEEPL_LANGUAGES.get(
self.language_id, None
)
context.update(
{
'version': get_rosetta_version(),
'LANGUAGES': LANGUAGES,
'rosetta_settings': rosetta_settings,
'rosetta_i18n_lang_name': rosetta_i18n_lang_name,
'rosetta_i18n_lang_code': self.language_id,
'rosetta_i18n_lang_code_normalized': self.language_id.replace('_', '-'),
'rosetta_i18n_lang_bidi': rosetta_i18n_lang_bidi,
'rosetta_i18n_filter': self.msg_filter,
'rosetta_i18n_write': self.po_file_is_writable,
'rosetta_messages': rosetta_messages,
'page_range': needs_pagination and page_range,
'needs_pagination': needs_pagination,
'main_language': main_language,
'rosetta_i18n_app': get_app_name(self.po_file_path),
'page': page,
'query': self.query,
'pagination_query_string_base': pagination_query_string_base,
'filter_query_string_base': filter_query_string_base,
'paginator': paginator,
'rosetta_i18n_pofile': self.po_file,
'ref_lang': self.ref_lang,
'deepl_language_code': deepl_language_code,
"version": get_rosetta_version(),
"LANGUAGES": LANGUAGES,
"rosetta_settings": rosetta_settings,
"rosetta_i18n_lang_name": rosetta_i18n_lang_name,
"rosetta_i18n_lang_code": self.language_id,
"rosetta_i18n_lang_code_normalized": self.language_id.replace("_", "-"),
"rosetta_i18n_lang_bidi": rosetta_i18n_lang_bidi,
"rosetta_i18n_filter": self.msg_filter,
"rosetta_i18n_write": self.po_file_is_writable,
"rosetta_messages": rosetta_messages,
"page_range": needs_pagination and page_range,
"needs_pagination": needs_pagination,
"main_language": main_language,
"rosetta_i18n_app": get_app_name(self.po_file_path),
"page": page,
"query": self.query,
"pagination_query_string_base": pagination_query_string_base,
"filter_query_string_base": filter_query_string_base,
"paginator": paginator,
"rosetta_i18n_pofile": self.po_file,
"ref_lang": self.ref_lang,
}
)
@ -560,8 +555,8 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
Throw a 404 if it's not in rosetta_settings.ROSETTA_LANGUAGES.
"""
ref_lang = self._request_request('ref_lang', 'msgid')
if ref_lang != 'msgid':
ref_lang = self._request_request("ref_lang", "msgid")
if ref_lang != "msgid":
allowed_languages = {lang[0] for lang in rosetta_settings.ROSETTA_LANGUAGES}
if ref_lang not in allowed_languages:
raise Http404
@ -573,11 +568,13 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
exists, otherwise None.
"""
ref_pofile = None
if rosetta_settings.ENABLE_REFLANG and self.ref_lang != 'msgid':
replacement = '{separator}locale{separator}{ref_lang}'.format(
if rosetta_settings.ENABLE_REFLANG and self.ref_lang != "msgid":
replacement = "{separator}locale{separator}{ref_lang}".format(
separator=os.sep, ref_lang=self.ref_lang
)
pattern = r'\{separator}locale\{separator}[a-z]{{2}}'.format(separator=os.sep)
pattern = r"\{separator}locale\{separator}[a-z]{{2}}".format(
separator=os.sep
)
ref_fn = re.sub(pattern, replacement, self.po_file_path)
try:
ref_pofile = pofile(ref_fn)
@ -598,10 +595,10 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
if self.query:
msg_filter = None
else:
msg_filter = self._request_request('msg_filter', 'all')
available_msg_filters = {'untranslated', 'translated', 'fuzzy', 'all'}
msg_filter = self._request_request("msg_filter", "all")
available_msg_filters = {"untranslated", "translated", "fuzzy", "all"}
if msg_filter not in available_msg_filters:
msg_filter = 'all'
msg_filter = "all"
return msg_filter
@cached_property
@ -609,7 +606,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
"""Strip and return the query (for searching the catalog) from the
request, or None.
"""
return self._request_request('query', '').strip() or None
return self._request_request("query", "").strip() or None
def get_entries(self):
"""Return a list of the entries (messages) that would be part of the
@ -626,9 +623,9 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
+ str(e.msgid)
+ str(e.msgctxt)
+ str(e.comment)
+ u''.join([o[0] for o in e.occurrences])
+ "".join([o[0] for o in e.occurrences])
+ str(e.msgid_plural)
+ u''.join(e.msgstr_plural.values())
+ "".join(e.msgstr_plural.values())
)
entries = [
@ -638,11 +635,11 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
]
else:
# Scenario #2: filtered list of messages
if self.msg_filter == 'untranslated':
if self.msg_filter == "untranslated":
entries = self.po_file.untranslated_entries()
elif self.msg_filter == 'translated':
elif self.msg_filter == "translated":
entries = self.po_file.translated_entries()
elif self.msg_filter == 'fuzzy':
elif self.msg_filter == "fuzzy":
entries = [e_ for e_ in self.po_file.fuzzy_entries() if not e_.obsolete]
else:
# ("all")
@ -656,16 +653,16 @@ class TranslationFileDownload(RosettaFileLevelMixin, View):
disk is unwritable (permissions-wise), return what's in the cache.
"""
http_method_names = [u'get']
http_method_names = ["get"]
def get(self, request, *args, **kwargs):
try:
if len(self.po_file_path.split('/')) >= 5:
offered_fn = '_'.join(self.po_file_path.split('/')[-5:])
if len(self.po_file_path.split("/")) >= 5:
offered_fn = "_".join(self.po_file_path.split("/")[-5:])
else:
offered_fn = self.po_file_path.split('/')[-1]
po_fn = str(self.po_file_path.split('/')[-1])
mo_fn = str(po_fn.replace('.po', '.mo')) # not so smart, huh
offered_fn = self.po_file_path.split("/")[-1]
po_fn = str(self.po_file_path.split("/")[-1])
mo_fn = str(po_fn.replace(".po", ".mo")) # not so smart, huh
zipdata = BytesIO()
with zipfile.ZipFile(zipdata, mode="w") as zipf:
zipf.writestr(po_fn, str(self.po_file).encode("utf8"))
@ -673,33 +670,33 @@ class TranslationFileDownload(RosettaFileLevelMixin, View):
zipdata.seek(0)
response = HttpResponse(zipdata.read())
filename = 'filename=%s.%s.zip' % (offered_fn, self.language_id)
response['Content-Disposition'] = 'attachment; %s' % filename
response['Content-Type'] = 'application/x-zip'
filename = "filename=%s.%s.zip" % (offered_fn, self.language_id)
response["Content-Disposition"] = "attachment; %s" % filename
response["Content-Type"] = "application/x-zip"
return response
except Exception:
# XXX: should add a message!
return HttpResponseRedirect(
reverse('rosetta-file-list', kwargs={'po_filter': 'project'})
reverse("rosetta-file-list", kwargs={"po_filter": "project"})
)
@user_passes_test(lambda user: can_translate(user), LoginURL())
def translate_text(request):
language_from = request.GET.get('from', None)
language_to = request.GET.get('to', None)
text = request.GET.get('text', None)
language_from = request.GET.get("from", None)
language_to = request.GET.get("to", None)
text = request.GET.get("text", None)
if language_from == language_to:
data = {'success': True, 'translation': text}
data = {"success": True, "translation": text}
else:
try:
translated_text = translate(text, language_from, language_to)
data = {'success': True, 'translation': translated_text}
data = {"success": True, "translation": translated_text}
except TranslationException as e:
data = {'success': False, 'error': str(e)}
data = {"success": False, "error": str(e)}
return JsonResponse(data)

View file

@ -0,0 +1,37 @@
interactions:
- request:
body: target_lang=FR&text=hello+world
headers:
Accept:
- '*/*'
Accept-Encoding:
- gzip, deflate
Authorization:
- DeepL-Auth-Key FAKE
Connection:
- keep-alive
Content-Length:
- '31'
Content-Type:
- application/x-www-form-urlencoded
User-Agent:
- python-requests/2.26.0
method: POST
uri: https://api-free.deepl.com/v2/translate
response:
body: {string: '{"translations": [{"detected_source_language": "EN", "text": "Salut tout le monde"}]}'}
headers:
access-control-allow-origin:
- '*'
content-length:
- '0'
date:
- Sun, 01 Jan 2023 13:07:33 GMT
server:
- nginx
strict-transport-security:
- max-age=63072000; includeSubDomains; preload
status:
code: 200
message: OK
version: 1

View file

@ -2,12 +2,15 @@
import os
import sys
if __name__ == "__main__":
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testproject.settings")
import django
django.setup()
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

View file

@ -3,6 +3,7 @@ import sys
import django
SITE_ID = 1
PROJECT_PATH = os.path.abspath(os.path.dirname(__file__))
@ -68,13 +69,13 @@ MIDDLEWARE = (
# Note: languages are overridden in the test runner
LANGUAGES = (
("en", u"English"),
("bs-Cyrl-BA", u"Bosnian (Cyrillic) (Bosnia and Herzegovina)"),
("ja", u"日本語"),
("xx", u"XXXXX"),
("fr", u"French"),
("zh_Hans", u"Chinese (Simplified)"),
("fr_FR.utf8", u"French (France), UTF8"),
("en", "English"),
("bs-Cyrl-BA", "Bosnian (Cyrillic) (Bosnia and Herzegovina)"),
("ja", "日本語"),
("xx", "XXXXX"),
("fr", "French"),
("zh_Hans", "Chinese (Simplified)"),
("fr_FR.utf8", "French (France), UTF8"),
)

View file

@ -6,4 +6,4 @@
one bottle of beer on the wall
{% plural %}
{{num_bottles}} bottles of beer on the wall
{% endblocktrans %}
{% endblocktrans %}

View file

@ -2,11 +2,12 @@ from django.contrib import admin
from django.contrib.staticfiles.urls import staticfiles_urlpatterns
from django.urls import include, re_path
admin.autodiscover()
urlpatterns = [
re_path(r'^admin/', admin.site.urls),
re_path(r'^rosetta/', include('rosetta.urls')),
re_path(r"^admin/", admin.site.urls),
re_path(r"^rosetta/", include("rosetta.urls")),
]
urlpatterns += staticfiles_urlpatterns()

View file

@ -21,7 +21,7 @@ requires = virtualenv>=20.3.0
[testenv]
changedir = testproject
commands =
python -Wd manage.py test rosetta
python -Wd manage.py test rosetta {posargs}
setenv =
PYTHONDONTWRITEBYTECODE=1