Merge branch 'develop' into reflang

This commit is contained in:
Virgil Dupras 2013-01-29 15:53:17 -05:00
commit 31a0427f78
14 changed files with 185 additions and 52 deletions

4
.gitignore vendored
View file

@ -5,4 +5,6 @@ dist
.svnignore
.svnexternals
build
rosetta/locale/xx/LC_MESSAGES/*.mo
rosetta/locale/xx/LC_MESSAGES/*.mo
/.settings
/.project

View file

@ -1,3 +1,7 @@
* Support timezones on the last modified PO header. Thanks @jmoiron (Issue #43)
* Actually move to the next block when submitting a lot of translations (Issue #13)
* Add msgctxt to the entry hash to differentiate entries with context. Thanks @metalpriest (Issue #39)
Version 0.6.8
-------------
* Switched to a pluggable storage backend model to increase compatibility with Django 1.4. Cache and Session-based storages are provided.

View file

@ -20,7 +20,7 @@ Features
Requirements
************
Rosetta requires Django 1.3 or later (it should work with Django 1.1 and 1.2, but it is not supported.)
Rosetta requires Django 1.3 or later
************
Installation

View file

@ -1,4 +1,4 @@
VERSION = (0, 6, 8)
VERSION = (0, 7, 0)
def get_version(svn=False, limit=3):

View file

@ -3,6 +3,12 @@ import django
from django.conf import settings
from rosetta.conf import settings as rosetta_settings
from django.core.cache import cache
from datetime import datetime
try:
from django.utils import timezone
except:
timezone = None
try:
set
@ -10,6 +16,22 @@ except NameError:
from sets import Set as set # Python 2.3 fallback
def timestamp_with_timezone(dt=None):
"""
Return a timestamp with a timezone for the configured locale. If all else
fails, consider localtime to be UTC.
"""
dt = dt or datetime.now()
if timezone is None:
return dt.strftime('%Y-%m-%d %H:%M%z')
if not dt.tzinfo:
tz = timezone.get_current_timezone()
if not tz:
tz = timezone.utc
dt = dt.replace(tzinfo=timezone.get_current_timezone())
return dt.strftime("%Y-%m-%d %H:%M%z")
def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False):
"""
scans a couple possible repositories of gettext catalogs for the given

View file

@ -1,5 +1,7 @@
from django.core.cache import cache
from django.conf import settings
from django.utils import importlib
from django.core.exceptions import ImproperlyConfigured
import hashlib
import time
@ -56,12 +58,28 @@ class CacheRosettaStorage(BaseRosettaStorage):
# so we need to per-user key prefix, which we store in the session
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']
else:
self._key_prefix = hashlib.new('sha1', str(time.time())).hexdigest()
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['default']['BACKEND'].lower():
raise ImproperlyConfigured("You can't use the CacheRosettaStorage if your cache isn't correctly set up (you are use the DummyCache cache backend).")
# Make sure the actually actually works
try:
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')
def get(self, key, default=None):
#print ('get', self._key_prefix + key)
return cache.get(self._key_prefix + key, default)

View file

@ -1,16 +1,16 @@
<!DOCTYPE html>
<!DOCTYPE html>{% load url from future %}
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>{% block pagetitle %}Rosetta{% endblock %}</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="{{ADMIN_MEDIA_PREFIX}}css/base.css" type="text/css"/>
<link rel="stylesheet" href="{{ADMIN_MEDIA_PREFIX}}css/forms.css" type="text/css"/>
<link rel="stylesheet" href="{{ADMIN_MEDIA_PREFIX}}css/changelists.css" type="text/css"/>
<link rel="stylesheet" href="{{ADMIN_MEDIA_PREFIX}}css/changelists.css" type="text/css"/>
<style type="text/css" media="screen">
{% include 'rosetta/css/rosetta.css' %}
</style>
<script src="http://www.google.com/jsapi" type="text/javascript"></script>
<script src="http://www.google.com/jsapi" type="text/javascript"></script>
<script type="text/javascript">
//<!--
google.load("jquery", "1.3");
@ -24,7 +24,7 @@
<div id="header">
{% block header %}
<div id="branding">
<h1 id="site-name"><a href="{% url rosetta-pick-file %}">Rosetta</a> </h1>
<h1 id="site-name"><a href="{% url 'rosetta-pick-file' %}">Rosetta</a> </h1>
</div>
{% endblock %}
</div>

View file

@ -1,10 +1,11 @@
{% extends "rosetta/base.html" %}
{% load i18n %}
{% load url from future %}
{% block pagetitle %}{{block.super}} - {% trans "Language selection" %}{% endblock %}
{% block breadcumbs %}
<div><a href="{% url rosetta-pick-file %}">{% trans "Home" %}</a> &rsaquo; {% trans "Language selection" %}</div>
<div><a href="{% url 'rosetta-pick-file' %}">{% trans "Home" %}</a> &rsaquo; {% trans "Language selection" %}</div>
{% if do_session_warn %}<p class="errornote session-warn">{% trans "Couldn't load the specified language file. This usually happens when using the Encrypted Cookies Session Storage backend on Django 1.4 or higher.<br/>Setting ROSETTA_STORAGE_CLASS = 'rosetta.storage.CacheRosettaStorage' in your settings file should fix this." %}</p>{% endif %}
{% endblock %}
@ -22,7 +23,7 @@
{% for lid,language,pos in languages %}
{% if pos %}
<div class="module">
<h2>{{language}}</h2>
<table cellspacing="0">
@ -40,7 +41,7 @@
<tbody>
{% for app,path,po in pos %}
<tr class="{% cycle row1,row2 %}">
<td><a href="{% url rosetta-language-selection lid,forloop.counter0 %}{% if do_django %}?django{% endif %}{% if do_rosetta %}?rosetta{% endif %}">{{ app|title }}</a></td>
<td><a href="{% url 'rosetta-language-selection' lid forloop.counter0 %}{% if do_django %}?django{% endif %}{% if do_rosetta %}?rosetta{% endif %}">{{ app|title }}</a></td>
<td class="ch-progress r">{{po.percent_translated|floatformat:2}}%</td>
{% with po.untranslated_entries|length as len_untranslated_entries %}
<td class="ch-messages r">{{po.translated_entries|length|add:len_untranslated_entries}}</td>
@ -55,7 +56,7 @@
</table>
</div>
{% endif %}
{% endfor %}
{% endfor %}
{% else %}
<h1>{% trans "Nothing to translate!" %}</h1>
<p>{% trans "You haven't specified any languages in your settings file, or haven't yet generated a batch of translation catalogs." %}</p>

View file

@ -1,26 +1,27 @@
{% extends "rosetta/base.html" %}
{% load rosetta i18n %}
{% load url from future %}
{% block header %}
{{block.super}}
<div id="user-tools">
<p>
<span><a href="{% url rosetta-pick-file %}">{% trans "Pick another file" %}</a> /
<a href="{% url rosetta-download-file %}">{% trans "Download this catalog" %}</a></span>
<span><a href="{% url 'rosetta-pick-file' %}">{% trans "Pick another file" %}</a> /
<a href="{% url 'rosetta-download-file' %}">{% trans "Download this catalog" %}</a></span>
</p>
</div>
<script type="text/javascript">
</script>
{% endblock %}
{% block pagetitle %}{{block.super}} - {{MESSAGES_SOURCE_LANGUAGE_NAME}} - {{rosetta_i18n_lang_name}} ({{ rosetta_i18n_pofile.percent_translated|floatformat:0 }}%){% endblock %}
{% block breadcumbs %}
<div>
<a href="{% url rosetta-pick-file %}">{% trans "Home" %}</a> &rsaquo;
{{ rosetta_i18n_lang_name }} &rsaquo;
{{ rosetta_i18n_app|title }} &rsaquo;
<a href="{% url 'rosetta-pick-file' %}">{% trans "Home" %}</a> &rsaquo;
{{ rosetta_i18n_lang_name }} &rsaquo;
{{ rosetta_i18n_app|title }} &rsaquo;
{% blocktrans with rosetta_i18n_pofile.percent_translated|floatformat:2 as percent_translated %}Progress: {{ percent_translated }}%{% endblocktrans %}
</div>
{% if not rosetta_i18n_write %}<p class="errornote read-only">{% trans "File is read-only: download the file when done editing!" %}</p>{% endif %}
@ -29,7 +30,7 @@
{% block main %}
<h1>{% blocktrans %}Translate into {{rosetta_i18n_lang_name}}{% endblocktrans %}</h1>
<ul class="object-tools">
<li class="nobubble">{% trans "Display:" %}</li>
<li {% ifequal rosetta_i18n_filter 'untranslated' %}class="active"{% endifequal %}><a href="?filter=untranslated">{% trans "Untranslated only" %}</a></li>
@ -64,7 +65,7 @@
{% endif %}
{% endcomment %}
</div>
<form method="post" action="">
<table>
<thead>
@ -86,7 +87,7 @@
<span class="part">{{message.msgid|format_message|linebreaksbr}}</span>
<span class="part">{{message.msgid_plural|format_message|linebreaksbr}}</span>
</div>
{% if message.msgctxt %}
<span class="context">{% trans "Context hint" %}: {{message.msgctxt|safe}}</span>
{% else %}
@ -94,7 +95,7 @@
<span class="context">{% trans "Context hint" %}: {{message.comment|safe}}</span>
{% endif %}
{% endif %}
</td>
<td class="translation">
{% for k, msgstr in message.msgstr_plural.items|dictsort:"0" %}
@ -110,7 +111,7 @@
{% else %}
{% if message.comment %}
<span class="context">{% trans "Context hint" %}: {{message.comment|safe}}</span>
{% endif %}
{% endif %}
{% endif %}
</td>
{% if main_language %}<td class="original">{{ message.main_lang|format_message|linebreaksbr }}</td>{% endif %}
@ -124,7 +125,7 @@
</td>
<td class="location">
{% for fn,lineno in message.occurrences %}
<code{% if forloop.counter|gt:"3" %} class="hide"{% endif %}>{{ fn }}:{{lineno}}</code>
<code{% if forloop.counter|gt:"3" %} class="hide"{% endif %}>{{ fn }}:{{lineno}}</code>
{% endfor %}
{% if message.occurrences|length|gt:"3" %}
<a href="#">&hellip; ({% blocktrans count message.occurrences|length|minus:"3" as more_count %}{{more_count}} more{% plural %}{{more_count}} more{% endblocktrans %})</a>
@ -140,8 +141,8 @@
<input type="hidden" name="query" value="{{query}}" />
{% endif %}
<input type="submit" class="default" name="_next" value="{% trans "Save and translate next block" %}" tabindex="{% increment tab_idx %}"/>
{% if needs_pagination %}
{% trans "Skip to page:" %}
{% for i in page_range %}
@ -150,7 +151,7 @@
{% else %}
{% ifequal i page %}
<span class="this-page">{{i}}</span>
{% else %}
{% else %}
<a href="?page={{i}}{% if query %}&amp;query={{query}}{% endif %}">{{i}}</a>
{% endifequal %}
{% endifequal %}
@ -158,12 +159,12 @@
{% else %}
{% trans "Displaying:" %}
{% endif %}
{% with paginator.object_list|length as hits %}
<strong>{% blocktrans count rosetta_i18n_pofile|length as message_number %}{{hits}}/{{message_number}} message{% plural %}{{hits}}/{{message_number}} messages{% endblocktrans %}</strong>
<strong>{% blocktrans count rosetta_i18n_pofile|length as message_number %}{{hits}}/{{message_number}} message{% plural %}{{hits}}/{{message_number}} messages{% endblocktrans %}</strong>
{% endwith %}
</p>
</div>
</form>

View file

@ -203,7 +203,7 @@ class RosettaTestCase(TestCase):
self.assertTrue('String 1' in r.content)
self.assertTrue('String 1' in r2.content)
self.assertTrue('m_08e4e11e2243d764fc45a5a4fba5d0f2' in r.content)
r = self.client.post(reverse('rosetta-home'), dict(m_08e4e11e2243d764fc45a5a4fba5d0f2='Hello, world', _next='_next'))
r = self.client.post(reverse('rosetta-home'), dict(m_08e4e11e2243d764fc45a5a4fba5d0f2='Hello, world', _next='_next'), follow=True)
r2 = self.client2.get(reverse('rosetta-home'))
# Client 2 reloads the home, forces a reload of the catalog,
@ -220,14 +220,19 @@ class RosettaTestCase(TestCase):
self.assertTrue('String 2' in r.content and 'm_e48f149a8b2e8baa81b816c0edf93890' in r.content)
# client 2 posts!
r2 = self.client2.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world, from client two!', _next='_next'))
r2 = self.client2.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world, from client two!', _next='_next'), follow=True)
self.assertTrue('save-conflict' not in r2.content)
# uh-oh here comes client 1
r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world, from client one!', _next='_next'))
r = self.client.post(reverse('rosetta-home'), dict(m_e48f149a8b2e8baa81b816c0edf93890='Hello, world, from client one!', _next='_next'), follow=True)
# An error message is displayed
self.assertTrue('save-conflict' in r.content)
# client 2 won
pofile_content = open(self.dest_file, 'r').read()
self.assertTrue('Hello, world, from client two!' in pofile_content)
# Both clients show all strings, error messages are gone
r = self.client.get(reverse('rosetta-home') + '?filter=translated')
self.assertTrue('save-conflict' not in r.content)
@ -472,3 +477,14 @@ class RosettaTestCase(TestCase):
self.client2.get(reverse('rosetta-language-selection', args=('xx', 0, ), kwargs=dict()))
self.assertTrue(self.client.session.get('rosetta_cache_storage_key_prefix') != self.client2.session.get('rosetta_cache_storage_key_prefix'))
def test_21_Test_Issue_gh39(self):
shutil.copy(os.path.normpath(os.path.join(self.curdir, './django.po.issue39gh.template')), self.dest_file)
self.client.get(reverse('rosetta-pick-file') + '?filter=third-party')
r = self.client.get(reverse('rosetta-language-selection', args=('xx', 0), kwargs=dict()))
r = self.client.get(reverse('rosetta-home'))
# We have distinct hashes, even though the msgid and msgstr are identical
self.assertTrue('m_4765f7de94996d3de5975fa797c3451f' in r.content)
self.assertTrue('m_08e4e11e2243d764fc45a5a4fba5d0f2' in r.content)

View file

@ -0,0 +1,26 @@
# SOME DESCRIPTIVE TITLE.
# 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"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-10-21 12:21+0200\n"
"PO-Revision-Date: 2008-09-22 11:02\n"
"Last-Translator: Admin Admin <admin@admin.com>\n"
"Language-Team: French <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Translated-Using: django-rosetta 0.4.RC2\n"
#: templates/base.html:43
msgctxt "Report (by_customer). Parent table: invoices"
msgid "String 1"
msgstr ""
#: templates/base.html:44
msgid "String 1"
msgstr ""

View file

@ -10,7 +10,7 @@ from django.utils.translation import ugettext_lazy as _
from django.views.decorators.cache import never_cache
from rosetta.conf import settings as rosetta_settings
from rosetta.polib import pofile
from rosetta.poutil import find_pos, pagination_range
from rosetta.poutil import find_pos, pagination_range, timestamp_with_timezone
from rosetta.signals import entry_changed, post_save
from rosetta.storage import get_storage
import re
@ -65,7 +65,11 @@ def home(request):
if rosetta_i18n_write:
rosetta_i18n_pofile = pofile(rosetta_i18n_fn, wrapwidth=rosetta_settings.POFILE_WRAP_WIDTH)
for entry in rosetta_i18n_pofile:
entry.md5hash = hashlib.md5(entry.msgid.encode("utf8") + entry.msgstr.encode("utf8")).hexdigest()
entry.md5hash = hashlib.md5(
entry.msgid.encode("utf8") +
entry.msgstr.encode("utf8") +
(entry.msgctxt and entry.msgctxt.encode("utf8") or "")
).hexdigest()
else:
rosetta_i18n_pofile = storage.get('rosetta_i18n_pofile')
@ -140,7 +144,7 @@ def home(request):
rosetta_i18n_pofile.metadata['Last-Translator'] = unicodedata.normalize('NFKD', u"%s %s <%s>" % (request.user.first_name, request.user.last_name, request.user.email)).encode('ascii', 'ignore')
rosetta_i18n_pofile.metadata['X-Translated-Using'] = u"django-rosetta %s" % rosetta.get_version(False)
rosetta_i18n_pofile.metadata['PO-Revision-Date'] = datetime.datetime.now().strftime('%Y-%m-%d %H:%M%z')
rosetta_i18n_pofile.metadata['PO-Revision-Date'] = timestamp_with_timezone()
except UnicodeDecodeError:
pass
@ -176,15 +180,11 @@ def home(request):
storage.set('rosetta_i18n_pofile', rosetta_i18n_pofile)
# Retain query arguments
query_arg = ''
if 'query' in request.REQUEST:
query_arg = '?query=%s' % request.REQUEST.get('query')
query_arg = '?_next=1'
if 'query' in request.GET or 'query' in request.POST:
query_arg += '&query=%s' % request.REQUEST.get('query')
if 'page' in request.GET:
if query_arg:
query_arg = query_arg + '&'
else:
query_arg = '?'
query_arg = query_arg + 'page=%d' % int(request.GET.get('page'))
query_arg += '&page=%d&_next=1' % int(request.GET.get('page'))
return HttpResponseRedirect(reverse('rosetta-home') + iri_to_uri(query_arg))
rosetta_i18n_lang_name = _(storage.get('rosetta_i18n_lang_name'))
rosetta_i18n_lang_code = storage.get('rosetta_i18n_lang_code')
@ -215,6 +215,14 @@ def home(request):
page = int(request.GET.get('page'))
else:
page = 1
if '_next' in request.GET or '_next' in request.POST:
page += 1
if page > paginator.num_pages:
page = 1
query_arg = '?page=%d' % page
return HttpResponseRedirect(reverse('rosetta-home') + iri_to_uri(query_arg))
rosetta_messages = paginator.page(page).object_list
if rosetta_settings.MAIN_LANGUAGE and rosetta_settings.MAIN_LANGUAGE != rosetta_i18n_lang_code:
@ -367,7 +375,11 @@ def lang_sel(request, langid, idx):
storage.set('rosetta_i18n_fn', file_)
po = pofile(file_)
for entry in po:
entry.md5hash = hashlib.md5(entry.msgid.encode("utf8") + entry.msgstr.encode("utf8")).hexdigest()
entry.md5hash = hashlib.md5(
entry.msgid.encode("utf8") +
entry.msgstr.encode("utf8") +
(entry.msgctxt and entry.msgctxt.encode("utf8") or "")
).hexdigest()
storage.set('rosetta_i18n_pofile', po)
try:

23
runtests_multi_venv.sh Normal file
View file

@ -0,0 +1,23 @@
#!/bin/bash
. venv_13/bin/activate
cd testproject
python manage.py --version
python manage.py test rosetta
cd ..
deactivate
. venv_14/bin/activate
cd testproject
python manage.py --version
python manage.py test rosetta
cd ..
deactivate
. venv_15/bin/activate
cd testproject
python manage.py --version
python manage.py test rosetta
cd ..
deactivate

View file

@ -17,6 +17,17 @@ DATABASES = {
}
}
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11211',
'KEY_PREFIX': 'ROSETTA_TEST'
}
}
#CACHES = {'default': {'BACKEND': 'django.core.cache.backends.dummy.DummyCache'}}
TEST_DATABASE_CHARSET = "utf8"
TEST_DATABASE_COLLATION = "utf8_general_ci"
@ -49,12 +60,9 @@ ROOT_URLCONF = 'testproject.urls'
DEBUG = True
TEMPLATE_DEBUG = True
STATIC_URL = '/static/'
#SESSION_ENGINE = "django.contrib.sessions.backends.signed_cookies"
#ROSETTA_STORAGE_CLASS = 'rosetta.storage.SessionRosettaStorage'
ROSETTA_STORAGE_CLASS = 'rosetta.storage.CacheRosettaStorage'
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.memcached.MemcachedCache',
'LOCATION': '127.0.0.1:11212',
}
}
SECRET_KEY = 'empty'