Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import re 

2from gettext import NullTranslations 

3from translationstring.compat import text_type 

4from translationstring.compat import string_types 

5from translationstring.compat import PY3 

6 

7NAME_RE = r"[a-zA-Z][-a-zA-Z0-9_]*" 

8 

9_interp_regex = re.compile(r'(?<!\$)(\$(?:(%(n)s)|{(%(n)s)}))' 

10 % ({'n': NAME_RE})) 

11 

12CONTEXT_MASK = text_type('%s\x04%s') 

13 

14class TranslationString(text_type): 

15 """ 

16 The constructor for a :term:`translation string`. A translation 

17 string is a Unicode-like object that has some extra metadata. 

18 

19 This constructor accepts one required argument named ``msgid``. 

20 ``msgid`` must be the :term:`message identifier` for the 

21 translation string. It must be a ``unicode`` object or a ``str`` 

22 object encoded in the default system encoding. 

23 

24 Optional keyword arguments to this object's constructor include 

25 ``domain``, ``default``, and ``mapping``. 

26 

27 ``domain`` represents the :term:`translation domain`. By default, 

28 the translation domain is ``None``, indicating that this 

29 translation string is associated with the default translation 

30 domain (usually ``messages``). 

31 

32 ``default`` represents an explicit *default text* for this 

33 translation string. Default text appears when the translation 

34 string cannot be translated. Usually, the ``msgid`` of a 

35 translation string serves double duty as its default text. 

36 However, using this option you can provide a different default 

37 text for this translation string. This feature is useful when the 

38 default of a translation string is too complicated or too long to 

39 be used as a message identifier. If ``default`` is provided, it 

40 must be a ``unicode`` object or a ``str`` object encoded in the 

41 default system encoding (usually means ASCII). If ``default`` is 

42 ``None`` (its default value), the ``msgid`` value used by this 

43 translation string will be assumed to be the value of ``default``. 

44 

45 ``mapping``, if supplied, must be a dictionary-like object which 

46 represents the replacement values for any :term:`translation 

47 string` *replacement marker* instances found within the ``msgid`` 

48 (or ``default``) value of this translation string. 

49 

50 ``context`` represents the :term:`translation context`. By default, 

51 the translation context is ``None``. 

52 

53 After a translation string is constructed, it behaves like most 

54 other ``unicode`` objects; its ``msgid`` value will be displayed 

55 when it is treated like a ``unicode`` object. Only when its 

56 ``ugettext`` method is called will it be translated. 

57 

58 Its default value is available as the ``default`` attribute of the 

59 object, its :term:`translation domain` is available as the 

60 ``domain`` attribute, and the ``mapping`` is available as the 

61 ``mapping`` attribute. The object otherwise behaves much like a 

62 Unicode string. 

63 """ 

64 __slots__ = ('domain', 'context', 'default', 'mapping') 

65 

66 def __new__(self, msgid, domain=None, default=None, mapping=None, context=None): 

67 

68 # NB: this function should never never lose the *original 

69 # identity* of a non-``None`` but empty ``default`` value 

70 # provided to it. See the comment in ChameleonTranslate. 

71 

72 self = text_type.__new__(self, msgid) 

73 if isinstance(msgid, self.__class__): 

74 domain = domain or msgid.domain and msgid.domain[:] 

75 context = context or msgid.context and msgid.context[:] 

76 default = default or msgid.default and msgid.default[:] 

77 if msgid.mapping: 

78 if mapping: 

79 for k, v in msgid.mapping.items(): 

80 mapping.setdefault(k, v) 

81 else: 

82 mapping = msgid.mapping.copy() 

83 msgid = text_type(msgid) 

84 self.domain = domain 

85 self.context = context 

86 if default is None: 

87 default = text_type(msgid) 

88 self.default = default 

89 self.mapping = mapping 

90 return self 

91 

92 def __mod__(self, options): 

93 """Create a new TranslationString instance with an updated mapping. 

94 This makes it possible to use the standard python %-style string 

95 formatting with translatable strings. Only dictionary 

96 arguments are supported. 

97 """ 

98 if not isinstance(options, dict): 

99 raise ValueError( 

100 'Can only interpolate translationstring ' 

101 'with dictionaries.') 

102 if self.mapping: 

103 mapping = self.mapping.copy() 

104 mapping.update(options) 

105 else: 

106 mapping = options.copy() 

107 return TranslationString(self, mapping=mapping) 

108 

109 def interpolate(self, translated=None): 

110 """ Interpolate the value ``translated`` which is assumed to 

111 be a Unicode object containing zero or more *replacement 

112 markers* (``$foo`` or ``${bar}``) using the ``mapping`` 

113 dictionary attached to this instance. If the ``mapping`` 

114 dictionary is empty or ``None``, no interpolation is 

115 performed. 

116 

117 If ``translated`` is ``None``, interpolation will be performed 

118 against the ``default`` value. 

119 """ 

120 if translated is None: 

121 translated = self.default 

122 

123 # NB: this function should never never lose the *original 

124 # identity* of a non-``None`` but empty ``default`` value it 

125 # is provided. If (translated == default) , it should return the 

126 # *original* default, not a derivation. See the comment below in 

127 # ChameleonTranslate. 

128 

129 if self.mapping and translated: 

130 def replace(match): 

131 whole, param1, param2 = match.groups() 

132 return text_type(self.mapping.get(param1 or param2, whole)) 

133 translated = _interp_regex.sub(replace, translated) 

134 

135 return translated 

136 

137 def __reduce__(self): 

138 return self.__class__, self.__getstate__() 

139 

140 def __getstate__(self): 

141 return text_type(self), self.domain, self.default, self.mapping, self.context 

142 

143def TranslationStringFactory(factory_domain): 

144 """ Create a factory which will generate translation strings 

145 without requiring that each call to the factory be passed a 

146 ``domain`` value. A single argument is passed to this class' 

147 constructor: ``domain``. This value will be used as the 

148 ``domain`` values of :class:`translationstring.TranslationString` 

149 objects generated by the ``__call__`` of this class. The 

150 ``msgid``, ``mapping``, and ``default`` values provided to the 

151 ``__call__`` method of an instance of this class have the meaning 

152 as described by the constructor of the 

153 :class:`translationstring.TranslationString`""" 

154 def create(msgid, mapping=None, default=None, context=None): 

155 """ Provided a msgid (Unicode object or :term:`translation 

156 string`) and optionally a mapping object, and a *default 

157 value*, return a :term:`translation string` object.""" 

158 

159 # if we are passing in a TranslationString as the msgid, then 

160 # use its domain 

161 if isinstance(msgid, TranslationString): 

162 domain = msgid.domain or factory_domain 

163 else: 

164 domain = factory_domain 

165 

166 return TranslationString(msgid, domain=domain, default=default, 

167 mapping=mapping, context=context) 

168 return create 

169 

170def ChameleonTranslate(translator): 

171 """ 

172 When necessary, use the result of calling this function as a 

173 Chameleon template 'translate' function (e.g. the ``translate`` 

174 argument to the ``chameleon.zpt.template.PageTemplate`` 

175 constructor) to allow our translation machinery to drive template 

176 translation. A single required argument ``translator`` is 

177 passsed. The ``translator`` provided should be a callable which 

178 accepts a single argument ``translation_string`` ( a 

179 :class:`translationstring.TranslationString` instance) which 

180 returns a ``unicode`` object as a translation. ``translator`` may 

181 also optionally be ``None``, in which case no translation is 

182 performed (the ``msgid`` or ``default`` value is returned 

183 untranslated). 

184 """ 

185 def translate(msgid, domain=None, mapping=None, context=None, 

186 target_language=None, default=None): 

187 

188 # NB: note that both TranslationString._init__ and 

189 # TranslationString.interpolate are careful to never lose the 

190 # *identity* of an empty but non-``None`` ``default`` value we 

191 # provide to them. For example, neither of those functions 

192 # are permitted to run an empty but non-``None`` ``default`` 

193 # through ``unicode`` and throw the original default value 

194 # away afterwards. 

195 

196 # This has a dubious cause: for Chameleon API reasons we must 

197 # ensure that, after translation, if ( (translated == msgid) 

198 # and (not default) and (default is not None) ) that we return 

199 # the ``default`` value provided to us *unmodified*, because 

200 # Chameleon uses it as a sentinel (it compares the return 

201 # value of this function by identity to what it passed in as 

202 # ``default``; this marker is a 

203 # chameleon.core.i18n.StringMarker instance, a subclass of str 

204 # that == ''). This is, of course, totally absurd, because 

205 # Chameleon *also* wants us to use ``default`` as the input to 

206 # a translation string in some cases, and maintaining the 

207 # identity of this object through translation operations isn't 

208 # a contract it spells out in its docs. 

209 

210 # Chameleon's use of ``default`` to represent both a sentinel 

211 # and input to a translation string is a Chameleon i18n 

212 # extensibility design bug. Until we overhaul its hook point 

213 # for translation extensibility, we need to appease it by 

214 # preserving ``default`` in the aforementioned case. So we 

215 # spray these indignant comments all over this module. ;-) 

216 

217 if not isinstance(msgid, string_types): 

218 if msgid is not None: 

219 msgid = text_type(msgid) 

220 return msgid 

221 

222 tstring = msgid 

223 

224 if not hasattr(tstring, 'interpolate'): 

225 tstring = TranslationString(msgid, domain, default, mapping, context) 

226 if translator is None: 

227 result = tstring.interpolate() 

228 else: 

229 result = translator(tstring) 

230 

231 return result 

232 

233 return translate 

234 

235def ugettext_policy(translations, tstring, domain, context): 

236 """ A translator policy function which unconditionally uses the 

237 ``ugettext`` API on the translations object.""" 

238 

239 if PY3: # pragma: no cover 

240 _gettext = translations.gettext 

241 else: # pragma: no cover 

242 _gettext = translations.ugettext 

243 

244 if context: 

245 # Workaround for http://bugs.python.org/issue2504? 

246 msgid = CONTEXT_MASK % (context, tstring) 

247 else: 

248 msgid = tstring 

249 

250 translated = _gettext(msgid) 

251 return tstring if translated == msgid else translated 

252 

253def dugettext_policy(translations, tstring, domain, context): 

254 """ A translator policy function which assumes the use of a 

255 :class:`babel.support.Translations` translations object, which 

256 supports the dugettext API; fall back to ugettext.""" 

257 if domain is None: 

258 default_domain = getattr(translations, 'domain', None) or 'messages' 

259 domain = getattr(tstring, 'domain', None) or default_domain 

260 context = context or getattr(tstring, 'context', None) 

261 if context: 

262 # Workaround for http://bugs.python.org/issue2504? 

263 msgid = CONTEXT_MASK % (context, tstring) 

264 else: 

265 msgid = tstring 

266 

267 if getattr(translations, 'dugettext', None) is not None: 

268 translated = translations.dugettext(domain, msgid) 

269 else: 

270 if PY3: # pragma: no cover 

271 _gettext = translations.gettext 

272 else: # pragma: no cover 

273 _gettext = translations.ugettext 

274 

275 translated = _gettext(msgid) 

276 return tstring if translated == msgid else translated 

277 

278def Translator(translations=None, policy=None): 

279 """ 

280 Return a translator object based on the ``translations`` and 

281 ``policy`` provided. ``translations`` should be an object 

282 supporting *at least* the Python :class:`gettext.NullTranslations` 

283 API but ideally the :class:`babel.support.Translations` API, which 

284 has support for domain lookups like dugettext. 

285 

286 ``policy`` should be a callable which accepts three arguments: 

287 ``translations``, ``tstring`` and ``domain``. It must perform the 

288 actual translation lookup. If ``policy`` is ``None``, the 

289 :func:`translationstring.dugettext_policy` policy will be used. 

290 

291 The callable returned accepts three arguments: ``tstring`` 

292 (required), ``domain`` (optional) and ``mapping`` (optional). 

293 When called, it will translate the ``tstring`` translation string 

294 to a ``unicode`` object using the ``translations`` provided. If 

295 ``translations`` is ``None``, the result of interpolation of the 

296 default value is returned. The optional ``domain`` argument can 

297 be used to specify or override the domain of the ``tstring`` 

298 (useful when ``tstring`` is a normal string rather than a 

299 translation string). The optional ``mapping`` argument can 

300 specify or override the ``tstring`` interpolation mapping, useful 

301 when the ``tstring`` argument is a simple string instead of a 

302 translation string. 

303 """ 

304 if policy is None: 

305 policy = dugettext_policy 

306 def translator(tstring, domain=None, mapping=None, context=None): 

307 if not hasattr(tstring, 'interpolate'): 

308 tstring = TranslationString(tstring, domain=domain, mapping=mapping, context=context) 

309 elif mapping: 

310 if tstring.mapping: 

311 new_mapping = tstring.mapping.copy() 

312 new_mapping.update(mapping) 

313 else: 

314 new_mapping = mapping 

315 tstring = TranslationString(tstring, domain=domain, mapping=new_mapping, context=context) 

316 translated = tstring 

317 domain = domain or tstring.domain 

318 context = context or tstring.context 

319 if translations is not None: 

320 translated = policy(translations, tstring, domain, context) 

321 if translated == tstring: 

322 translated = tstring.default 

323 if translated and '$' in translated and tstring.mapping: 

324 translated = tstring.interpolate(translated) 

325 return translated 

326 return translator 

327 

328def ungettext_policy(translations, singular, plural, n, domain, context): 

329 """ A pluralizer policy function which unconditionally uses the 

330 ``ungettext`` API on the translations object.""" 

331 

332 if PY3: # pragma: no cover 

333 _gettext = translations.ngettext 

334 else: # pragma: no cover 

335 _gettext = translations.ungettext 

336 

337 if context: 

338 # Workaround for http://bugs.python.org/issue2504? 

339 msgid = CONTEXT_MASK % (context, singular) 

340 else: 

341 msgid = singular 

342 

343 translated = _gettext(msgid, plural, n) 

344 return singular if translated == msgid else translated 

345 

346def dungettext_policy(translations, singular, plural, n, domain, context): 

347 """ A pluralizer policy function which assumes the use of the 

348 :class:`babel.support.Translations` class, which supports the 

349 dungettext API; falls back to ungettext.""" 

350 

351 default_domain = getattr(translations, 'domain', None) or 'messages' 

352 domain = domain or default_domain 

353 if context: 

354 # Workaround for http://bugs.python.org/issue2504? 

355 msgid = CONTEXT_MASK % (context, singular) 

356 else: 

357 msgid = singular 

358 if getattr(translations, 'dungettext', None) is not None: 

359 translated = translations.dungettext(domain, msgid, plural, n) 

360 else: 

361 if PY3: # pragma: no cover 

362 _gettext = translations.ngettext 

363 else: # pragma: no cover 

364 _gettext = translations.ungettext 

365 

366 translated = _gettext(msgid, plural, n) 

367 return singular if translated == msgid else translated 

368 

369def Pluralizer(translations=None, policy=None): 

370 """ 

371 Return a pluralizer object based on the ``translations`` and 

372 ``policy`` provided. ``translations`` should be an object 

373 supporting *at least* the Python :class:`gettext.NullTranslations` 

374 API but ideally the :class:`babel.support.Translations` API, which 

375 has support for domain lookups like dugettext. 

376 

377 ``policy`` should be a callable which accepts five arguments: 

378 ``translations``, ``singular`` and ``plural``, ``n`` and 

379 ``domain``. It must perform the actual pluralization lookup. If 

380 ``policy`` is ``None``, the 

381 :func:`translationstring.dungettext_policy` policy will be used. 

382 

383 The object returned will be a callable which has the following 

384 signature:: 

385 

386 def pluralizer(singular, plural, n, domain=None, mapping=None): 

387 ... 

388 

389 The ``singular`` and ``plural`` objects passed may be translation 

390 strings or unicode strings. ``n`` represents the number of 

391 elements. ``domain`` is the translation domain to use to do the 

392 pluralization, and ``mapping`` is the interpolation mapping that 

393 should be used on the result. Note that if the objects passed are 

394 translation strings, their domains and mappings are ignored. The 

395 domain and mapping arguments must be used instead. If the ``domain`` is 

396 not supplied, a default domain is used (usually ``messages``). 

397 """ 

398 

399 if policy is None: 

400 policy = dungettext_policy 

401 if translations is None: 

402 translations = NullTranslations() 

403 def pluralizer(singular, plural, n, domain=None, mapping=None, context=None): 

404 """ Pluralize this object """ 

405 translated = text_type( 

406 policy(translations, singular, plural, n, domain, context)) 

407 if translated and '$' in translated and mapping: 

408 return TranslationString(translated, mapping=mapping).interpolate() 

409 return translated 

410 return pluralizer