locales.py 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152
  1. # -*- coding: utf-8 -*-
  2. # SPDX-License-Identifier: AGPL-3.0-or-later
  3. # lint: pylint
  4. """Initialize :py:obj:`LOCALE_NAMES`, :py:obj:`RTL_LOCALES`.
  5. """
  6. from typing import Set
  7. import os
  8. import pathlib
  9. from babel import Locale
  10. from babel.support import Translations
  11. import flask_babel
  12. import flask
  13. from flask.ctx import has_request_context
  14. from searx import logger
  15. logger = logger.getChild('locales')
  16. # safe before monkey patching flask_babel.get_translations
  17. _flask_babel_get_translations = flask_babel.get_translations
  18. LOCALE_NAMES = {}
  19. """Mapping of locales and their description. Locales e.g. 'fr' or 'pt-BR' (see
  20. :py:obj:`locales_initialize`)."""
  21. RTL_LOCALES: Set[str] = set()
  22. """List of *Right-To-Left* locales e.g. 'he' or 'fa-IR' (see
  23. :py:obj:`locales_initialize`)."""
  24. ADDITIONAL_TRANSLATIONS = {
  25. "oc": "Occitan",
  26. "szl": "Ślōnski (Silesian)",
  27. "pap": "Papiamento",
  28. }
  29. """Additional languages SearXNG has translations for but not supported by
  30. python-babel (see :py:obj:`locales_initialize`)."""
  31. LOCALE_BEST_MATCH = {
  32. "oc": 'fr-FR',
  33. "szl": "pl",
  34. "nl-BE": "nl",
  35. "zh-HK": "zh-Hant-TW",
  36. "pap": "pt-BR",
  37. }
  38. """Map a locale we do not have a translations for to a locale we have a
  39. translation for. By example: use Taiwan version of the translation for Hong
  40. Kong."""
  41. def localeselector():
  42. locale = 'en'
  43. if has_request_context():
  44. value = flask.request.preferences.get_value('locale')
  45. if value:
  46. locale = value
  47. # first, set the language that is not supported by babel
  48. if locale in ADDITIONAL_TRANSLATIONS:
  49. flask.request.form['use-translation'] = locale
  50. # second, map locale to a value python-babel supports
  51. locale = LOCALE_BEST_MATCH.get(locale, locale)
  52. if locale == '':
  53. # if there is an error loading the preferences
  54. # the locale is going to be ''
  55. locale = 'en'
  56. # babel uses underscore instead of hyphen.
  57. locale = locale.replace('-', '_')
  58. return locale
  59. def get_translations():
  60. """Monkey patch of :py:obj:`flask_babel.get_translations`"""
  61. if has_request_context() and flask.request.form.get('use-translation') == 'oc':
  62. babel_ext = flask_babel.current_app.extensions['babel']
  63. return Translations.load(next(babel_ext.translation_directories), 'oc')
  64. if has_request_context() and flask.request.form.get('use-translation') == 'szl':
  65. babel_ext = flask_babel.current_app.extensions['babel']
  66. return Translations.load(next(babel_ext.translation_directories), 'szl')
  67. if has_request_context() and flask.request.form.get('use-translation') == 'pap':
  68. babel_ext = flask_babel.current_app.extensions['babel']
  69. return Translations.load(next(babel_ext.translation_directories), 'pap')
  70. return _flask_babel_get_translations()
  71. def get_locale_descr(locale, locale_name):
  72. """Get locale name e.g. 'Français - fr' or 'Português (Brasil) - pt-BR'
  73. :param locale: instance of :py:class:`Locale`
  74. :param locale_name: name e.g. 'fr' or 'pt_BR' (delimiter is *underscore*)
  75. """
  76. native_language, native_territory = _get_locale_descr(locale, locale_name)
  77. english_language, english_territory = _get_locale_descr(locale, 'en')
  78. if native_territory == english_territory:
  79. english_territory = None
  80. if not native_territory and not english_territory:
  81. if native_language == english_language:
  82. return native_language
  83. return native_language + ' (' + english_language + ')'
  84. result = native_language + ', ' + native_territory + ' (' + english_language
  85. if english_territory:
  86. return result + ', ' + english_territory + ')'
  87. return result + ')'
  88. def _get_locale_descr(locale, language_code):
  89. language_name = locale.get_language_name(language_code).capitalize()
  90. if language_name and ('a' <= language_name[0] <= 'z'):
  91. language_name = language_name.capitalize()
  92. terrirtory_name = locale.get_territory_name(language_code)
  93. return language_name, terrirtory_name
  94. def locales_initialize(directory=None):
  95. """Initialize locales environment of the SearXNG session.
  96. - monkey patch :py:obj:`flask_babel.get_translations` by :py:obj:`get_translations`
  97. - init global names :py:obj:`LOCALE_NAMES`, :py:obj:`RTL_LOCALES`
  98. """
  99. directory = directory or pathlib.Path(__file__).parent / 'translations'
  100. logger.debug("locales_initialize: %s", directory)
  101. flask_babel.get_translations = get_translations
  102. for tag, descr in ADDITIONAL_TRANSLATIONS.items():
  103. LOCALE_NAMES[tag] = descr
  104. for tag in LOCALE_BEST_MATCH:
  105. descr = LOCALE_NAMES.get(tag)
  106. if not descr:
  107. locale = Locale.parse(tag, sep='-')
  108. LOCALE_NAMES[tag] = get_locale_descr(locale, tag.replace('-', '_'))
  109. for dirname in sorted(os.listdir(directory)):
  110. # Based on https://flask-babel.tkte.ch/_modules/flask_babel.html#Babel.list_translations
  111. if not os.path.isdir(os.path.join(directory, dirname, 'LC_MESSAGES')):
  112. continue
  113. tag = dirname.replace('_', '-')
  114. descr = LOCALE_NAMES.get(tag)
  115. if not descr:
  116. locale = Locale.parse(dirname)
  117. LOCALE_NAMES[tag] = get_locale_descr(locale, dirname)
  118. if locale.text_direction == 'rtl':
  119. RTL_LOCALES.add(tag)