diff --git a/.gitignore b/.gitignore
index 7c640e4..6fa2aa7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,9 @@ build
rosetta/locale/xx/LC_MESSAGES/*.mo
/.settings
/.project
+testproject/coverage.xml
+testproject/htmlcov/
+testproject/rosetta.db
+testproject/src/
+testproject/.coverage
+venv_*
diff --git a/CHANGES b/CHANGES
index 7ab3b2b..1d9ad62 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,6 +1,20 @@
+Version 0.7.2
+-------------
+* Fix for when settings imports unicode_literals for some reason (Issue #67)
+
+Version 0.7.1
+-------------
+* Fix: value missing in context
+
+Version 0.7.0
+-------------
+* Support for Django 1.5 and HEAD, support for Python 3.
+* Upgraded bundled polib to version 1.0.3 - http://pypi.python.org/pypi/polib/1.0.3
* 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)
+* Better discovery of locale files on Django 1.4+ Thanks @tijs (Issues #63, #64)
+* List apps in alphabetical order
Version 0.6.8
-------------
@@ -16,7 +30,7 @@ Version 0.6.7
Version 0.6.6
-------------
* Django 1.4 support (Issue #30, #33)
-* Better handling of translation callbacks on Bing's translation API and support of composite locales (Issue #26)
+* Better handling of translation callbacks on Bing's translation API and support of composite locales (Issue #26)
Version 0.6.5
-------------
@@ -26,7 +40,7 @@ Version 0.6.5
Version 0.6.4
-------------
-* Added ROSETTA_REQUIRES_AUTH option to grant access to non authenticated users (False by default)
+* Added ROSETTA_REQUIRES_AUTH option to grant access to non authenticated users (False by default)
Version 0.6.3
-------------
diff --git a/MANIFEST.in b/MANIFEST.in
index 4166b83..d07c984 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,9 @@
include MANIFEST.in
include LICENSE
+exclude *.pyc
+exclude *.sh
recursive-include rosetta/locale *
recursive-include rosetta/tests *
recursive-include rosetta/templates *
prune testproject
+prune rosetta/tests/__pycache__
diff --git a/README.rst b/README.rst
index 0dc86a7..fc2c3d4 100644
--- a/README.rst
+++ b/README.rst
@@ -20,7 +20,7 @@ Features
Requirements
************
-Rosetta requires Django 1.3 or later
+Rosetta requires Django 1.3, 1.4 or 1.5. When running with Django 1.5, Python 3.x is supported.
************
Installation
@@ -105,7 +105,7 @@ Usage
Generate a batch of files to translate
--------------------------------------
-See `Django's documentation on Internationalization `_ to setup your project to use i18n and create the ``gettext`` catalog files.
+See `Django's documentation on Internationalization `_ to setup your project to use i18n and create the ``gettext`` catalog files.
Translate away!
---------------
@@ -141,5 +141,5 @@ By default Rosetta hides its own catalog files in the file selection interface (
Acknowledgments
***************
-* Rosetta uses the excellent `polib `_ library to parse and handle gettext files.
+* Rosetta uses the excellent `polib `_ library to parse and handle gettext files.
diff --git a/rosetta/__init__.py b/rosetta/__init__.py
index 7655aad..711524e 100644
--- a/rosetta/__init__.py
+++ b/rosetta/__init__.py
@@ -1,4 +1,4 @@
-VERSION = (0, 7, 0)
+VERSION = (0, 7, 2)
def get_version(svn=False, limit=3):
diff --git a/rosetta/polib.py b/rosetta/polib.py
index b151b03..a40fcec 100644
--- a/rosetta/polib.py
+++ b/rosetta/polib.py
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*-
+# -* coding: utf-8 -*-
#
# License: MIT (see LICENSE file provided)
# vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4:
@@ -12,10 +12,10 @@ modify entries, comments or metadata, etc. or create new po files from scratch.
:func:`~polib.mofile` convenience functions.
"""
-__author__ = 'David Jean Louis '
-__version__ = '0.7.0'
-__all__ = ['pofile', 'POFile', 'POEntry', 'mofile', 'MOFile', 'MOEntry',
- 'detect_encoding', 'escape', 'unescape', 'detect_encoding',]
+__author__ = 'David Jean Louis '
+__version__ = '1.0.3'
+__all__ = ['pofile', 'POFile', 'POEntry', 'mofile', 'MOFile', 'MOEntry',
+ 'default_encoding', 'escape', 'unescape', 'detect_encoding', ]
import array
import codecs
@@ -25,11 +25,13 @@ import struct
import sys
import textwrap
+
# the default encoding to use when encoding cannot be detected
default_encoding = 'utf-8'
# python 2/3 compatibility helpers {{{
+
if sys.version_info[:2] < (3, 0):
PY3 = False
text_type = unicode
@@ -49,10 +51,10 @@ else:
def u(s):
return s
-
# }}}
# _pofile_or_mofile {{{
+
def _pofile_or_mofile(f, type, **kwargs):
"""
Internal function used by :func:`polib.pofile` and :func:`polib.mofile` to
@@ -68,15 +70,16 @@ def _pofile_or_mofile(f, type, **kwargs):
parser = kls(
f,
encoding=enc,
- check_for_duplicates=kwargs.get('check_for_duplicates', False)
+ check_for_duplicates=kwargs.get('check_for_duplicates', False),
+ klass=kwargs.get('klass')
)
instance = parser.parse()
instance.wrapwidth = kwargs.get('wrapwidth', 78)
return instance
-
# }}}
# function pofile() {{{
+
def pofile(pofile, **kwargs):
"""
Convenience function that parses the po or pot file ``pofile`` and returns
@@ -98,12 +101,17 @@ def pofile(pofile, **kwargs):
``check_for_duplicates``
whether to check for duplicate entries when adding entries to the
file (optional, default: ``False``).
+
+ ``klass``
+ class which is used to instantiate the return value (optional,
+ default: ``None``, the return value with be a :class:`~polib.POFile`
+ instance).
"""
return _pofile_or_mofile(pofile, 'pofile', **kwargs)
-
# }}}
# function mofile() {{{
+
def mofile(mofile, **kwargs):
"""
Convenience function that parses the mo file ``mofile`` and returns a
@@ -126,12 +134,17 @@ def mofile(mofile, **kwargs):
``check_for_duplicates``
whether to check for duplicate entries when adding entries to the
file (optional, default: ``False``).
+
+ ``klass``
+ class which is used to instantiate the return value (optional,
+ default: ``None``, the return value with be a :class:`~polib.POFile`
+ instance).
"""
return _pofile_or_mofile(mofile, 'mofile', **kwargs)
-
# }}}
# function detect_encoding() {{{
+
def detect_encoding(file, binary_mode=False):
"""
Try to detect the encoding used by the ``file``. The ``file`` argument can
@@ -159,7 +172,12 @@ def detect_encoding(file, binary_mode=False):
return False
return True
- if not os.path.exists(file):
+ try:
+ is_file = os.path.exists(file)
+ except (ValueError, UnicodeEncodeError):
+ is_file = False
+
+ if not is_file:
match = rxt.search(file)
if match:
enc = match.group(1).strip()
@@ -185,10 +203,10 @@ def detect_encoding(file, binary_mode=False):
return enc
f.close()
return default_encoding
-
# }}}
# function escape() {{{
+
def escape(st):
"""
Escapes the characters ``\\\\``, ``\\t``, ``\\n``, ``\\r`` and ``"`` in
@@ -199,10 +217,10 @@ def escape(st):
.replace('\r', r'\r')\
.replace('\n', r'\n')\
.replace('\"', r'\"')
-
# }}}
# function unescape() {{{
+
def unescape(st):
"""
Unescapes the characters ``\\\\``, ``\\t``, ``\\n``, ``\\r`` and ``"`` in
@@ -218,12 +236,12 @@ def unescape(st):
return '\r'
if m == '\\':
return '\\'
- return m # handles escaped double quote
+ return m # handles escaped double quote
return re.sub(r'\\(\\|n|t|r|")', unescape_repl, st)
-
# }}}
# class _BaseFile {{{
+
class _BaseFile(list):
"""
Common base class for the :class:`~polib.POFile` and :class:`~polib.MOFile`
@@ -301,15 +319,17 @@ class _BaseFile(list):
Overriden ``list`` method to implement the membership test (in and
not in).
The method considers that an entry is in the file if it finds an entry
- that has the same msgid (the test is **case sensitive**).
+ that has the same msgid (the test is **case sensitive**) and the same
+ msgctxt (or none for both entries).
Argument:
``entry``
an instance of :class:`~polib._BaseEntry`.
"""
- return self.find(entry.msgid, by='msgid') is not None
-
+ return self.find(entry.msgid, by='msgid', msgctxt=entry.msgctxt) \
+ is not None
+
def __eq__(self, other):
return str(self) == str(other)
@@ -420,7 +440,7 @@ class _BaseFile(list):
entries = [e for e in self if not e.obsolete]
for e in entries:
if getattr(e, by) == st:
- if msgctxt and e.msgctxt != msgctxt:
+ if msgctxt is not False and e.msgctxt != msgctxt:
continue
return e
return None
@@ -428,7 +448,7 @@ class _BaseFile(list):
def ordered_metadata(self):
"""
Convenience method that returns an ordered version of the metadata
- dictionnary. The return value is list of tuples (metadata name,
+ dictionary. The return value is list of tuples (metadata name,
metadata_value).
"""
# copy the dict first
@@ -464,6 +484,7 @@ class _BaseFile(list):
"""
offsets = []
entries = self.translated_entries()
+
# the keys are sorted in the .mo file
def cmp(_self, other):
# msgfmt compares entries with msgctxt if it exists
@@ -500,11 +521,11 @@ class _BaseFile(list):
msgid += self._encode(e.msgid)
msgstr = self._encode(e.msgstr)
offsets.append((len(ids), len(msgid), len(strs), len(msgstr)))
- ids += msgid + b('\0')
+ ids += msgid + b('\0')
strs += msgstr + b('\0')
# The header is 7 32-bit unsigned integers.
- keystart = 7*4+16*entries_len
+ keystart = 7 * 4 + 16 * entries_len
# and the values start after the keys
valuestart = keystart + len(ids)
koffsets = []
@@ -512,8 +533,8 @@ class _BaseFile(list):
# The string table first has the list of keys, then the list of values.
# Each entry has first the size of the string, then the file offset.
for o1, l1, o2, l2 in offsets:
- koffsets += [l1, o1+keystart]
- voffsets += [l2, o2+valuestart]
+ koffsets += [l1, o1 + keystart]
+ voffsets += [l2, o2 + valuestart]
offsets = koffsets + voffsets
# check endianness for magic number
if struct.pack('@h', 1) == struct.pack(' 1: # python 3.2 or superior
output += array.array("i", offsets).tobytes()
@@ -547,10 +574,10 @@ class _BaseFile(list):
if isinstance(mixed, text_type):
mixed = mixed.encode(self.encoding)
return mixed
-
# }}}
# class POFile {{{
+
class POFile(_BaseFile):
"""
Po (or Pot) file reader/writer.
@@ -606,7 +633,7 @@ class POFile(_BaseFile):
"""
Convenience method that returns the list of untranslated entries.
"""
- return [e for e in self if not e.translated() and not e.obsolete \
+ return [e for e in self if not e.translated() and not e.obsolete
and not 'fuzzy' in e.flags]
def fuzzy_entries(self):
@@ -637,32 +664,36 @@ class POFile(_BaseFile):
``refpot``
object POFile, the reference catalog.
"""
+ # Store entries in dict/set for faster access
+ self_entries = dict((entry.msgid, entry) for entry in self)
+ refpot_msgids = set(entry.msgid for entry in refpot)
+ # Merge entries that are in the refpot
for entry in refpot:
- e = self.find(entry.msgid, include_obsolete_entries=True)
+ e = self_entries.get(entry.msgid)
if e is None:
e = POEntry()
self.append(e)
e.merge(entry)
# ok, now we must "obsolete" entries that are not in the refpot anymore
for entry in self:
- if refpot.find(entry.msgid) is None:
+ if entry.msgid not in refpot_msgids:
entry.obsolete = True
-
# }}}
# class MOFile {{{
+
class MOFile(_BaseFile):
"""
Mo file reader/writer.
This class inherits the :class:`~polib._BaseFile` class and, by
extension, the python ``list`` type.
"""
- BIG_ENDIAN = 0xde120495
+ BIG_ENDIAN = 0xde120495
LITTLE_ENDIAN = 0x950412de
def __init__(self, *args, **kwargs):
"""
- Constructor, accepts all keywords arguments accepted by
+ Constructor, accepts all keywords arguments accepted by
:class:`~polib._BaseFile` class.
"""
_BaseFile.__init__(self, *args, **kwargs)
@@ -720,10 +751,10 @@ class MOFile(_BaseFile):
Convenience method to keep the same interface with POFile instances.
"""
return []
-
# }}}
# class _BaseEntry {{{
+
class _BaseEntry(object):
"""
Base class for :class:`~polib.POEntry` and :class:`~polib.MOEntry` classes.
@@ -775,12 +806,14 @@ class _BaseEntry(object):
ret = []
# write the msgctxt if any
if self.msgctxt is not None:
- ret += self._str_field("msgctxt", delflag, "", self.msgctxt, wrapwidth)
+ ret += self._str_field("msgctxt", delflag, "", self.msgctxt,
+ wrapwidth)
# write the msgid
ret += self._str_field("msgid", delflag, "", self.msgid, wrapwidth)
# write the msgid_plural if any
if self.msgid_plural:
- ret += self._str_field("msgid_plural", delflag, "", self.msgid_plural, wrapwidth)
+ ret += self._str_field("msgid_plural", delflag, "",
+ self.msgid_plural, wrapwidth)
if self.msgstr_plural:
# write the msgstr_plural if any
msgstrs = self.msgstr_plural
@@ -789,10 +822,12 @@ class _BaseEntry(object):
for index in keys:
msgstr = msgstrs[index]
plural_index = '[%s]' % index
- ret += self._str_field("msgstr", delflag, plural_index, msgstr, wrapwidth)
+ ret += self._str_field("msgstr", delflag, plural_index, msgstr,
+ wrapwidth)
else:
# otherwise write the msgstr
- ret += self._str_field("msgstr", delflag, "", self.msgstr, wrapwidth)
+ ret += self._str_field("msgstr", delflag, "", self.msgstr,
+ wrapwidth)
ret.append('')
ret = u('\n').join(ret)
return ret
@@ -806,20 +841,21 @@ class _BaseEntry(object):
Returns the string representation of the entry.
"""
return unicode(self).encode(self.encoding)
-
+
def __eq__(self, other):
return str(self) == str(other)
- def _str_field(self, fieldname, delflag, plural_index, field, wrapwidth=78):
+ def _str_field(self, fieldname, delflag, plural_index, field,
+ wrapwidth=78):
lines = field.splitlines(True)
if len(lines) > 1:
- lines = [''] + lines # start with initial empty line
+ lines = [''] + lines # start with initial empty line
else:
escaped_field = escape(field)
specialchars_count = 0
for c in ['\\', '\n', '\r', '\t', '"']:
specialchars_count += field.count(c)
- # comparison must take into account fieldname length + one space
+ # comparison must take into account fieldname length + one space
# + 2 quotes (eg. msgid "")
flength = len(fieldname) + 3
if plural_index:
@@ -829,7 +865,7 @@ class _BaseEntry(object):
# Wrap the line but take field name into account
lines = [''] + [unescape(item) for item in wrap(
escaped_field,
- wrapwidth - 2, # 2 for quotes ""
+ wrapwidth - 2, # 2 for quotes ""
drop_whitespace=False,
break_long_words=False
)]
@@ -845,10 +881,10 @@ class _BaseEntry(object):
#import pdb; pdb.set_trace()
ret.append('%s"%s"' % (delflag, escape(mstr)))
return ret
-
# }}}
# class POEntry {{{
+
class POEntry(_BaseEntry):
"""
Represents a po file entry.
@@ -923,9 +959,9 @@ class POEntry(_BaseEntry):
filelist.append(fpath)
filestr = ' '.join(filelist)
if wrapwidth > 0 and len(filestr) + 3 > wrapwidth:
- # textwrap split words that contain hyphen, this is not
- # what we want for filenames, so the dirty hack is to
- # temporally replace hyphens with a char that a file cannot
+ # textwrap split words that contain hyphen, this is not
+ # what we want for filenames, so the dirty hack is to
+ # temporally replace hyphens with a char that a file cannot
# contain, like "*"
ret += [l.replace('*', '-') for l in wrap(
filestr.replace('-', '*'),
@@ -942,7 +978,8 @@ class POEntry(_BaseEntry):
ret.append('#, %s' % ', '.join(self.flags))
# previous context and previous msgid/msgid_plural
- fields = ['previous_msgctxt', 'previous_msgid', 'previous_msgid_plural']
+ fields = ['previous_msgctxt', 'previous_msgid',
+ 'previous_msgid_plural']
for f in fields:
val = getattr(self, f)
if val:
@@ -988,8 +1025,10 @@ class POEntry(_BaseEntry):
else:
return -1
# Finally: Compare message ID
- if self.msgid > other.msgid: return 1
- elif self.msgid < other.msgid: return -1
+ if self.msgid > other.msgid:
+ return 1
+ elif self.msgid < other.msgid:
+ return -1
return 0
def __gt__(self, other):
@@ -1050,19 +1089,19 @@ class POEntry(_BaseEntry):
self.msgstr_plural[pos]
except KeyError:
self.msgstr_plural[pos] = ''
-
# }}}
# class MOEntry {{{
+
class MOEntry(_BaseEntry):
"""
Represents a mo file entry.
"""
pass
-
# }}}
# class _POFileParser {{{
+
class _POFileParser(object):
"""
A finite state machine to parse efficiently and correctly po
@@ -1096,7 +1135,10 @@ class _POFileParser(object):
else:
self.fhandle = pofile.splitlines()
- self.instance = POFile(
+ klass = kwargs.get('klass')
+ if klass is None:
+ klass = POFile
+ self.instance = klass(
pofile=pofile,
encoding=enc,
check_for_duplicates=kwargs.get('check_for_duplicates', False)
@@ -1139,7 +1181,7 @@ class _POFileParser(object):
self.add('PP', all, 'PP')
self.add('CT', ['ST', 'HE', 'GC', 'OC', 'FL', 'TC', 'PC', 'PM',
'PP', 'MS', 'MX'], 'CT')
- self.add('MI', ['ST', 'HE', 'GC', 'OC', 'FL', 'CT', 'TC', 'PC',
+ self.add('MI', ['ST', 'HE', 'GC', 'OC', 'FL', 'CT', 'TC', 'PC',
'PM', 'PP', 'MS', 'MX'], 'MI')
self.add('MP', ['TC', 'GC', 'PC', 'PM', 'PP', 'MI'], 'MP')
self.add('MS', ['MI', 'MP', 'TC'], 'MS')
@@ -1174,6 +1216,9 @@ class _POFileParser(object):
tokens = line.split(None, 2)
nb_tokens = len(tokens)
+ if tokens[0] == '#~|':
+ continue
+
if tokens[0] == '#~' and nb_tokens > 1:
line = line[3:].strip()
tokens = tokens[1:]
@@ -1186,41 +1231,56 @@ class _POFileParser(object):
# msgid, msgid_plural, msgctxt & msgstr.
if tokens[0] in keywords and nb_tokens > 1:
line = line[len(tokens[0]):].lstrip()
+ if re.search(r'([^\\]|^)"', line[1:-1]):
+ raise IOError('Syntax error in po file %s (line %s): '
+ 'unescaped double quote found' %
+ (self.instance.fpath, i))
self.current_token = line
self.process(keywords[tokens[0]], i)
continue
self.current_token = line
- if tokens[0] == '#:' and nb_tokens > 1:
+ if tokens[0] == '#:':
+ if nb_tokens <= 1:
+ continue
# we are on a occurrences line
self.process('OC', i)
elif line[:1] == '"':
# we are on a continuation line
+ if re.search(r'([^\\]|^)"', line[1:-1]):
+ raise IOError('Syntax error in po file %s (line %s): '
+ 'unescaped double quote found' %
+ (self.instance.fpath, i))
self.process('MC', i)
elif line[:7] == 'msgstr[':
# we are on a msgstr plural
self.process('MX', i)
- elif tokens[0] == '#,' and nb_tokens > 1:
+ elif tokens[0] == '#,':
+ if nb_tokens <= 1:
+ continue
# we are on a flags line
self.process('FL', i)
- elif tokens[0] == '#':
- if line == '#': line += ' '
+ elif tokens[0] == '#' or tokens[0].startswith('##'):
+ if line == '#':
+ line += ' '
# we are on a translator comment line
self.process('TC', i)
- elif tokens[0] == '#.' and nb_tokens > 1:
+ elif tokens[0] == '#.':
+ if nb_tokens <= 1:
+ continue
# we are on a generated comment line
self.process('GC', i)
elif tokens[0] == '#|':
- if nb_tokens < 2:
- self.process('??', i)
- continue
+ if nb_tokens <= 1:
+ raise IOError('Syntax error in po file %s (line %s)' %
+ (self.instance.fpath, i))
# Remove the marker and any whitespace right after that.
line = line[2:].lstrip()
@@ -1233,12 +1293,16 @@ class _POFileParser(object):
if nb_tokens == 2:
# Invalid continuation line.
- self.process('??', i)
+ raise IOError('Syntax error in po file %s (line %s): '
+ 'invalid continuation line' %
+ (self.instance.fpath, i))
# we are on a "previous translation" comment line,
if tokens[1] not in prev_keywords:
# Unknown keyword in previous translation comment.
- self.process('??', i)
+ raise IOError('Syntax error in po file %s (line %s): '
+ 'unknown keyword %s' %
+ (self.instance.fpath, i, tokens[1]))
# Remove the keyword and any whitespace
# between it and the starting quote.
@@ -1247,27 +1311,28 @@ class _POFileParser(object):
self.process(prev_keywords[tokens[1]], i)
else:
- self.process('??', i)
+ raise IOError('Syntax error in po file %s (line %s)' %
+ (self.instance.fpath, i))
if self.current_entry:
# since entries are added when another entry is found, we must add
# the last entry here (only if there are lines)
self.instance.append(self.current_entry)
- # before returning the instance, check if there's metadata and if
+ # before returning the instance, check if there's metadata and if
# so extract it in a dict
- firstentry = self.instance[0]
- if firstentry.msgid == '': # metadata found
+ metadataentry = self.instance.find('')
+ if metadataentry: # metadata found
# remove the entry
- firstentry = self.instance.pop(0)
- self.instance.metadata_is_fuzzy = firstentry.flags
+ self.instance.remove(metadataentry)
+ self.instance.metadata_is_fuzzy = metadataentry.flags
key = None
- for msg in firstentry.msgstr.splitlines():
+ for msg in metadataentry.msgstr.splitlines():
try:
key, val = msg.split(':', 1)
self.instance.metadata[key] = val.strip()
- except:
+ except (ValueError, KeyError):
if key is not None:
- self.instance.metadata[key] += '\n'+ msg.strip()
+ self.instance.metadata[key] += '\n' + msg.strip()
# close opened file
if not isinstance(self.fhandle, list): # must be file
self.fhandle.close()
@@ -1310,7 +1375,7 @@ class _POFileParser(object):
if action():
self.current_state = state
except Exception:
- raise IOError('Syntax error in po file: %s (line %s)' % (self.instance.fpath, linenum))
+ raise IOError('Syntax error in po file (line %s)' % linenum)
# state handlers
@@ -1328,7 +1393,10 @@ class _POFileParser(object):
self.current_entry = POEntry()
if self.current_entry.tcomment != '':
self.current_entry.tcomment += '\n'
- self.current_entry.tcomment += self.current_token[2:]
+ tcomment = self.current_token.lstrip('#')
+ if tcomment.startswith(' '):
+ tcomment = tcomment[1:]
+ self.current_entry.tcomment += tcomment
return True
def handle_gc(self):
@@ -1352,10 +1420,10 @@ class _POFileParser(object):
try:
fil, line = occurrence.split(':')
if not line.isdigit():
- fil = fil + line
+ fil = fil + line
line = ''
self.current_entry.occurrences.append((fil, line))
- except:
+ except (ValueError, AttributeError):
self.current_entry.occurrences.append((occurrence, ''))
return True
@@ -1432,38 +1500,30 @@ class _POFileParser(object):
"""Handle a msgid or msgstr continuation line."""
token = unescape(self.current_token[1:-1])
if self.current_state == 'CT':
- typ = 'msgctxt'
self.current_entry.msgctxt += token
elif self.current_state == 'MI':
- typ = 'msgid'
self.current_entry.msgid += token
elif self.current_state == 'MP':
- typ = 'msgid_plural'
self.current_entry.msgid_plural += token
elif self.current_state == 'MS':
- typ = 'msgstr'
self.current_entry.msgstr += token
elif self.current_state == 'MX':
- typ = 'msgstr[%s]' % self.msgstr_index
self.current_entry.msgstr_plural[self.msgstr_index] += token
elif self.current_state == 'PP':
- typ = 'previous_msgid_plural'
token = token[3:]
self.current_entry.previous_msgid_plural += token
elif self.current_state == 'PM':
- typ = 'previous_msgid'
token = token[3:]
self.current_entry.previous_msgid += token
elif self.current_state == 'PC':
- typ = 'previous_msgctxt'
token = token[3:]
self.current_entry.previous_msgctxt += token
# don't change the current state
return False
-
# }}}
# class _MOFileParser {{{
+
class _MOFileParser(object):
"""
A class to parse binary mo files.
@@ -1487,7 +1547,11 @@ class _MOFileParser(object):
file (optional, default: ``False``).
"""
self.fhandle = open(mofile, 'rb')
- self.instance = MOFile(
+
+ klass = kwargs.get('klass')
+ if klass is None:
+ klass = MOFile
+ self.instance = klass(
fpath=mofile,
encoding=kwargs.get('encoding', default_encoding),
check_for_duplicates=kwargs.get('check_for_duplicates', False)
@@ -1529,7 +1593,7 @@ class _MOFileParser(object):
self.fhandle.seek(msgstrs_index[i][1])
msgstr = self.fhandle.read(msgstrs_index[i][0])
- if i == 0: # metadata
+ if i == 0: # metadata
raw_metadata, metadata = msgstr.split(b('\n')), {}
for line in raw_metadata:
tokens = line.split(b(':'), 1)
@@ -1548,7 +1612,8 @@ class _MOFileParser(object):
entry = self._build_entry(
msgid=msgid_tokens[0],
msgid_plural=msgid_tokens[1],
- msgstr_plural=dict((k,v) for k,v in enumerate(msgstr.split(b('\0'))))
+ msgstr_plural=dict((k, v) for k, v in
+ enumerate(msgstr.split(b('\0'))))
)
else:
entry = self._build_entry(msgid=msgid, msgstr=msgstr)
@@ -1564,7 +1629,7 @@ class _MOFileParser(object):
if len(msgctxt_msgid) > 1:
kwargs = {
'msgctxt': msgctxt_msgid[0].decode(encoding),
- 'msgid' : msgctxt_msgid[1].decode(encoding),
+ 'msgid': msgctxt_msgid[1].decode(encoding),
}
else:
kwargs = {'msgid': msgid.decode(encoding)}
@@ -1588,17 +1653,17 @@ class _MOFileParser(object):
if len(tup) == 1:
return tup[0]
return tup
-
# }}}
# class TextWrapper {{{
+
class TextWrapper(textwrap.TextWrapper):
"""
Subclass of textwrap.TextWrapper that backport the
drop_whitespace option.
"""
def __init__(self, *args, **kwargs):
- drop_whitespace = kwargs.pop('drop_whitespace', True)
+ drop_whitespace = kwargs.pop('drop_whitespace', True)
textwrap.TextWrapper.__init__(self, *args, **kwargs)
self.drop_whitespace = drop_whitespace
@@ -1662,7 +1727,7 @@ class TextWrapper(textwrap.TextWrapper):
self._handle_long_word(chunks, cur_line, cur_len, width)
# If the last chunk on this line is all whitespace, drop it.
- if self.drop_whitespace and cur_line and cur_line[-1].strip() == '':
+ if self.drop_whitespace and cur_line and not cur_line[-1].strip():
del cur_line[-1]
# Convert current line back to a string and store it in list
@@ -1671,10 +1736,10 @@ class TextWrapper(textwrap.TextWrapper):
lines.append(indent + ''.join(cur_line))
return lines
-
# }}}
# function wrap() {{{
+
def wrap(text, width=70, **kwargs):
"""
Wrap a single paragraph of text, returning a list of wrapped lines.
@@ -1683,4 +1748,4 @@ def wrap(text, width=70, **kwargs):
return TextWrapper(width=width, **kwargs).wrap(text)
return textwrap.wrap(text, width=width, **kwargs)
-#}}}
+# }}}
diff --git a/rosetta/poutil.py b/rosetta/poutil.py
index 5e72a0b..f22c6e9 100644
--- a/rosetta/poutil.py
+++ b/rosetta/poutil.py
@@ -1,9 +1,10 @@
-import os
-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
+from django.conf import settings
+from django.core.cache import cache
+from rosetta.conf import settings as rosetta_settings
+import django
+import os
+import six
try:
from django.utils import timezone
except:
@@ -46,7 +47,10 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False)
project = __import__(parts[0], {}, {}, [])
abs_project_path = os.path.normpath(os.path.abspath(os.path.dirname(project.__file__)))
if project_apps:
- 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')))
# django/locale
if django_apps:
@@ -70,12 +74,11 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False)
continue
p = appname.rfind('.')
if p >= 0:
- app = getattr(__import__(appname[:p], {}, {}, [appname[p + 1:]]), appname[p + 1:])
+ app = getattr(__import__(appname[:p], {}, {}, [str(appname[p + 1:])]), appname[p + 1:])
else:
app = __import__(appname, {}, {}, [])
apppath = os.path.normpath(os.path.abspath(os.path.join(os.path.dirname(app.__file__), 'locale')))
-
# django apps
if 'contrib' in apppath and 'django' in apppath and not django_apps:
@@ -84,52 +87,57 @@ def find_pos(lang, project_apps=True, django_apps=False, third_party_apps=False)
# third party external
if not third_party_apps and abs_project_path not in apppath:
continue
-
+
# local apps
if not project_apps and abs_project_path in apppath:
continue
-
-
+
if os.path.isdir(apppath):
paths.append(apppath)
-
-
-
-
+
ret = set()
langs = (lang,)
if u'-' in lang:
- _l,_c = map(lambda x:x.lower(),lang.split(u'-'))
- langs += (u'%s_%s' %(_l, _c), u'%s_%s' %(_l, _c.upper()), )
+ _l, _c = map(lambda x: x.lower(), lang.split(u'-'))
+ langs += (u'%s_%s' % (_l, _c), u'%s_%s' % (_l, _c.upper()), )
elif u'_' in lang:
- _l,_c = map(lambda x:x.lower(),lang.split(u'_'))
- langs += (u'%s-%s' %(_l, _c), u'%s-%s' %(_l, _c.upper()), )
-
+ _l, _c = map(lambda x: x.lower(), lang.split(u'_'))
+ langs += (u'%s-%s' % (_l, _c), u'%s-%s' % (_l, _c.upper()), )
+
paths = map(os.path.normpath, paths)
+ paths = list(set(paths))
for path in paths:
for lang_ in langs:
dirname = os.path.join(path, lang_, 'LC_MESSAGES')
- for fn in ('django.po','djangojs.po',):
+ for fn in ('django.po', 'djangojs.po',):
filename = os.path.join(dirname, fn)
if os.path.isfile(filename):
ret.add(os.path.abspath(filename))
return list(ret)
-def pagination_range(first,last,current):
+
+def pagination_range(first, last, current):
r = []
-
+
r.append(first)
- if first + 1 < last: r.append(first+1)
-
- if current -2 > first and current -2 < last: r.append(current-2)
- if current -1 > first and current -1 < last: r.append(current-1)
- if current > first and current < last: r.append(current)
- if current + 1 < last and current+1 > first: r.append(current+1)
- if current + 2 < last and current+2 > first: r.append(current+2)
-
- if last-1 > first: r.append(last-1)
+ if first + 1 < last:
+ r.append(first + 1)
+
+ if current - 2 > first and current - 2 < last:
+ r.append(current - 2)
+ if current - 1 > first and current - 1 < last:
+ r.append(current - 1)
+ if current > first and current < last:
+ r.append(current)
+ if current + 1 < last and current + 1 > first:
+ r.append(current + 1)
+ if current + 2 < last and current + 2 > first:
+ r.append(current + 2)
+
+ if last - 1 > first:
+ r.append(last - 1)
r.append(last)
-
+
r = list(set(r))
r.sort()
prev = 10000
diff --git a/rosetta/storage.py b/rosetta/storage.py
index 75e6dd0..2faf605 100644
--- a/rosetta/storage.py
+++ b/rosetta/storage.py
@@ -4,6 +4,7 @@ from django.utils import importlib
from django.core.exceptions import ImproperlyConfigured
import hashlib
import time
+import six
class BaseRosettaStorage(object):
@@ -62,7 +63,7 @@ class CacheRosettaStorage(BaseRosettaStorage):
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._key_prefix = hashlib.new('sha1', six.text_type(time.time()).encode('utf8')).hexdigest()
self.request.session['rosetta_cache_storage_key_prefix'] = self._key_prefix
if self.request.session['rosetta_cache_storage_key_prefix'] != self._key_prefix:
@@ -86,7 +87,7 @@ class CacheRosettaStorage(BaseRosettaStorage):
def set(self, key, val):
#print ('set', self._key_prefix + key)
- cache.set(self._key_prefix + key, val)
+ cache.set(self._key_prefix + key, val, 86400)
def has(self, key):
#print ('has', self._key_prefix + key)
diff --git a/rosetta/templates/rosetta/base.html b/rosetta/templates/rosetta/base.html
index e3949df..79a5085 100644
--- a/rosetta/templates/rosetta/base.html
+++ b/rosetta/templates/rosetta/base.html
@@ -14,7 +14,7 @@
diff --git a/rosetta/templates/rosetta/js/rosetta.js b/rosetta/templates/rosetta/js/rosetta.js
index 4399177..a8ed143 100644
--- a/rosetta/templates/rosetta/js/rosetta.js
+++ b/rosetta/templates/rosetta/js/rosetta.js
@@ -7,16 +7,16 @@ google.setOnLoadCallback(function() {
$('.hide', $(this).parent()).hide();
});
-{% if ENABLE_TRANSLATION_SUGGESTIONS and BING_APP_ID %}
+{% if rosetta_settings.ENABLE_TRANSLATION_SUGGESTIONS and rosetta_settings.BING_APP_ID %}
$('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 sourceLang = '{{ MESSAGES_SOURCE_LANGUAGE_CODE }}';
+ var sourceLang = '{{ rosetta_settings.MESSAGES_SOURCE_LANGUAGE_CODE }}';
var destLang = '{{ rosetta_i18n_lang_code }}';
- var app_id = '{{ BING_APP_ID }}';
+ var app_id = '{{ rosetta_settings.BING_APP_ID }}';
var apiUrl = "http://api.microsofttranslator.com/V2/Ajax.svc/Translate";
orig = unescape(orig).replace(/ /g,'\n').replace(//,'').replace(/<\/code>/g,'').replace(/>/g,'>').replace(/</g,'<');
@@ -50,7 +50,7 @@ google.setOnLoadCallback(function() {
$($('.part',td).get(j)).css('top',textareaY + 'px');
});
});
-
+
$('.translation textarea').blur(function() {
if($(this).val()) {
$('.alert', $(this).parents('tr')).remove();
@@ -70,7 +70,7 @@ google.setOnLoadCallback(function() {
} else {
if (!(origs === null && trads === null)) {
$(this).before(error);
- return false;
+ return false;
}
}
return true;
@@ -78,5 +78,5 @@ google.setOnLoadCallback(function() {
});
$('.translation textarea').eq(0).focus();
-
+
});
diff --git a/rosetta/templates/rosetta/pofile.html b/rosetta/templates/rosetta/pofile.html
index 63353fa..681da53 100644
--- a/rosetta/templates/rosetta/pofile.html
+++ b/rosetta/templates/rosetta/pofile.html
@@ -15,7 +15,7 @@
{% endblock %}
-{% block pagetitle %}{{block.super}} - {{MESSAGES_SOURCE_LANGUAGE_NAME}} - {{rosetta_i18n_lang_name}} ({{ rosetta_i18n_pofile.percent_translated|floatformat:0 }}%){% endblock %}
+{% block pagetitle %}{{block.super}} - {{rosetta_settings.MESSAGES_SOURCE_LANGUAGE_NAME}} - {{rosetta_i18n_lang_name}} ({{ rosetta_i18n_pofile.percent_translated|floatformat:0 }}%){% endblock %}
{% block breadcumbs %}