mirror of
https://github.com/Hopiu/django-rosetta.git
synced 2026-03-16 21:30:24 +00:00
Merge branch 'mbi-develop' into develop
This commit is contained in:
commit
9405a1e5cc
38 changed files with 807 additions and 682 deletions
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
|
|
@ -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
29
.pre-commit-config.yaml
Normal 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'
|
||||
3
CHANGES
3
CHANGES
|
|
@ -7,6 +7,9 @@ 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, @biermeester and @matthiask)
|
||||
* Format code with pre-commit
|
||||
* Test against Django 4.2a
|
||||
|
||||
|
||||
Version 0.9.8
|
||||
|
|
|
|||
43
docs/conf.py
43
docs/conf.py
|
|
@ -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
35
pyproject.toml
Normal 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
|
||||
)
|
||||
'''
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
|
|
@ -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 پیام"
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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}&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(/</g, '<').replace(/>/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){
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -22,6 +22,3 @@ msgstr ""
|
|||
|
||||
#~ msgid "String 2"
|
||||
#~ msgstr ""
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
]
|
||||
|
|
|
|||
241
rosetta/views.py
241
rosetta/views.py
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@
|
|||
one bottle of beer on the wall
|
||||
{% plural %}
|
||||
{{num_bottles}} bottles of beer on the wall
|
||||
{% endblocktrans %}
|
||||
{% endblocktrans %}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
11
tox.ini
11
tox.ini
|
|
@ -1,16 +1,16 @@
|
|||
[tox]
|
||||
envlist =
|
||||
flake8,
|
||||
py{38,39,310}-django{32,40,41},
|
||||
py{38,39,310}-django{32,40,41,42},
|
||||
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.10: py310-django32, py310-django40, py310-django41, py310-django42
|
||||
3.9: py39-django32, py39-django40, py39-django41, py39-django42
|
||||
3.8: py38-django32, py38-django40, py38-django41, py38-django42
|
||||
|
||||
|
||||
skipsdist = True
|
||||
|
|
@ -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
|
||||
|
|
@ -30,6 +30,7 @@ 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
|
||||
|
||||
pymemcache
|
||||
requests
|
||||
|
|
|
|||
Loading…
Reference in a new issue