Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/utils/translation/trans_real.py: 49%

309 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1"""Translation helper functions.""" 

2import functools 

3import gettext as gettext_module 

4import os 

5import re 

6import sys 

7import warnings 

8 

9from asgiref.local import Local 

10 

11from django.apps import apps 

12from django.conf import settings 

13from django.conf.locale import LANG_INFO 

14from django.core.exceptions import AppRegistryNotReady 

15from django.core.signals import setting_changed 

16from django.dispatch import receiver 

17from django.utils.regex_helper import _lazy_re_compile 

18from django.utils.safestring import SafeData, mark_safe 

19 

20from . import to_language, to_locale 

21 

22# Translations are cached in a dictionary for every language. 

23# The active translations are stored by threadid to make them thread local. 

24_translations = {} 

25_active = Local() 

26 

27# The default translation is based on the settings file. 

28_default = None 

29 

30# magic gettext number to separate context from message 

31CONTEXT_SEPARATOR = "\x04" 

32 

33# Format of Accept-Language header values. From RFC 2616, section 14.4 and 3.9 

34# and RFC 3066, section 2.1 

35accept_language_re = _lazy_re_compile( 

36 r""" 

37 # "en", "en-au", "x-y-z", "es-419", "*" 

38 ([A-Za-z]{1,8}(?:-[A-Za-z0-9]{1,8})*|\*) 

39 # Optional "q=1.00", "q=0.8" 

40 (?:\s*;\s*q=(0(?:\.\d{,3})?|1(?:\.0{,3})?))? 

41 # Multiple accepts per header. 

42 (?:\s*,\s*|$) 

43 """, 

44 re.VERBOSE, 

45) 

46 

47language_code_re = _lazy_re_compile( 

48 r"^[a-z]{1,8}(?:-[a-z0-9]{1,8})*(?:@[a-z0-9]{1,20})?$", re.IGNORECASE 

49) 

50 

51language_code_prefix_re = _lazy_re_compile(r"^/(\w+([@-]\w+)?)(/|$)") 

52 

53 

54@receiver(setting_changed) 

55def reset_cache(**kwargs): 

56 """ 

57 Reset global state when LANGUAGES setting has been changed, as some 

58 languages should no longer be accepted. 

59 """ 

60 if kwargs["setting"] in ("LANGUAGES", "LANGUAGE_CODE"): 

61 check_for_language.cache_clear() 

62 get_languages.cache_clear() 

63 get_supported_language_variant.cache_clear() 

64 

65 

66class TranslationCatalog: 

67 """ 

68 Simulate a dict for DjangoTranslation._catalog so as multiple catalogs 

69 with different plural equations are kept separate. 

70 """ 

71 

72 def __init__(self, trans=None): 

73 self._catalogs = [trans._catalog.copy()] if trans else [{}] 

74 self._plurals = [trans.plural] if trans else [lambda n: int(n != 1)] 74 ↛ exitline 74 didn't run the lambda on line 74

75 

76 def __getitem__(self, key): 

77 for cat in self._catalogs: 

78 try: 

79 return cat[key] 

80 except KeyError: 

81 pass 

82 raise KeyError(key) 

83 

84 def __setitem__(self, key, value): 

85 self._catalogs[0][key] = value 

86 

87 def __contains__(self, key): 

88 return any(key in cat for cat in self._catalogs) 

89 

90 def items(self): 

91 for cat in self._catalogs: 

92 yield from cat.items() 

93 

94 def keys(self): 

95 for cat in self._catalogs: 

96 yield from cat.keys() 

97 

98 def update(self, trans): 

99 # Merge if plural function is the same, else prepend. 

100 for cat, plural in zip(self._catalogs, self._plurals): 

101 if trans.plural.__code__ == plural.__code__: 

102 cat.update(trans._catalog) 

103 break 

104 else: 

105 self._catalogs.insert(0, trans._catalog.copy()) 

106 self._plurals.insert(0, trans.plural) 

107 

108 def get(self, key, default=None): 

109 missing = object() 

110 for cat in self._catalogs: 

111 result = cat.get(key, missing) 

112 if result is not missing: 

113 return result 

114 return default 

115 

116 def plural(self, msgid, num): 

117 for cat, plural in zip(self._catalogs, self._plurals): 

118 tmsg = cat.get((msgid, plural(num))) 

119 if tmsg is not None: 

120 return tmsg 

121 raise KeyError 

122 

123 

124class DjangoTranslation(gettext_module.GNUTranslations): 

125 """ 

126 Set up the GNUTranslations context with regard to output charset. 

127 

128 This translation object will be constructed out of multiple GNUTranslations 

129 objects by merging their catalogs. It will construct an object for the 

130 requested language and add a fallback to the default language, if it's 

131 different from the requested language. 

132 """ 

133 

134 domain = "django" 

135 

136 def __init__(self, language, domain=None, localedirs=None): 

137 """Create a GNUTranslations() using many locale directories""" 

138 gettext_module.GNUTranslations.__init__(self) 

139 if domain is not None: 139 ↛ 140line 139 didn't jump to line 140, because the condition on line 139 was never true

140 self.domain = domain 

141 

142 self.__language = language 

143 self.__to_language = to_language(language) 

144 self.__locale = to_locale(language) 

145 self._catalog = None 

146 # If a language doesn't have a catalog, use the Germanic default for 

147 # pluralization: anything except one is pluralized. 

148 self.plural = lambda n: int(n != 1) 148 ↛ exitline 148 didn't run the lambda on line 148

149 

150 if self.domain == "django": 150 ↛ 159line 150 didn't jump to line 159, because the condition on line 150 was never false

151 if localedirs is not None: 151 ↛ 153line 151 didn't jump to line 153, because the condition on line 151 was never true

152 # A module-level cache is used for caching 'django' translations 

153 warnings.warn( 

154 "localedirs is ignored when domain is 'django'.", RuntimeWarning 

155 ) 

156 localedirs = None 

157 self._init_translation_catalog() 

158 

159 if localedirs: 159 ↛ 160line 159 didn't jump to line 160, because the condition on line 159 was never true

160 for localedir in localedirs: 

161 translation = self._new_gnu_trans(localedir) 

162 self.merge(translation) 

163 else: 

164 self._add_installed_apps_translations() 

165 

166 self._add_local_translations() 

167 if ( 167 ↛ 173line 167 didn't jump to line 173

168 self.__language == settings.LANGUAGE_CODE 

169 and self.domain == "django" 

170 and self._catalog is None 

171 ): 

172 # default lang should have at least one translation file available. 

173 raise OSError( 

174 "No translation files found for default language %s." 

175 % settings.LANGUAGE_CODE 

176 ) 

177 self._add_fallback(localedirs) 

178 if self._catalog is None: 178 ↛ 180line 178 didn't jump to line 180, because the condition on line 178 was never true

179 # No catalogs found for this language, set an empty catalog. 

180 self._catalog = TranslationCatalog() 

181 

182 def __repr__(self): 

183 return "<DjangoTranslation lang:%s>" % self.__language 

184 

185 def _new_gnu_trans(self, localedir, use_null_fallback=True): 

186 """ 

187 Return a mergeable gettext.GNUTranslations instance. 

188 

189 A convenience wrapper. By default gettext uses 'fallback=False'. 

190 Using param `use_null_fallback` to avoid confusion with any other 

191 references to 'fallback'. 

192 """ 

193 return gettext_module.translation( 

194 domain=self.domain, 

195 localedir=localedir, 

196 languages=[self.__locale], 

197 fallback=use_null_fallback, 

198 ) 

199 

200 def _init_translation_catalog(self): 

201 """Create a base catalog using global django translations.""" 

202 settingsfile = sys.modules[settings.__module__].__file__ 

203 localedir = os.path.join(os.path.dirname(settingsfile), "locale") 

204 translation = self._new_gnu_trans(localedir) 

205 self.merge(translation) 

206 

207 def _add_installed_apps_translations(self): 

208 """Merge translations from each installed app.""" 

209 try: 

210 app_configs = reversed(list(apps.get_app_configs())) 

211 except AppRegistryNotReady: 

212 raise AppRegistryNotReady( 

213 "The translation infrastructure cannot be initialized before the " 

214 "apps registry is ready. Check that you don't make non-lazy " 

215 "gettext calls at import time." 

216 ) 

217 for app_config in app_configs: 

218 localedir = os.path.join(app_config.path, "locale") 

219 if os.path.exists(localedir): 

220 translation = self._new_gnu_trans(localedir) 

221 self.merge(translation) 

222 

223 def _add_local_translations(self): 

224 """Merge translations defined in LOCALE_PATHS.""" 

225 for localedir in reversed(settings.LOCALE_PATHS): 225 ↛ 226line 225 didn't jump to line 226, because the loop on line 225 never started

226 translation = self._new_gnu_trans(localedir) 

227 self.merge(translation) 

228 

229 def _add_fallback(self, localedirs=None): 

230 """Set the GNUTranslations() fallback with the default language.""" 

231 # Don't set a fallback for the default language or any English variant 

232 # (as it's empty, so it'll ALWAYS fall back to the default language) 

233 if self.__language == settings.LANGUAGE_CODE or self.__language.startswith( 233 ↛ 237line 233 didn't jump to line 237, because the condition on line 233 was never false

234 "en" 

235 ): 

236 return 

237 if self.domain == "django": 

238 # Get from cache 

239 default_translation = translation(settings.LANGUAGE_CODE) 

240 else: 

241 default_translation = DjangoTranslation( 

242 settings.LANGUAGE_CODE, domain=self.domain, localedirs=localedirs 

243 ) 

244 self.add_fallback(default_translation) 

245 

246 def merge(self, other): 

247 """Merge another translation into this catalog.""" 

248 if not getattr(other, "_catalog", None): 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true

249 return # NullTranslations() has no _catalog 

250 if self._catalog is None: 

251 # Take plural and _info from first catalog found (generally Django's). 

252 self.plural = other.plural 

253 self._info = other._info.copy() 

254 self._catalog = TranslationCatalog(other) 

255 else: 

256 self._catalog.update(other) 

257 if other._fallback: 

258 self.add_fallback(other._fallback) 

259 

260 def language(self): 

261 """Return the translation language.""" 

262 return self.__language 

263 

264 def to_language(self): 

265 """Return the translation language name.""" 

266 return self.__to_language 

267 

268 def ngettext(self, msgid1, msgid2, n): 

269 try: 

270 tmsg = self._catalog.plural(msgid1, n) 

271 except KeyError: 

272 if self._fallback: 

273 return self._fallback.ngettext(msgid1, msgid2, n) 

274 if n == 1: 

275 tmsg = msgid1 

276 else: 

277 tmsg = msgid2 

278 return tmsg 

279 

280 

281def translation(language): 

282 """ 

283 Return a translation object in the default 'django' domain. 

284 """ 

285 global _translations 

286 if language not in _translations: 

287 _translations[language] = DjangoTranslation(language) 

288 return _translations[language] 

289 

290 

291def activate(language): 

292 """ 

293 Fetch the translation object for a given language and install it as the 

294 current translation object for the current thread. 

295 """ 

296 if not language: 296 ↛ 297line 296 didn't jump to line 297, because the condition on line 296 was never true

297 return 

298 _active.value = translation(language) 

299 

300 

301def deactivate(): 

302 """ 

303 Uninstall the active translation object so that further _() calls resolve 

304 to the default translation object. 

305 """ 

306 if hasattr(_active, "value"): 306 ↛ 307line 306 didn't jump to line 307, because the condition on line 306 was never true

307 del _active.value 

308 

309 

310def deactivate_all(): 

311 """ 

312 Make the active translation object a NullTranslations() instance. This is 

313 useful when we want delayed translations to appear as the original string 

314 for some reason. 

315 """ 

316 _active.value = gettext_module.NullTranslations() 

317 _active.value.to_language = lambda *args: None 

318 

319 

320def get_language(): 

321 """Return the currently selected language.""" 

322 t = getattr(_active, "value", None) 

323 if t is not None: 

324 try: 

325 return t.to_language() 

326 except AttributeError: 

327 pass 

328 # If we don't have a real translation object, assume it's the default language. 

329 return settings.LANGUAGE_CODE 

330 

331 

332def get_language_bidi(): 

333 """ 

334 Return selected language's BiDi layout. 

335 

336 * False = left-to-right layout 

337 * True = right-to-left layout 

338 """ 

339 lang = get_language() 

340 if lang is None: 

341 return False 

342 else: 

343 base_lang = get_language().split("-")[0] 

344 return base_lang in settings.LANGUAGES_BIDI 

345 

346 

347def catalog(): 

348 """ 

349 Return the current active catalog for further processing. 

350 This can be used if you need to modify the catalog or want to access the 

351 whole message catalog instead of just translating one string. 

352 """ 

353 global _default 

354 

355 t = getattr(_active, "value", None) 

356 if t is not None: 

357 return t 

358 if _default is None: 

359 _default = translation(settings.LANGUAGE_CODE) 

360 return _default 

361 

362 

363def gettext(message): 

364 """ 

365 Translate the 'message' string. It uses the current thread to find the 

366 translation object to use. If no current translation is activated, the 

367 message will be run through the default translation object. 

368 """ 

369 global _default 

370 

371 eol_message = message.replace("\r\n", "\n").replace("\r", "\n") 

372 

373 if eol_message: 373 ↛ 381line 373 didn't jump to line 381, because the condition on line 373 was never false

374 _default = _default or translation(settings.LANGUAGE_CODE) 

375 translation_object = getattr(_active, "value", _default) 

376 

377 result = translation_object.gettext(eol_message) 

378 else: 

379 # Return an empty value of the corresponding type if an empty message 

380 # is given, instead of metadata, which is the default gettext behavior. 

381 result = type(message)("") 

382 

383 if isinstance(message, SafeData): 383 ↛ 384line 383 didn't jump to line 384, because the condition on line 383 was never true

384 return mark_safe(result) 

385 

386 return result 

387 

388 

389def pgettext(context, message): 

390 msg_with_ctxt = "%s%s%s" % (context, CONTEXT_SEPARATOR, message) 

391 result = gettext(msg_with_ctxt) 

392 if CONTEXT_SEPARATOR in result: 

393 # Translation not found 

394 result = message 

395 elif isinstance(message, SafeData): 

396 result = mark_safe(result) 

397 return result 

398 

399 

400def gettext_noop(message): 

401 """ 

402 Mark strings for translation but don't translate them now. This can be 

403 used to store strings in global variables that should stay in the base 

404 language (because they might be used externally) and will be translated 

405 later. 

406 """ 

407 return message 

408 

409 

410def do_ntranslate(singular, plural, number, translation_function): 

411 global _default 

412 

413 t = getattr(_active, "value", None) 

414 if t is not None: 

415 return getattr(t, translation_function)(singular, plural, number) 

416 if _default is None: 

417 _default = translation(settings.LANGUAGE_CODE) 

418 return getattr(_default, translation_function)(singular, plural, number) 

419 

420 

421def ngettext(singular, plural, number): 

422 """ 

423 Return a string of the translation of either the singular or plural, 

424 based on the number. 

425 """ 

426 return do_ntranslate(singular, plural, number, "ngettext") 

427 

428 

429def npgettext(context, singular, plural, number): 

430 msgs_with_ctxt = ( 

431 "%s%s%s" % (context, CONTEXT_SEPARATOR, singular), 

432 "%s%s%s" % (context, CONTEXT_SEPARATOR, plural), 

433 number, 

434 ) 

435 result = ngettext(*msgs_with_ctxt) 

436 if CONTEXT_SEPARATOR in result: 

437 # Translation not found 

438 result = ngettext(singular, plural, number) 

439 return result 

440 

441 

442def all_locale_paths(): 

443 """ 

444 Return a list of paths to user-provides languages files. 

445 """ 

446 globalpath = os.path.join( 

447 os.path.dirname(sys.modules[settings.__module__].__file__), "locale" 

448 ) 

449 app_paths = [] 

450 for app_config in apps.get_app_configs(): 

451 locale_path = os.path.join(app_config.path, "locale") 

452 if os.path.exists(locale_path): 

453 app_paths.append(locale_path) 

454 return [globalpath, *settings.LOCALE_PATHS, *app_paths] 

455 

456 

457@functools.lru_cache(maxsize=1000) 

458def check_for_language(lang_code): 

459 """ 

460 Check whether there is a global language file for the given language 

461 code. This is used to decide whether a user-provided language is 

462 available. 

463 

464 lru_cache should have a maxsize to prevent from memory exhaustion attacks, 

465 as the provided language codes are taken from the HTTP request. See also 

466 <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>. 

467 """ 

468 # First, a quick check to make sure lang_code is well-formed (#21458) 

469 if lang_code is None or not language_code_re.search(lang_code): 469 ↛ 470line 469 didn't jump to line 470, because the condition on line 469 was never true

470 return False 

471 return any( 471 ↛ exitline 471 didn't finish the generator expression on line 471

472 gettext_module.find("django", path, [to_locale(lang_code)]) is not None 

473 for path in all_locale_paths() 

474 ) 

475 

476 

477@functools.lru_cache() 

478def get_languages(): 

479 """ 

480 Cache of settings.LANGUAGES in a dictionary for easy lookups by key. 

481 """ 

482 return dict(settings.LANGUAGES) 

483 

484 

485@functools.lru_cache(maxsize=1000) 

486def get_supported_language_variant(lang_code, strict=False): 

487 """ 

488 Return the language code that's listed in supported languages, possibly 

489 selecting a more generic variant. Raise LookupError if nothing is found. 

490 

491 If `strict` is False (the default), look for a country-specific variant 

492 when neither the language code nor its generic variant is found. 

493 

494 lru_cache should have a maxsize to prevent from memory exhaustion attacks, 

495 as the provided language codes are taken from the HTTP request. See also 

496 <https://www.djangoproject.com/weblog/2007/oct/26/security-fix/>. 

497 """ 

498 if lang_code: 498 ↛ 520line 498 didn't jump to line 520, because the condition on line 498 was never false

499 # If 'zh-hant-tw' is not supported, try special fallback or subsequent 

500 # language codes i.e. 'zh-hant' and 'zh'. 

501 possible_lang_codes = [lang_code] 

502 try: 

503 possible_lang_codes.extend(LANG_INFO[lang_code]["fallback"]) 

504 except KeyError: 

505 pass 

506 i = None 

507 while (i := lang_code.rfind("-", 0, i)) > -1: 

508 possible_lang_codes.append(lang_code[:i]) 

509 generic_lang_code = possible_lang_codes[-1] 

510 supported_lang_codes = get_languages() 

511 

512 for code in possible_lang_codes: 512 ↛ 515line 512 didn't jump to line 515, because the loop on line 512 didn't complete

513 if code in supported_lang_codes and check_for_language(code): 513 ↛ 512line 513 didn't jump to line 512, because the condition on line 513 was never false

514 return code 

515 if not strict: 

516 # if fr-fr is not supported, try fr-ca. 

517 for supported_code in supported_lang_codes: 

518 if supported_code.startswith(generic_lang_code + "-"): 

519 return supported_code 

520 raise LookupError(lang_code) 

521 

522 

523def get_language_from_path(path, strict=False): 

524 """ 

525 Return the language code if there's a valid language code found in `path`. 

526 

527 If `strict` is False (the default), look for a country-specific variant 

528 when neither the language code nor its generic variant is found. 

529 """ 

530 regex_match = language_code_prefix_re.match(path) 

531 if not regex_match: 

532 return None 

533 lang_code = regex_match[1] 

534 try: 

535 return get_supported_language_variant(lang_code, strict=strict) 

536 except LookupError: 

537 return None 

538 

539 

540def get_language_from_request(request, check_path=False): 

541 """ 

542 Analyze the request to find what language the user wants the system to 

543 show. Only languages listed in settings.LANGUAGES are taken into account. 

544 If the user requests a sublanguage where we have a main language, we send 

545 out the main language. 

546 

547 If check_path is True, the URL path prefix will be checked for a language 

548 code, otherwise this is skipped for backwards compatibility. 

549 """ 

550 if check_path: 

551 lang_code = get_language_from_path(request.path_info) 

552 if lang_code is not None: 

553 return lang_code 

554 

555 lang_code = request.COOKIES.get(settings.LANGUAGE_COOKIE_NAME) 

556 if ( 

557 lang_code is not None 

558 and lang_code in get_languages() 

559 and check_for_language(lang_code) 

560 ): 

561 return lang_code 

562 

563 try: 

564 return get_supported_language_variant(lang_code) 

565 except LookupError: 

566 pass 

567 

568 accept = request.META.get("HTTP_ACCEPT_LANGUAGE", "") 

569 for accept_lang, unused in parse_accept_lang_header(accept): 

570 if accept_lang == "*": 

571 break 

572 

573 if not language_code_re.search(accept_lang): 

574 continue 

575 

576 try: 

577 return get_supported_language_variant(accept_lang) 

578 except LookupError: 

579 continue 

580 

581 try: 

582 return get_supported_language_variant(settings.LANGUAGE_CODE) 

583 except LookupError: 

584 return settings.LANGUAGE_CODE 

585 

586 

587@functools.lru_cache(maxsize=1000) 

588def parse_accept_lang_header(lang_string): 

589 """ 

590 Parse the lang_string, which is the body of an HTTP Accept-Language 

591 header, and return a tuple of (lang, q-value), ordered by 'q' values. 

592 

593 Return an empty tuple if there are any format errors in lang_string. 

594 """ 

595 result = [] 

596 pieces = accept_language_re.split(lang_string.lower()) 

597 if pieces[-1]: 

598 return () 

599 for i in range(0, len(pieces) - 1, 3): 

600 first, lang, priority = pieces[i : i + 3] 

601 if first: 

602 return () 

603 if priority: 

604 priority = float(priority) 

605 else: 

606 priority = 1.0 

607 result.append((lang, priority)) 

608 result.sort(key=lambda k: k[1], reverse=True) 

609 return tuple(result)