1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """Base classes for storage interfaces.
22
23 @organization: Zuza Software Foundation
24 @copyright: 2006-2009 Zuza Software Foundation
25 @license: U{GPL <http://www.fsf.org/licensing/licenses/gpl.html>}
26 """
27
28 try:
29 import cPickle as pickle
30 except ImportError:
31 import pickle
32 from exceptions import NotImplementedError
33 import translate.i18n
34 from translate.storage.placeables import StringElem, general, parse as rich_parse
35 from translate.storage.workflow import StateEnum as states
36 from translate.misc.typecheck import accepts, Self, IsOneOf
37 from translate.misc.multistring import multistring
38
39
41 """Forces derived classes to override method."""
42
43 if type(method.im_self) == type(baseclass):
44
45 actualclass = method.im_self
46 else:
47 actualclass = method.im_class
48 if actualclass != baseclass:
49 raise NotImplementedError(
50 "%s does not reimplement %s as required by %s" % \
51 (actualclass.__name__, method.__name__, baseclass.__name__))
52
53
55
57 self.inner_exc = inner_exc
58
60 return repr(self.inner_exc)
61
62
64 """Base class for translation units.
65
66 Our concept of a I{translation unit} is influenced heavily by XLIFF:
67 U{http://www.oasis-open.org/committees/xliff/documents/xliff-specification.htm}
68
69 As such most of the method- and variable names borrows from XLIFF
70 terminology.
71
72 A translation unit consists of the following:
73 - A I{source} string. This is the original translatable text.
74 - A I{target} string. This is the translation of the I{source}.
75 - Zero or more I{notes} on the unit. Notes would typically be some
76 comments from a translator on the unit, or some comments originating
77 from the source code.
78 - Zero or more I{locations}. Locations indicate where in the original
79 source code this unit came from.
80 - Zero or more I{errors}. Some tools (eg. L{pofilter<filters.pofilter>})
81 can run checks on translations and produce error messages.
82
83 @group Source: *source*
84 @group Target: *target*
85 @group Notes: *note*
86 @group Locations: *location*
87 @group Errors: *error*
88 """
89
90 rich_parsers = []
91 """A list of functions to use for parsing a string into a rich string
92 tree."""
93
94
95 S_OBSOLETE = states.OBSOLETE
96 S_EMPTY = states.EMPTY
97 S_NEEDS_WORK = states.NEEDS_WORK
98 S_REJECTED = states.REJECTED
99 S_NEEDS_REVIEW = states.NEEDS_REVIEW
100 S_UNREVIEWED = states.UNREVIEWED
101 S_FINAL = states.FINAL
102
103 STATE = {
104 S_OBSOLETE: (states.OBSOLETE, states.EMPTY),
105 S_EMPTY: (states.EMPTY, states.NEEDS_WORK),
106 S_NEEDS_WORK: (states.NEEDS_WORK, states.REJECTED),
107 S_REJECTED: (states.REJECTED, states.NEEDS_REVIEW),
108 S_NEEDS_REVIEW: (states.NEEDS_REVIEW, states.UNREVIEWED),
109 S_UNREVIEWED: (states.UNREVIEWED, states.FINAL),
110 S_FINAL: (states.FINAL, states.MAX),
111 }
112 """
113 Default supported states:
114 * obsolete: The unit is not to be used.
115 * empty: The unit has not been translated before.
116 * needs work: Some translation has been done, but is not complete.
117 * rejected: The unit has been reviewed, but was rejected.
118 * needs review: The unit has been translated, but review was requested.
119 * unreviewed: The unit has been translated, but not reviewed.
120 * final: The unit is translated, reviewed and accepted.
121 """
122
124 """Constructs a TranslationUnit containing the given source string."""
125 self.notes = ""
126 self._store = None
127 self.source = source
128 self._target = None
129 self._rich_source = None
130 self._rich_target = None
131 self._state_n = 0
132
134 """Compares two TranslationUnits.
135
136 @type other: L{TranslationUnit}
137 @param other: Another L{TranslationUnit}
138 @rtype: Boolean
139 @return: Returns True if the supplied TranslationUnit equals this unit.
140 """
141 return self.source == other.source and self.target == other.target
142
144 """Converts to a string representation that can be parsed back using
145 L{parsestring()}."""
146
147 store = getattr(self, "_store", None)
148 self._store = None
149 dump = pickle.dumps(self)
150 self._store = store
151 return dump
152
154 """Convert a "rich" string tree to a C{multistring}:
155
156 >>> from translate.storage.placeables.interfaces import X
157 >>> rich = [StringElem(['foo', X(id='xxx', sub=[' ']), 'bar'])]
158 >>> TranslationUnit.rich_to_multistring(rich)
159 multistring(u'foo bar')
160 """
161 return multistring([unicode(elem) for elem in elem_list])
162 rich_to_multistring = classmethod(rich_to_multistring)
163
165 """Convert a multistring to a list of "rich" string trees:
166
167 >>> target = multistring([u'foo', u'bar', u'baz'])
168 >>> TranslationUnit.multistring_to_rich(target)
169 [<StringElem([<StringElem([u'foo'])>])>,
170 <StringElem([<StringElem([u'bar'])>])>,
171 <StringElem([<StringElem([u'baz'])>])>]
172 """
173 if isinstance(mulstring, multistring):
174 return [rich_parse(s, self.rich_parsers) for s in mulstring.strings]
175 return [rich_parse(mulstring, self.rich_parsers)]
176
178 """Sets the source string to the given value."""
179 self._rich_source = None
180 self._source = source
181 source = property(lambda self: self._source, setsource)
182
184 """Sets the target string to the given value."""
185 self._rich_target = None
186 self._target = target
187 target = property(lambda self: self._target, settarget)
188
193
195 if not hasattr(value, '__iter__'):
196 raise ValueError('value must be iterable')
197 if len(value) < 1:
198 raise ValueError('value must have at least one element.')
199 if not isinstance(value[0], StringElem):
200 raise ValueError('value[0] must be of type StringElem.')
201 self._rich_source = list(value)
202 multi = self.rich_to_multistring(value)
203 if self.source != multi:
204 self.source = multi
205 rich_source = property(_get_rich_source, _set_rich_source)
206 """ @see: rich_to_multistring
207 @see: multistring_to_rich"""
208
213
215 if not hasattr(value, '__iter__'):
216 raise ValueError('value must be iterable')
217 if len(value) < 1:
218 raise ValueError('value must have at least one element.')
219 if not isinstance(value[0], StringElem):
220 raise ValueError('value[0] must be of type StringElem.')
221 self._rich_target = list(value)
222 self.target = self.rich_to_multistring(value)
223 rich_target = property(_get_rich_target, _set_rich_target)
224 """ @see: rich_to_multistring
225 @see: multistring_to_rich"""
226
228 """Returns the length of the target string.
229
230 @note: Plural forms might be combined.
231 @rtype: Integer
232 """
233 length = len(self.target or "")
234 strings = getattr(self.target, "strings", [])
235 if strings:
236 length += sum([len(pluralform) for pluralform in strings[1:]])
237 return length
238
240 """A unique identifier for this unit.
241
242 @rtype: string
243 @return: an identifier for this unit that is unique in the store
244
245 Derived classes should override this in a way that guarantees a unique
246 identifier for each unit in the store.
247 """
248 return self.source
249
251 """Sets the unique identified for this unit.
252
253 only implemented if format allows ids independant from other
254 unit properties like source or context"""
255 pass
256
258 """A list of source code locations.
259
260 @note: Shouldn't be implemented if the format doesn't support it.
261 @rtype: List
262 """
263 return []
264
266 """Add one location to the list of locations.
267
268 @note: Shouldn't be implemented if the format doesn't support it.
269 """
270 pass
271
273 """Add a location or a list of locations.
274
275 @note: Most classes shouldn't need to implement this,
276 but should rather implement L{addlocation()}.
277 @warning: This method might be removed in future.
278 """
279 if isinstance(location, list):
280 for item in location:
281 self.addlocation(item)
282 else:
283 self.addlocation(location)
284
285 - def getcontext(self):
286 """Get the message context."""
287 return ""
288
289 - def setcontext(self, context):
290 """Set the message context"""
291 pass
292
294 """Returns all notes about this unit.
295
296 It will probably be freeform text or something reasonable that can be
297 synthesised by the format.
298 It should not include location comments (see L{getlocations()}).
299 """
300 return getattr(self, "notes", "")
301
302 - def addnote(self, text, origin=None, position="append"):
303 """Adds a note (comment).
304
305 @type text: string
306 @param text: Usually just a sentence or two.
307 @type origin: string
308 @param origin: Specifies who/where the comment comes from.
309 Origin can be one of the following text strings:
310 - 'translator'
311 - 'developer', 'programmer', 'source code' (synonyms)
312 """
313 if position == "append" and getattr(self, "notes", None):
314 self.notes += '\n' + text
315 else:
316 self.notes = text
317
319 """Remove all the translator's notes."""
320 self.notes = u''
321
322 - def adderror(self, errorname, errortext):
323 """Adds an error message to this unit.
324
325 @type errorname: string
326 @param errorname: A single word to id the error.
327 @type errortext: string
328 @param errortext: The text describing the error.
329 """
330 pass
331
333 """Get all error messages.
334
335 @rtype: Dictionary
336 """
337 return {}
338
340 """Marks the unit to indicate whether it needs review.
341
342 @keyword needsreview: Defaults to True.
343 @keyword explanation: Adds an optional explanation as a note.
344 """
345 pass
346
348 """Indicates whether this unit is translated.
349
350 This should be used rather than deducing it from .target,
351 to ensure that other classes can implement more functionality
352 (as XLIFF does).
353 """
354 return bool(self.target) and not self.isfuzzy()
355
357 """Indicates whether this unit can be translated.
358
359 This should be used to distinguish real units for translation from
360 header, obsolete, binary or other blank units.
361 """
362 return bool(self.source)
363
365 """Indicates whether this unit is fuzzy."""
366 return False
367
369 """Marks the unit as fuzzy or not."""
370 pass
371
373 """indicate whether a unit is obsolete"""
374 return False
375
377 """Make a unit obsolete"""
378 pass
379
381 """Indicates whether this unit is a header."""
382 return False
383
385 """Indicates whether this unit needs review."""
386 return False
387
389 """Used to see if this unit has no source or target string.
390
391 @note: This is probably used more to find translatable units,
392 and we might want to move in that direction rather and get rid of this.
393 """
394 return not (self.source or self.target)
395
397 """Tells whether or not this specific unit has plural strings."""
398
399 return False
400
403
406
407 - def merge(self, otherunit, overwrite=False, comments=True,
408 authoritative=False):
412
414 """Iterator that only returns this unit."""
415 yield self
416
418 """This unit in a list."""
419 return [self]
420
422 """Build a native unit from a foreign unit, preserving as much
423 information as possible."""
424 if type(unit) == cls and hasattr(unit, "copy") and callable(unit.copy):
425 return unit.copy()
426 newunit = cls(unit.source)
427 newunit.target = unit.target
428 newunit.markfuzzy(unit.isfuzzy())
429 locations = unit.getlocations()
430 if locations:
431 newunit.addlocations(locations)
432 notes = unit.getnotes()
433 if notes:
434 newunit.addnote(notes)
435 return newunit
436 buildfromunit = classmethod(buildfromunit)
437
438 xid = property(lambda self: None, lambda self, value: None)
439 rid = property(lambda self: None, lambda self, value: None)
440
442 if n is None:
443 n = self._state_n
444 for state_id, state_range in self.STATE.iteritems():
445 if state_range[0] <= n < state_range[1]:
446 return n
447 raise ValueError('No state containing value %s' % (n))
448
451
453 self._state_n = value
454
456 """Empty method that should be overridden in sub-classes to infer the
457 current state(_n) of the unit from its current state."""
458 pass
459
460
462 """Base class for stores for multiple translation units of type
463 UnitClass."""
464
465 UnitClass = TranslationUnit
466 """The class of units that will be instantiated and used by this class"""
467 Name = "Base translation store"
468 """The human usable name of this store type"""
469 Mimetypes = None
470 """A list of MIME types associated with this store type"""
471 Extensions = None
472 """A list of file extentions associated with this store type"""
473 _binary = False
474 """Indicates whether a file should be accessed as a binary file."""
475 suggestions_in_format = False
476 """Indicates if format can store suggestions and alternative translation
477 for a unit"""
478
487
489 """Gets the source language for this store"""
490 return self.sourcelanguage
491
495
497 """Gets the target language for this store"""
498 return self.targetlanguage
499
503
505 """Iterator over all the units in this store."""
506 for unit in self.units:
507 yield unit
508
510 """Return a list of all units in this store."""
511 return [unit for unit in self.unit_iter()]
512
514 """Appends the given unit to the object's list of units.
515
516 This method should always be used rather than trying to modify the
517 list manually.
518
519 @type unit: L{TranslationUnit}
520 @param unit: The unit that will be added.
521 """
522 unit._store = self
523 self.units.append(unit)
524
526 """Adds and returns a new unit with the given source string.
527
528 @rtype: L{TranslationUnit}
529 """
530 unit = self.UnitClass(source)
531 self.addunit(unit)
532 return unit
533
535 """find unit with matching id by checking id_index"""
536 self.require_index()
537 return self.id_index.get(id, None)
538
540 """Finds the unit with the given source string.
541
542 @rtype: L{TranslationUnit} or None
543 """
544 if len(getattr(self, "sourceindex", [])):
545 if source in self.sourceindex:
546 return self.sourceindex[source][0]
547 else:
548 for unit in self.units:
549 if unit.source == source:
550 return unit
551 return None
552
554 """Finds the units with the given source string.
555
556 @rtype: L{TranslationUnit} or None
557 """
558 if len(getattr(self, "sourceindex", [])):
559 if source in self.sourceindex:
560 return self.sourceindex[source]
561 else:
562
563
564 result = []
565 for unit in self.units:
566 if unit.source == source:
567 result.append(unit)
568 return result
569 return None
570
572 """Returns the translated string for a given source string.
573
574 @rtype: String or None
575 """
576 unit = self.findunit(source)
577 if unit and unit.target:
578 return unit.target
579 else:
580 return None
581
583 """Remove a unit from source and locaton indexes"""
584
585 def remove_unit(source):
586 if source in self.sourceindex:
587 try:
588 self.sourceindex[source].remove(unit)
589 if len(self.sourceindex[source]) == 0:
590 del(self.sourceindex[source])
591 except ValueError:
592 pass
593
594 if unit.hasplural():
595 for source in unit.source.strings:
596 remove_unit(source)
597 else:
598 remove_unit(unit.source)
599
600 for location in unit.getlocations():
601 if location in self.locationindex \
602 and self.locationindex[location] is not None \
603 and self.locationindex[location] == unit:
604 del(self.locationindex[location])
605
607 """Add a unit to source and location idexes"""
608 self.id_index[unit.getid()] = unit
609
610 def insert_unit(source):
611 if not source in self.sourceindex:
612 self.sourceindex[source] = [unit]
613 else:
614 self.sourceindex[source].append(unit)
615
616 if unit.hasplural():
617 for source in unit.source.strings:
618 insert_unit(source)
619 else:
620 insert_unit(unit.source)
621
622 for location in unit.getlocations():
623 if location in self.locationindex:
624
625
626 self.locationindex[location] = None
627 else:
628 self.locationindex[location] = unit
629
631 """Indexes the items in this store. At least .sourceindex should be
632 usefull."""
633 self.locationindex = {}
634 self.sourceindex = {}
635 self.id_index = {}
636 for index, unit in enumerate(self.units):
637 unit.index = index
638 if unit.istranslatable():
639 self.add_unit_to_index(unit)
640
642 """make sure source index exists"""
643 if not hasattr(self, "id_index"):
644 self.makeindex()
645
646 - def getids(self, filename=None):
647 """return a list of unit ids"""
648 self.require_index()
649 return self.id_index.keys()
650
652 odict = self.__dict__.copy()
653 odict['fileobj'] = None
654 return odict
655
657 self.__dict__.update(dict)
658 if getattr(self, "filename", False):
659 self.fileobj = open(self.filename)
660
662 """Converts to a string representation that can be parsed back using
663 L{parsestring()}."""
664
665 fileobj = getattr(self, "fileobj", None)
666 self.fileobj = None
667 dump = pickle.dumps(self)
668 self.fileobj = fileobj
669 return dump
670
672 """Returns True if the object doesn't contain any translation units."""
673 if len(self.units) == 0:
674 return True
675 for unit in self.units:
676 if unit.istranslatable():
677 return False
678 return True
679
681 """Tries to work out what the name of the filesystem file is and
682 assigns it to .filename."""
683 fileobj = getattr(self, "fileobj", None)
684 if fileobj:
685 filename = getattr(fileobj, "name",
686 getattr(fileobj, "filename", None))
687 if filename:
688 self.filename = filename
689
691 """Converts the string representation back to an object."""
692 newstore = cls()
693 if storestring:
694 newstore.parse(storestring)
695 return newstore
696 parsestring = classmethod(parsestring)
697
699 """parser to process the given source string"""
700 self.units = pickle.loads(data).units
701
703 """Writes the string representation to the given file (or filename)."""
704 if isinstance(storefile, basestring):
705 mode = 'w'
706 if self._binary:
707 mode = 'wb'
708 storefile = open(storefile, mode)
709 self.fileobj = storefile
710 self._assignname()
711 storestring = str(self)
712 storefile.write(storestring)
713 storefile.close()
714
716 """Save to the file that data was originally read from, if
717 available."""
718 fileobj = getattr(self, "fileobj", None)
719 mode = 'w'
720 if self._binary:
721 mode = 'wb'
722 if not fileobj:
723 filename = getattr(self, "filename", None)
724 if filename:
725 fileobj = file(filename, mode)
726 else:
727 fileobj.close()
728 filename = getattr(fileobj, "name",
729 getattr(fileobj, "filename", None))
730 if not filename:
731 raise ValueError("No file or filename to save to")
732 fileobj = fileobj.__class__(filename, mode)
733 self.savefile(fileobj)
734
736 """Reads the given file (or opens the given filename) and parses back
737 to an object."""
738 mode = 'r'
739 if cls._binary:
740 mode = 'rb'
741 if isinstance(storefile, basestring):
742 storefile = open(storefile, mode)
743 mode = getattr(storefile, "mode", mode)
744
745 if mode == 1 or "r" in mode:
746 storestring = storefile.read()
747 storefile.close()
748 else:
749 storestring = ""
750 newstore = cls.parsestring(storestring)
751 newstore.fileobj = storefile
752 newstore._assignname()
753 return newstore
754 parsefile = classmethod(parsefile)
755