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 %}
@@ -33,10 +33,10 @@ {% if ENABLE_REFLANG %} @@ -50,7 +50,7 @@