2 # -*- coding: utf-8 -*-
4 # Copyright 2006-2008 Zuza Software Foundation
6 # This file is part of translate.
8 # translate is free software; you can redistribute it and/or modify
9 # it under the terms of the GNU General Public License as published by
10 # the Free Software Foundation; either version 2 of the License, or
11 # (at your option) any later version.
13 # translate is distributed in the hope that it will be useful,
14 # but WITHOUT ANY WARRANTY; without even the implied warranty of
15 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 # GNU General Public License for more details.
18 # You should have received a copy of the GNU General Public License
19 # along with translate; if not, write to the Free Software
20 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
22 """Base classes for storage interfaces.
24 @organization: Zuza Software Foundation
25 @copyright: 2006-2007 Zuza Software Foundation
26 @license: U{GPL <http://www.fsf.org/licensing/licenses/gpl.html>}
30 import cPickle
as pickle
33 from exceptions
import NotImplementedError
35 def force_override(method
, baseclass
):
36 """Forces derived classes to override method."""
38 if type(method
.im_self
) == type(baseclass
):
39 # then this is a classmethod and im_self is the actual class
40 actualclass
= method
.im_self
42 actualclass
= method
.im_class
43 if actualclass
!= baseclass
:
44 raise NotImplementedError("%s does not reimplement %s as required by %s" % (actualclass
.__name
__, method
.__name
__, baseclass
.__name
__))
46 class ParseError(Exception):
49 class TranslationUnit(object):
50 """Base class for translation units.
52 Our concept of a I{translation unit} is influenced heavily by XLIFF:
53 U{http://www.oasis-open.org/committees/xliff/documents/xliff-specification.htm}
55 As such most of the method- and variable names borrows from XLIFF terminology.
57 A translation unit consists of the following:
58 - A I{source} string. This is the original translatable text.
59 - A I{target} string. This is the translation of the I{source}.
60 - Zero or more I{notes} on the unit. Notes would typically be some
61 comments from a translator on the unit, or some comments originating from
63 - Zero or more I{locations}. Locations indicate where in the original
64 source code this unit came from.
65 - Zero or more I{errors}. Some tools (eg. L{pofilter <filters.pofilter>}) can run checks on
66 translations and produce error messages.
68 @group Source: *source*
69 @group Target: *target*
71 @group Locations: *location*
72 @group Errors: *error*
75 def __init__(self
, source
):
76 """Constructs a TranslationUnit containing the given source string."""
81 super(TranslationUnit
, self
).__init
__()
83 def __eq__(self
, other
):
84 """Compares two TranslationUnits.
86 @type other: L{TranslationUnit}
87 @param other: Another L{TranslationUnit}
89 @return: Returns True if the supplied TranslationUnit equals this unit.
93 return self
.source
== other
.source
and self
.target
== other
.target
95 def settarget(self
, target
):
96 """Sets the target string to the given value."""
100 def gettargetlen(self
):
101 """Returns the length of the target string.
103 @note: Plural forms might be combined.
108 length
= len(self
.target
or "")
109 strings
= getattr(self
.target
, "strings", [])
111 length
+= sum([len(pluralform
) for pluralform
in strings
[1:]])
115 """A unique identifier for this unit.
118 @return: an identifier for this unit that is unique in the store
120 Derived classes should override this in a way that guarantees a unique
121 identifier for each unit in the store.
125 def getlocations(self
):
126 """A list of source code locations.
128 @note: Shouldn't be implemented if the format doesn't support it.
135 def addlocation(self
, location
):
136 """Add one location to the list of locations.
138 @note: Shouldn't be implemented if the format doesn't support it.
143 def addlocations(self
, location
):
144 """Add a location or a list of locations.
146 @note: Most classes shouldn't need to implement this,
147 but should rather implement L{addlocation()}.
148 @warning: This method might be removed in future.
152 if isinstance(location
, list):
153 for item
in location
:
154 self
.addlocation(item
)
156 self
.addlocation(location
)
158 def getcontext(self
):
159 """Get the message context."""
162 def getnotes(self
, origin
=None):
163 """Returns all notes about this unit.
165 It will probably be freeform text or something reasonable that can be
166 synthesised by the format.
167 It should not include location comments (see L{getlocations()}).
170 return getattr(self
, "notes", "")
172 def addnote(self
, text
, origin
=None):
173 """Adds a note (comment).
176 @param text: Usually just a sentence or two.
178 @param origin: Specifies who/where the comment comes from.
179 Origin can be one of the following text strings:
181 - 'developer', 'programmer', 'source code' (synonyms)
184 if getattr(self
, "notes", None):
185 self
.notes
+= '\n'+text
189 def removenotes(self
):
190 """Remove all the translator's notes."""
194 def adderror(self
, errorname
, errortext
):
195 """Adds an error message to this unit.
197 @type errorname: string
198 @param errorname: A single word to id the error.
199 @type errortext: string
200 @param errortext: The text describing the error.
207 """Get all error messages.
215 def markreviewneeded(self
, needsreview
=True, explanation
=None):
216 """Marks the unit to indicate whether it needs review.
218 @keyword needsreview: Defaults to True.
219 @keyword explanation: Adds an optional explanation as a note.
225 def istranslated(self
):
226 """Indicates whether this unit is translated.
228 This should be used rather than deducing it from .target,
229 to ensure that other classes can implement more functionality
234 return bool(self
.target
) and not self
.isfuzzy()
236 def istranslatable(self
):
237 """Indicates whether this unit can be translated.
239 This should be used to distinguish real units for translation from
240 header, obsolete, binary or other blank units.
245 """Indicates whether this unit is fuzzy."""
249 def markfuzzy(self
, value
=True):
250 """Marks the unit as fuzzy or not."""
254 """Indicates whether this unit is a header."""
259 """Indicates whether this unit needs review."""
264 """Used to see if this unit has no source or target string.
266 @note: This is probably used more to find translatable units,
267 and we might want to move in that direction rather and get rid of this.
271 return not (self
.source
or self
.target
)
274 """Tells whether or not this specific unit has plural strings."""
279 def merge(self
, otherunit
, overwrite
=False, comments
=True):
280 """Do basic format agnostic merging."""
282 if self
.target
== "" or overwrite
:
283 self
.target
= otherunit
.target
286 """Iterator that only returns this unit."""
290 """This unit in a list."""
293 def buildfromunit(cls
, unit
):
294 """Build a native unit from a foreign unit, preserving as much
295 information as possible."""
297 if type(unit
) == cls
and hasattr(unit
, "copy") and callable(unit
.copy
):
299 newunit
= cls(unit
.source
)
300 newunit
.target
= unit
.target
301 newunit
.markfuzzy(unit
.isfuzzy())
302 locations
= unit
.getlocations()
304 newunit
.addlocations(locations
)
305 notes
= unit
.getnotes()
307 newunit
.addnote(notes
)
309 buildfromunit
= classmethod(buildfromunit
)
311 class TranslationStore(object):
312 """Base class for stores for multiple translation units of type UnitClass."""
314 UnitClass
= TranslationUnit
316 def __init__(self
, unitclass
=None):
317 """Constructs a blank TranslationStore."""
324 self
.UnitClass
= unitclass
325 super(TranslationStore
, self
).__init
__()
328 """Iterator over all the units in this store."""
329 for unit
in self
.units
:
333 """Return a list of all units in this store."""
334 return [unit
for unit
in self
.unit_iter()]
336 def addunit(self
, unit
):
337 """Appends the given unit to the object's list of units.
339 This method should always be used rather than trying to modify the
342 @type unit: L{TranslationUnit}
343 @param unit: The unit that will be added.
347 self
.units
.append(unit
)
349 def addsourceunit(self
, source
):
350 """Adds and returns a new unit with the given source string.
352 @rtype: L{TranslationUnit}
356 unit
= self
.UnitClass(source
)
360 def findunit(self
, source
):
361 """Finds the unit with the given source string.
363 @rtype: L{TranslationUnit} or None
367 if len(getattr(self
, "sourceindex", [])):
368 if source
in self
.sourceindex
:
369 return self
.sourceindex
[source
]
371 for unit
in self
.units
:
372 if unit
.source
== source
:
376 def translate(self
, source
):
377 """Returns the translated string for a given source string.
379 @rtype: String or None
383 unit
= self
.findunit(source
)
384 if unit
and unit
.target
:
390 """Indexes the items in this store. At least .sourceindex should be usefull."""
392 self
.locationindex
= {}
393 self
.sourceindex
= {}
394 for unit
in self
.units
:
395 # Do we need to test if unit.source exists?
396 self
.sourceindex
[unit
.source
] = unit
398 for nounform
in unit
.source
.strings
[1:]:
399 self
.sourceindex
[nounform
] = unit
400 for location
in unit
.getlocations():
401 if location
in self
.locationindex
:
402 # if sources aren't unique, don't use them
403 self
.locationindex
[location
] = None
405 self
.locationindex
[location
] = unit
408 """Converts to a string representation that can be parsed back using L{parsestring()}."""
410 # We can't pickle fileobj if it is there, so let's hide it for a while.
411 fileobj
= getattr(self
, "fileobj", None)
413 dump
= pickle
.dumps(self
)
414 self
.fileobj
= fileobj
418 """Returns True if the object doesn't contain any translation units."""
420 if len(self
.units
) == 0:
422 for unit
in self
.units
:
423 if not (unit
.isblank() or unit
.isheader()):
427 def _assignname(self
):
428 """Tries to work out what the name of the filesystem file is and
429 assigns it to .filename."""
430 fileobj
= getattr(self
, "fileobj", None)
432 filename
= getattr(fileobj
, "name", getattr(fileobj
, "filename", None))
434 self
.filename
= filename
436 def parsestring(cls
, storestring
):
437 """Converts the string representation back to an object."""
440 newstore
.parse(storestring
)
442 parsestring
= classmethod(parsestring
)
444 def parse(self
, data
):
445 """parser to process the given source string"""
446 self
.units
= pickle
.loads(data
).units
448 def savefile(self
, storefile
):
449 """Writes the string representation to the given file (or filename)."""
450 if isinstance(storefile
, basestring
):
451 storefile
= open(storefile
, "w")
452 self
.fileobj
= storefile
454 storestring
= str(self
)
455 storefile
.write(storestring
)
459 """Save to the file that data was originally read from, if available."""
460 fileobj
= getattr(self
, "fileobj", None)
462 filename
= getattr(self
, "filename", None)
464 fileobj
= file(filename
, "w")
467 filename
= getattr(fileobj
, "name", getattr(fileobj
, "filename", None))
469 raise ValueError("No file or filename to save to")
470 fileobj
= fileobj
.__class
__(filename
, "w")
471 self
.savefile(fileobj
)
473 def parsefile(cls
, storefile
):
474 """Reads the given file (or opens the given filename) and parses back to an object."""
476 if isinstance(storefile
, basestring
):
477 storefile
= open(storefile
, "r")
478 mode
= getattr(storefile
, "mode", "r")
479 #For some reason GzipFile returns 1, so we have to test for that here
480 if mode
== 1 or "r" in mode
:
481 storestring
= storefile
.read()
485 newstore
= cls
.parsestring(storestring
)
486 newstore
.fileobj
= storefile
487 newstore
._assignname
()
489 parsefile
= classmethod(parsefile
)