Fixed unicode handling in gettext headers. Fixes #259

This commit is contained in:
Marco Bonetti 2021-07-17 18:14:27 +02:00
parent 417094f0f1
commit cc12a30761
6 changed files with 52 additions and 49 deletions

View file

View file

@ -6,6 +6,7 @@ Version 0.9.7 (unreleased)
-------------------------- --------------------------
* Arabic translation. (#257, thanks @Bashar) * Arabic translation. (#257, thanks @Bashar)
* Translations via the DeepL API (#258, thanks @halitcelik) * Translations via the DeepL API (#258, thanks @halitcelik)
* Fixed unicode handling in gettext headers (#259, thanks @NotSqrt)
Version 0.9.6 Version 0.9.6

View file

@ -1,12 +1,12 @@
from django import template
from django.utils.safestring import mark_safe
from django.utils.html import escape
import re import re
import six import six
from django import template
from django.utils.html import escape
from django.utils.safestring import mark_safe
from rosetta.access import can_translate from rosetta.access import can_translate
register = template.Library() register = template.Library()
rx = re.compile(r'(%(\([^\s\)]*\))?[sd]|\{[\w\d_]+?\})') rx = re.compile(r'(%(\([^\s\)]*\))?[sd]|\{[\w\d_]+?\})')
@ -14,33 +14,45 @@ can_translate = register.filter(can_translate)
def format_message(message): def format_message(message):
return mark_safe(rx.sub('<code>\\1</code>', escape(message).replace(r'\n', '<br />\n'))) return mark_safe(
rx.sub('<code>\\1</code>', escape(message).replace(r'\n', '<br />\n'))
)
format_message = register.filter(format_message) format_message = register.filter(format_message)
def lines_count(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) lines_count = register.filter(lines_count)
def mult(a, b): def mult(a, b):
return int(a) * int(b) return int(a) * int(b)
mult = register.filter(mult) mult = register.filter(mult)
def minus(a, b): def minus(a, b):
try: try:
return int(a) - int(b) return int(a) - int(b)
except: except Exception:
return 0 return 0
minus = register.filter(minus) minus = register.filter(minus)
def gt(a, b): def gt(a, b):
try: try:
return int(a) > int(b) return int(a) > int(b)
except: except Exception:
return False return False
gt = register.filter(gt) gt = register.filter(gt)
@ -54,6 +66,8 @@ def do_incr(parser, token):
if name not in parser._namedIncrNodes: if name not in parser._namedIncrNodes:
parser._namedIncrNodes[name] = IncrNode(0) parser._namedIncrNodes[name] = IncrNode(0)
return parser._namedIncrNodes[name] return parser._namedIncrNodes[name]
do_incr = register.tag('increment', do_incr) do_incr = register.tag('increment', do_incr)
@ -68,4 +82,6 @@ class IncrNode(template.Node):
def is_fuzzy(message): 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) is_fuzzy = register.filter(is_fuzzy)

View file

@ -4,6 +4,7 @@ import os
import shutil import shutil
from urllib.parse import urlencode from urllib.parse import urlencode
import vcr
from django import VERSION from django import VERSION
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
@ -13,9 +14,6 @@ from django.test import RequestFactory, TestCase, override_settings
from django.test.client import Client from django.test.client import Client
from django.urls import resolve, reverse from django.urls import resolve, reverse
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
import six
import vcr
from rosetta import views from rosetta import views
from rosetta.signals import entry_changed, post_save from rosetta.signals import entry_changed, post_save
from rosetta.storage import get_storage from rosetta.storage import get_storage
@ -138,10 +136,10 @@ class RosettaTestCase(TestCase):
self.copy_po_file_from_template('./django.po.issue67.template') self.copy_po_file_from_template('./django.po.issue67.template')
# Make sure the plurals string is valid # Make sure the plurals string is valid
with open(self.dest_file, 'rb') as f_: with open(self.dest_file, 'r') as f_:
content = f_.read() content = f_.read()
self.assertTrue('Hello, world' not in six.text_type(content)) self.assertTrue('Hello, world' not in content)
self.assertTrue('|| n%100>=20) ? 1 : 2)' in six.text_type(content)) self.assertTrue('|| n%100>=20) ? 1 : 2)' in content)
del content del content
r = self.client.get(self.xx_form_url + '?msg_filter=untranslated') r = self.client.get(self.xx_form_url + '?msg_filter=untranslated')
@ -157,7 +155,7 @@ class RosettaTestCase(TestCase):
self.client.post(self.xx_form_url + '?msg_filter=untranslated', data) self.client.post(self.xx_form_url + '?msg_filter=untranslated', data)
# Make sure the plurals string is still valid # Make sure the plurals string is still valid
with open(self.dest_file, 'rb') as f_: with open(self.dest_file, 'r') as f_:
content = f_.read() content = f_.read()
self.assertTrue('Hello, world' in str(content)) self.assertTrue('Hello, world' in str(content))
self.assertTrue('|| n%100>=20) ? 1 : 2)' in str(content)) self.assertTrue('|| n%100>=20) ? 1 : 2)' in str(content))
@ -277,13 +275,13 @@ class RosettaTestCase(TestCase):
# this user. # this user.
with self.settings(ROSETTA_REQUIRES_AUTH=True): with self.settings(ROSETTA_REQUIRES_AUTH=True):
r = self.client3.get(self.xx_form_url) r = self.client3.get(self.xx_form_url)
self.assertFalse(r.content) self.assertFalse(r.content.decode())
self.assertEqual(r.status_code, 302) self.assertEqual(r.status_code, 302)
# When it's not required, we sail through. # When it's not required, we sail through.
with self.settings(ROSETTA_REQUIRES_AUTH=False): with self.settings(ROSETTA_REQUIRES_AUTH=False):
r = self.client3.get(self.xx_form_url) r = self.client3.get(self.xx_form_url)
self.assertTrue(r.content) self.assertTrue(r.content.decode())
self.assertEqual(r.status_code, 200) self.assertEqual(r.status_code, 200)
@override_settings(ROSETTA_LANGUAGES=(('fr', 'French'), ('xx', 'Dummy Language'))) @override_settings(ROSETTA_LANGUAGES=(('fr', 'French'), ('xx', 'Dummy Language')))
@ -509,13 +507,13 @@ class RosettaTestCase(TestCase):
data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'} data = {'m_e48f149a8b2e8baa81b816c0edf93890': 'Hello, world'}
r = self.client.post(self.xx_form_url + '?filter=untranslated', data) r = self.client.post(self.xx_form_url + '?filter=untranslated', data)
# read the result # read the result
with open(self.dest_file, 'rb') as f_: with open(self.dest_file, 'r') as f_:
content = six.text_type(f_.read()) content = f_.read()
# make sure unicode data was properly converted to ascii # make sure unicode data was properly converted to ascii
self.assertTrue('Hello, world' in content) self.assertTrue('Hello, world' in content)
self.assertTrue('save_header_data@test.com' in content) self.assertTrue('save_header_data@test.com' in content)
self.assertTrue('aeaeae aaaaaaa aaaa uuuu' in content) self.assertTrue('aéaéaé aàaàaàa aâââ üüüü' in content)
def test_24_percent_translation(self): def test_24_percent_translation(self):
self.copy_po_file_from_template('./django.po.template') self.copy_po_file_from_template('./django.po.template')
@ -741,12 +739,8 @@ class RosettaTestCase(TestCase):
def test_40_issue_155_auto_compile(self): def test_40_issue_155_auto_compile(self):
def file_hash(file_string): def file_hash(file_string):
if six.PY3: with open(file_string, encoding="latin-1") as file:
with open(file_string, encoding="latin-1") as file: file_content = file.read().encode('utf-8')
file_content = file.read().encode('utf-8')
else:
with open(file_string) as file:
file_content = file.read()
return hashlib.md5(file_content).hexdigest() return hashlib.md5(file_content).hexdigest()
def message_hashes(): def message_hashes():
@ -886,7 +880,7 @@ class RosettaTestCase(TestCase):
# But if the language isn't an option, we get a 404 # But if the language isn't an option, we get a 404
with self.settings( with self.settings(
ROSETTA_LANGUAGES=[l for l in settings.LANGUAGES if l[0] != 'xx'] ROSETTA_LANGUAGES=[lang for lang, __ in settings.LANGUAGES if lang != 'xx']
): ):
view = self._setup_view( view = self._setup_view(
view=views.TranslationFormView(), request=request, **kwargs view=views.TranslationFormView(), request=request, **kwargs

View file

@ -2,20 +2,16 @@ import hashlib
import os import os
import os.path import os.path
import re import re
import unicodedata
import zipfile import zipfile
from urllib.parse import urlencode from urllib.parse import urlencode
import six
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import user_passes_test from django.contrib.auth.decorators import user_passes_test
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.http import ( from django.http import (Http404, HttpResponse, HttpResponseRedirect,
Http404, JsonResponse)
HttpResponse,
HttpResponseRedirect,
JsonResponse
)
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator from django.utils.decorators import method_decorator
from django.utils.encoding import force_bytes from django.utils.encoding import force_bytes
@ -23,8 +19,6 @@ from django.utils.functional import Promise, cached_property
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.decorators.cache import never_cache from django.views.decorators.cache import never_cache
from django.views.generic import TemplateView, View from django.views.generic import TemplateView, View
import six
from polib import pofile from polib import pofile
from . import get_version as get_rosetta_version from . import get_version as get_rosetta_version
@ -104,7 +98,7 @@ class RosettaFileLevelMixin(RosettaBaseMixin):
""" """
# (Formerly known as "rosetta_i18n_lang_code") # (Formerly known as "rosetta_i18n_lang_code")
lang_id = self.kwargs['lang_id'] lang_id = self.kwargs['lang_id']
if lang_id not in {l[0] for l in rosetta_settings.ROSETTA_LANGUAGES}: if lang_id not in {lang[0] for lang in rosetta_settings.ROSETTA_LANGUAGES}:
raise Http404 raise Http404
if not can_translate_language(self.request.user, lang_id): if not can_translate_language(self.request.user, lang_id):
raise Http404 raise Http404
@ -227,7 +221,8 @@ class TranslationFileListView(RosettaBaseMixin, TemplateView):
third_party_apps=third_party_apps, third_party_apps=third_party_apps,
) )
po_files = [ po_files = [
(get_app_name(l), os.path.realpath(l), pofile(l)) for l in po_paths (get_app_name(lang), os.path.realpath(lang), pofile(lang))
for lang in po_paths
] ]
po_files.sort(key=lambda app: app[0]) po_files.sort(key=lambda app: app[0])
languages.append((language[0], _(language[1]), po_files)) languages.append((language[0], _(language[1]), po_files))
@ -367,15 +362,11 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
if file_change and self.po_file_is_writable: if file_change and self.po_file_is_writable:
try: try:
self.po_file.metadata['Last-Translator'] = unicodedata.normalize( self.po_file.metadata['Last-Translator'] = "{} {} <{}>".format(
'NFKD', getattr(self.request.user, 'first_name', 'Anonymous'),
u"%s %s <%s>" getattr(self.request.user, 'last_name', 'User'),
% ( getattr(self.request.user, 'email', 'anonymous@user.tld'),
getattr(self.request.user, 'first_name', 'Anonymous'), )
getattr(self.request.user, 'last_name', 'User'),
getattr(self.request.user, 'email', 'anonymous@user.tld'),
),
).encode('ascii', 'ignore')
self.po_file.metadata['X-Translated-Using'] = u"django-rosetta %s" % ( self.po_file.metadata['X-Translated-Using'] = u"django-rosetta %s" % (
get_rosetta_version() get_rosetta_version()
) )
@ -412,7 +403,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
import uwsgi import uwsgi
uwsgi.reload() # pretty easy right? uwsgi.reload() # pretty easy right?
except: except Exception:
pass # we may not be running under uwsgi :P pass # we may not be running under uwsgi :P
# XXX: It would be nice to add a success message here! # XXX: It would be nice to add a success message here!
except Exception as e: except Exception as e:
@ -575,7 +566,7 @@ class TranslationFormView(RosettaFileLevelMixin, TemplateView):
""" """
ref_lang = self._request_request('ref_lang', 'msgid') ref_lang = self._request_request('ref_lang', 'msgid')
if ref_lang != 'msgid': if ref_lang != 'msgid':
allowed_languages = {l[0] for l in rosetta_settings.ROSETTA_LANGUAGES} allowed_languages = {lang[0] for lang in rosetta_settings.ROSETTA_LANGUAGES}
if ref_lang not in allowed_languages: if ref_lang not in allowed_languages:
raise Http404 raise Http404
return ref_lang return ref_lang

View file

@ -42,6 +42,7 @@ deps =
goslate goslate
vcrpy vcrpy
coverage coverage
pudb
[testenv:gettext] [testenv:gettext]
basepython = python3 basepython = python3
@ -74,6 +75,6 @@ commands=
[testenv:flake8] [testenv:flake8]
basepython = python3 basepython = python3
deps = flake8==2.4.1 deps = flake8==3.9.2
commands= commands=
flake8 {toxinidir}/rosetta flake8 {toxinidir}/rosetta