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 gettext 

2import os 

3 

4from translationstring import ( 

5 Translator, 

6 Pluralizer, 

7 TranslationString, # API 

8 TranslationStringFactory, # API 

9) 

10 

11from pyramid.compat import PY2 

12from pyramid.decorator import reify 

13 

14from pyramid.interfaces import ( 

15 ILocalizer, 

16 ITranslationDirectories, 

17 ILocaleNegotiator, 

18) 

19 

20from pyramid.threadlocal import get_current_registry 

21 

22TranslationString = TranslationString # PyFlakes 

23TranslationStringFactory = TranslationStringFactory # PyFlakes 

24 

25DEFAULT_PLURAL = lambda n: int(n != 1) 

26 

27 

28class Localizer(object): 

29 """ 

30 An object providing translation and pluralizations related to 

31 the current request's locale name. A 

32 :class:`pyramid.i18n.Localizer` object is created using the 

33 :func:`pyramid.i18n.get_localizer` function. 

34 """ 

35 

36 def __init__(self, locale_name, translations): 

37 self.locale_name = locale_name 

38 self.translations = translations 

39 self.pluralizer = None 

40 self.translator = None 

41 

42 def translate(self, tstring, domain=None, mapping=None): 

43 """ 

44 Translate a :term:`translation string` to the current language 

45 and interpolate any *replacement markers* in the result. The 

46 ``translate`` method accepts three arguments: ``tstring`` 

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

48 When called, it will translate the ``tstring`` translation 

49 string to a ``unicode`` object using the current locale. If 

50 the current locale could not be determined, the result of 

51 interpolation of the default value is returned. The optional 

52 ``domain`` argument can be used to specify or override the 

53 domain of the ``tstring`` (useful when ``tstring`` is a normal 

54 string rather than a translation string). The optional 

55 ``mapping`` argument can specify or override the ``tstring`` 

56 interpolation mapping, useful when the ``tstring`` argument is 

57 a simple string instead of a translation string. 

58 

59 Example:: 

60 

61 from pyramid.18n import TranslationString 

62 ts = TranslationString('Add ${item}', domain='mypackage', 

63 mapping={'item':'Item'}) 

64 translated = localizer.translate(ts) 

65 

66 Example:: 

67 

68 translated = localizer.translate('Add ${item}', domain='mypackage', 

69 mapping={'item':'Item'}) 

70 

71 """ 

72 if self.translator is None: 

73 self.translator = Translator(self.translations) 

74 return self.translator(tstring, domain=domain, mapping=mapping) 

75 

76 def pluralize(self, singular, plural, n, domain=None, mapping=None): 

77 """ 

78 Return a Unicode string translation by using two 

79 :term:`message identifier` objects as a singular/plural pair 

80 and an ``n`` value representing the number that appears in the 

81 message using gettext plural forms support. The ``singular`` 

82 and ``plural`` objects should be unicode strings. There is no 

83 reason to use translation string objects as arguments as all 

84 metadata is ignored. 

85 

86 ``n`` represents the number of elements. ``domain`` is the 

87 translation domain to use to do the pluralization, and ``mapping`` 

88 is the interpolation mapping that should be used on the result. If 

89 the ``domain`` is not supplied, a default domain is used (usually 

90 ``messages``). 

91 

92 Example:: 

93 

94 num = 1 

95 translated = localizer.pluralize('Add ${num} item', 

96 'Add ${num} items', 

97 num, 

98 mapping={'num':num}) 

99 

100 If using the gettext plural support, which is required for 

101 languages that have pluralisation rules other than n != 1, the 

102 ``singular`` argument must be the message_id defined in the 

103 translation file. The plural argument is not used in this case. 

104 

105 Example:: 

106 

107 num = 1 

108 translated = localizer.pluralize('item_plural', 

109 '', 

110 num, 

111 mapping={'num':num}) 

112 

113 

114 """ 

115 if self.pluralizer is None: 

116 self.pluralizer = Pluralizer(self.translations) 

117 return self.pluralizer( 

118 singular, plural, n, domain=domain, mapping=mapping 

119 ) 

120 

121 

122def default_locale_negotiator(request): 

123 """ The default :term:`locale negotiator`. Returns a locale name 

124 or ``None``. 

125 

126 - First, the negotiator looks for the ``_LOCALE_`` attribute of 

127 the request object (possibly set by a view or a listener for an 

128 :term:`event`). If the attribute exists and it is not ``None``, 

129 its value will be used. 

130 

131 - Then it looks for the ``request.params['_LOCALE_']`` value. 

132 

133 - Then it looks for the ``request.cookies['_LOCALE_']`` value. 

134 

135 - Finally, the negotiator returns ``None`` if the locale could not 

136 be determined via any of the previous checks (when a locale 

137 negotiator returns ``None``, it signifies that the 

138 :term:`default locale name` should be used.) 

139 """ 

140 name = '_LOCALE_' 

141 locale_name = getattr(request, name, None) 

142 if locale_name is None: 

143 locale_name = request.params.get(name) 

144 if locale_name is None: 

145 locale_name = request.cookies.get(name) 

146 return locale_name 

147 

148 

149def negotiate_locale_name(request): 

150 """ Negotiate and return the :term:`locale name` associated with 

151 the current request.""" 

152 try: 

153 registry = request.registry 

154 except AttributeError: 

155 registry = get_current_registry() 

156 negotiator = registry.queryUtility( 

157 ILocaleNegotiator, default=default_locale_negotiator 

158 ) 

159 locale_name = negotiator(request) 

160 

161 if locale_name is None: 

162 settings = registry.settings or {} 

163 locale_name = settings.get('default_locale_name', 'en') 

164 

165 return locale_name 

166 

167 

168def get_locale_name(request): 

169 """ 

170 .. deprecated:: 1.5 

171 Use :attr:`pyramid.request.Request.locale_name` directly instead. 

172 Return the :term:`locale name` associated with the current request. 

173 """ 

174 return request.locale_name 

175 

176 

177def make_localizer(current_locale_name, translation_directories): 

178 """ Create a :class:`pyramid.i18n.Localizer` object 

179 corresponding to the provided locale name from the 

180 translations found in the list of translation directories.""" 

181 translations = Translations() 

182 translations._catalog = {} 

183 

184 locales_to_try = [] 

185 if '_' in current_locale_name: 

186 locales_to_try = [current_locale_name.split('_')[0]] 

187 locales_to_try.append(current_locale_name) 

188 

189 # intent: order locales left to right in least specific to most specific, 

190 # e.g. ['de', 'de_DE']. This services the intent of creating a 

191 # translations object that returns a "more specific" translation for a 

192 # region, but will fall back to a "less specific" translation for the 

193 # locale if necessary. Ordering from least specific to most specific 

194 # allows us to call translations.add in the below loop to get this 

195 # behavior. 

196 

197 for tdir in translation_directories: 

198 locale_dirs = [] 

199 for lname in locales_to_try: 

200 ldir = os.path.realpath(os.path.join(tdir, lname)) 

201 if os.path.isdir(ldir): 

202 locale_dirs.append(ldir) 

203 

204 for locale_dir in locale_dirs: 

205 messages_dir = os.path.join(locale_dir, 'LC_MESSAGES') 

206 if not os.path.isdir(os.path.realpath(messages_dir)): 

207 continue 

208 for mofile in os.listdir(messages_dir): 

209 mopath = os.path.realpath(os.path.join(messages_dir, mofile)) 

210 if mofile.endswith('.mo') and os.path.isfile(mopath): 

211 with open(mopath, 'rb') as mofp: 

212 domain = mofile[:-3] 

213 dtrans = Translations(mofp, domain) 

214 translations.add(dtrans) 

215 

216 return Localizer( 

217 locale_name=current_locale_name, translations=translations 

218 ) 

219 

220 

221def get_localizer(request): 

222 """ 

223 .. deprecated:: 1.5 

224 Use the :attr:`pyramid.request.Request.localizer` attribute directly 

225 instead. Retrieve a :class:`pyramid.i18n.Localizer` object 

226 corresponding to the current request's locale name. 

227 """ 

228 return request.localizer 

229 

230 

231class Translations(gettext.GNUTranslations, object): 

232 """An extended translation catalog class (ripped off from Babel) """ 

233 

234 DEFAULT_DOMAIN = 'messages' 

235 

236 def __init__(self, fileobj=None, domain=DEFAULT_DOMAIN): 

237 """Initialize the translations catalog. 

238 

239 :param fileobj: the file-like object the translation should be read 

240 from 

241 """ 

242 # germanic plural by default; self.plural will be overwritten by 

243 # GNUTranslations._parse (called as a side effect if fileobj is 

244 # passed to GNUTranslations.__init__) with a "real" self.plural for 

245 # this domain; see https://github.com/Pylons/pyramid/issues/235 

246 # It is only overridden the first time a new message file is found 

247 # for a given domain, so all message files must have matching plural 

248 # rules if they are in the same domain. We keep track of if we have 

249 # overridden so we can special case the default domain, which is always 

250 # instantiated before a message file is read. 

251 # See also https://github.com/Pylons/pyramid/pull/2102 

252 self.plural = DEFAULT_PLURAL 

253 gettext.GNUTranslations.__init__(self, fp=fileobj) 

254 self.files = list(filter(None, [getattr(fileobj, 'name', None)])) 

255 self.domain = domain 

256 self._domains = {} 

257 

258 @classmethod 

259 def load(cls, dirname=None, locales=None, domain=DEFAULT_DOMAIN): 

260 """Load translations from the given directory. 

261 

262 :param dirname: the directory containing the ``MO`` files 

263 :param locales: the list of locales in order of preference (items in 

264 this list can be either `Locale` objects or locale 

265 strings) 

266 :param domain: the message domain 

267 :return: the loaded catalog, or a ``NullTranslations`` instance if no 

268 matching translations were found 

269 :rtype: `Translations` 

270 """ 

271 if locales is not None: 

272 if not isinstance(locales, (list, tuple)): 

273 locales = [locales] 

274 locales = [str(l) for l in locales] 

275 if not domain: 

276 domain = cls.DEFAULT_DOMAIN 

277 filename = gettext.find(domain, dirname, locales) 

278 if not filename: 

279 return gettext.NullTranslations() 

280 with open(filename, 'rb') as fp: 

281 return cls(fileobj=fp, domain=domain) 

282 

283 def __repr__(self): 

284 return '<%s: "%s">' % ( 

285 type(self).__name__, 

286 self._info.get('project-id-version'), 

287 ) 

288 

289 def add(self, translations, merge=True): 

290 """Add the given translations to the catalog. 

291 

292 If the domain of the translations is different than that of the 

293 current catalog, they are added as a catalog that is only accessible 

294 by the various ``d*gettext`` functions. 

295 

296 :param translations: the `Translations` instance with the messages to 

297 add 

298 :param merge: whether translations for message domains that have 

299 already been added should be merged with the existing 

300 translations 

301 :return: the `Translations` instance (``self``) so that `merge` calls 

302 can be easily chained 

303 :rtype: `Translations` 

304 """ 

305 domain = getattr(translations, 'domain', self.DEFAULT_DOMAIN) 

306 if domain == self.DEFAULT_DOMAIN and self.plural is DEFAULT_PLURAL: 

307 self.plural = translations.plural 

308 

309 if merge and domain == self.domain: 

310 return self.merge(translations) 

311 

312 existing = self._domains.get(domain) 

313 if merge and existing is not None: 

314 existing.merge(translations) 

315 else: 

316 translations.add_fallback(self) 

317 self._domains[domain] = translations 

318 

319 return self 

320 

321 def merge(self, translations): 

322 """Merge the given translations into the catalog. 

323 

324 Message translations in the specified catalog override any messages 

325 with the same identifier in the existing catalog. 

326 

327 :param translations: the `Translations` instance with the messages to 

328 merge 

329 :return: the `Translations` instance (``self``) so that `merge` calls 

330 can be easily chained 

331 :rtype: `Translations` 

332 """ 

333 if isinstance(translations, gettext.GNUTranslations): 

334 self._catalog.update(translations._catalog) 

335 if isinstance(translations, Translations): 

336 self.files.extend(translations.files) 

337 

338 return self 

339 

340 def dgettext(self, domain, message): 

341 """Like ``gettext()``, but look the message up in the specified 

342 domain. 

343 """ 

344 return self._domains.get(domain, self).gettext(message) 

345 

346 def ldgettext(self, domain, message): 

347 """Like ``lgettext()``, but look the message up in the specified 

348 domain. 

349 """ 

350 return self._domains.get(domain, self).lgettext(message) 

351 

352 def dugettext(self, domain, message): 

353 """Like ``ugettext()``, but look the message up in the specified 

354 domain. 

355 """ 

356 if PY2: 

357 return self._domains.get(domain, self).ugettext(message) 

358 else: 

359 return self._domains.get(domain, self).gettext(message) 

360 

361 def dngettext(self, domain, singular, plural, num): 

362 """Like ``ngettext()``, but look the message up in the specified 

363 domain. 

364 """ 

365 return self._domains.get(domain, self).ngettext(singular, plural, num) 

366 

367 def ldngettext(self, domain, singular, plural, num): 

368 """Like ``lngettext()``, but look the message up in the specified 

369 domain. 

370 """ 

371 return self._domains.get(domain, self).lngettext(singular, plural, num) 

372 

373 def dungettext(self, domain, singular, plural, num): 

374 """Like ``ungettext()`` but look the message up in the specified 

375 domain. 

376 """ 

377 if PY2: 

378 return self._domains.get(domain, self).ungettext( 

379 singular, plural, num 

380 ) 

381 else: 

382 return self._domains.get(domain, self).ngettext( 

383 singular, plural, num 

384 ) 

385 

386 

387class LocalizerRequestMixin(object): 

388 @reify 

389 def localizer(self): 

390 """ Convenience property to return a localizer """ 

391 registry = self.registry 

392 

393 current_locale_name = self.locale_name 

394 localizer = registry.queryUtility(ILocalizer, name=current_locale_name) 

395 

396 if localizer is None: 

397 # no localizer utility registered yet 

398 tdirs = registry.queryUtility(ITranslationDirectories, default=[]) 

399 localizer = make_localizer(current_locale_name, tdirs) 

400 

401 registry.registerUtility( 

402 localizer, ILocalizer, name=current_locale_name 

403 ) 

404 

405 return localizer 

406 

407 @reify 

408 def locale_name(self): 

409 locale_name = negotiate_locale_name(self) 

410 return locale_name