locales.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  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. }
  28. """Additional languages SearXNG has translations for but not supported by
  29. python-babel (see :py:obj:`locales_initialize`)."""
  30. LOCALE_BEST_MATCH = {
  31. "oc": 'fr-FR',
  32. "szl": "pl",
  33. "nl-BE": "nl",
  34. "zh-HK": "zh-Hant-TW",
  35. }
  36. """Map a locale we do not have a translations for to a locale we have a
  37. translation for. By example: use Taiwan version of the translation for Hong
  38. Kong."""
  39. def localeselector():
  40. locale = 'en'
  41. if has_request_context():
  42. value = flask.request.preferences.get_value('locale')
  43. if value:
  44. locale = value
  45. # first, set the language that is not supported by babel
  46. if locale in ADDITIONAL_TRANSLATIONS:
  47. flask.request.form['use-translation'] = locale
  48. # second, map locale to a value python-babel supports
  49. locale = LOCALE_BEST_MATCH.get(locale, locale)
  50. if locale == '':
  51. # if there is an error loading the preferences
  52. # the locale is going to be ''
  53. locale = 'en'
  54. # babel uses underscore instead of hyphen.
  55. locale = locale.replace('-', '_')
  56. return locale
  57. def get_translations():
  58. """Monkey patch of flask_babel.get_translations"""
  59. if has_request_context() and flask.request.form.get('use-translation') == 'oc':
  60. babel_ext = flask_babel.current_app.extensions['babel']
  61. return Translations.load(next(babel_ext.translation_directories), 'oc')
  62. if has_request_context() and flask.request.form.get('use-translation') == 'szl':
  63. babel_ext = flask_babel.current_app.extensions['babel']
  64. return Translations.load(next(babel_ext.translation_directories), 'szl')
  65. return _flask_babel_get_translations()
  66. def get_locale_descr(locale, locale_name):
  67. """Get locale name e.g. 'Français - fr' or 'Português (Brasil) - pt-BR'
  68. :param locale: instance of :py:class:`Locale`
  69. :param locale_name: name e.g. 'fr' or 'pt_BR' (delimiter is *underscore*)
  70. """
  71. native_language, native_territory = _get_locale_descr(locale, locale_name)
  72. english_language, english_territory = _get_locale_descr(locale, 'en')
  73. if native_territory == english_territory:
  74. english_territory = None
  75. if not native_territory and not english_territory:
  76. if native_language == english_language:
  77. return native_language
  78. return native_language + ' (' + english_language + ')'
  79. result = native_language + ', ' + native_territory + ' (' + english_language
  80. if english_territory:
  81. return result + ', ' + english_territory + ')'
  82. return result + ')'
  83. def _get_locale_descr(locale, language_code):
  84. language_name = locale.get_language_name(language_code).capitalize()
  85. if language_name and ('a' <= language_name[0] <= 'z'):
  86. language_name = language_name.capitalize()
  87. terrirtory_name = locale.get_territory_name(language_code)
  88. return language_name, terrirtory_name
  89. def locales_initialize(directory=None):
  90. """Initialize locales environment of the SearXNG session.
  91. - monkey patch :py:obj:`flask_babel.get_translations` by :obj:py:`get_translations`
  92. - init global names :py:obj:`LOCALE_NAMES`, :py:obj:`RTL_LOCALES`
  93. """
  94. directory = directory or pathlib.Path(__file__).parent / 'translations'
  95. logger.debug("locales_initialize: %s", directory)
  96. flask_babel.get_translations = get_translations
  97. for tag, descr in ADDITIONAL_TRANSLATIONS.items():
  98. LOCALE_NAMES[tag] = descr
  99. for tag in LOCALE_BEST_MATCH:
  100. descr = LOCALE_NAMES.get(tag)
  101. if not descr:
  102. locale = Locale.parse(tag, sep='-')
  103. LOCALE_NAMES[tag] = get_locale_descr(locale, tag.replace('-', '_'))
  104. for dirname in sorted(os.listdir(directory)):
  105. # Based on https://flask-babel.tkte.ch/_modules/flask_babel.html#Babel.list_translations
  106. if not os.path.isdir(os.path.join(directory, dirname, 'LC_MESSAGES')):
  107. continue
  108. tag = dirname.replace('_', '-')
  109. descr = LOCALE_NAMES.get(tag)
  110. if not descr:
  111. locale = Locale.parse(dirname)
  112. LOCALE_NAMES[tag] = get_locale_descr(locale, dirname)
  113. if locale.text_direction == 'rtl':
  114. RTL_LOCALES.add(tag)