[fix] calculator: use locale from UI (not from selected language)

Closes: https://github.com/searxng/searxng/issues/3956
Signed-off-by: Markus Heiser <markus.heiser@darmarit.de>
This commit is contained in:
Markus Heiser 2024-10-27 10:25:42 +01:00 committed by Markus Heiser
parent da28f5280b
commit b176323e89
3 changed files with 64 additions and 42 deletions

View file

@ -8,7 +8,8 @@ import operator
from multiprocessing import Process, Queue from multiprocessing import Process, Queue
from typing import Callable from typing import Callable
import babel.numbers import flask
import babel
from flask_babel import gettext from flask_babel import gettext
from searx.plugins import logger from searx.plugins import logger
@ -100,14 +101,17 @@ def post_search(_request, search):
# replace commonly used math operators with their proper Python operator # replace commonly used math operators with their proper Python operator
query = query.replace("x", "*").replace(":", "/") query = query.replace("x", "*").replace(":", "/")
# use UI language
ui_locale = babel.Locale.parse(flask.request.preferences.get_value('locale'), sep='-')
# parse the number system in a localized way # parse the number system in a localized way
def _decimal(match: re.Match) -> str: def _decimal(match: re.Match) -> str:
val = match.string[match.start() : match.end()] val = match.string[match.start() : match.end()]
val = babel.numbers.parse_decimal(val, search.search_query.locale, numbering_system="latn") val = babel.numbers.parse_decimal(val, ui_locale, numbering_system="latn")
return str(val) return str(val)
decimal = search.search_query.locale.number_symbols["latn"]["decimal"] decimal = ui_locale.number_symbols["latn"]["decimal"]
group = search.search_query.locale.number_symbols["latn"]["group"] group = ui_locale.number_symbols["latn"]["group"]
query = re.sub(f"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query) query = re.sub(f"[0-9]+[{decimal}|{group}][0-9]+[{decimal}|{group}]?[0-9]?", _decimal, query)
# only numbers and math operators are accepted # only numbers and math operators are accepted
@ -121,6 +125,6 @@ def post_search(_request, search):
result = timeout_func(0.05, _eval_expr, query_py_formatted) result = timeout_func(0.05, _eval_expr, query_py_formatted)
if result is None or result == "": if result is None or result == "":
return True return True
result = babel.numbers.format_decimal(result, locale=search.search_query.locale) result = babel.numbers.format_decimal(result, locale=ui_locale)
search.result_container.answers['calculate'] = {'answer': f"{search.search_query.query} = {result}"} search.result_container.answers['calculate'] = {'answer': f"{search.search_query.query} = {result}"}
return True return True

View file

@ -517,7 +517,7 @@ def pre_request():
preferences.parse_dict({"language": language}) preferences.parse_dict({"language": language})
logger.debug('set language %s (from browser)', preferences.get_value("language")) logger.debug('set language %s (from browser)', preferences.get_value("language"))
# locale is defined neither in settings nor in preferences # UI locale is defined neither in settings nor in preferences
# use browser headers # use browser headers
if not preferences.get_value("locale"): if not preferences.get_value("locale"):
locale = _get_browser_language(request, LOCALE_NAMES.keys()) locale = _get_browser_language(request, LOCALE_NAMES.keys())

View file

@ -1,9 +1,10 @@
# SPDX-License-Identifier: AGPL-3.0-or-later # SPDX-License-Identifier: AGPL-3.0-or-later
# pylint: disable=missing-module-docstring # pylint: disable=missing-module-docstring
from mock import Mock import flask
from parameterized.parameterized import parameterized from parameterized.parameterized import parameterized
from searx import plugins from searx import plugins
from searx import preferences
from tests import SearxTestCase from tests import SearxTestCase
from .test_utils import random_string from .test_utils import random_string
@ -11,63 +12,78 @@ from .test_plugins import get_search_mock
class PluginCalculator(SearxTestCase): # pylint: disable=missing-class-docstring class PluginCalculator(SearxTestCase): # pylint: disable=missing-class-docstring
def setUp(self): def setUp(self):
from searx import webapp # pylint: disable=import-outside-toplevel
self.webapp = webapp
self.store = plugins.PluginStore() self.store = plugins.PluginStore()
plugin = plugins.load_and_initialize_plugin('searx.plugins.calculator', False, (None, {})) plugin = plugins.load_and_initialize_plugin('searx.plugins.calculator', False, (None, {}))
self.store.register(plugin) self.store.register(plugin)
self.preferences = preferences.Preferences(["simple"], ["general"], {}, self.store)
self.preferences.parse_dict({"locale": "en"})
def test_plugin_store_init(self): def test_plugin_store_init(self):
self.assertEqual(1, len(self.store.plugins)) self.assertEqual(1, len(self.store.plugins))
def test_single_page_number_true(self): def test_single_page_number_true(self):
request = Mock(remote_addr='127.0.0.1') with self.webapp.app.test_request_context():
flask.request.preferences = self.preferences
search = get_search_mock(query=random_string(10), pageno=2) search = get_search_mock(query=random_string(10), pageno=2)
result = self.store.call(self.store.plugins, 'post_search', request, search) result = self.store.call(self.store.plugins, 'post_search', flask.request, search)
self.assertTrue(result) self.assertTrue(result)
self.assertNotIn('calculate', search.result_container.answers) self.assertNotIn('calculate', search.result_container.answers)
def test_long_query_true(self): def test_long_query_true(self):
request = Mock(remote_addr='127.0.0.1') with self.webapp.app.test_request_context():
flask.request.preferences = self.preferences
search = get_search_mock(query=random_string(101), pageno=1) search = get_search_mock(query=random_string(101), pageno=1)
result = self.store.call(self.store.plugins, 'post_search', request, search) result = self.store.call(self.store.plugins, 'post_search', flask.request, search)
self.assertTrue(result) self.assertTrue(result)
self.assertNotIn('calculate', search.result_container.answers) self.assertNotIn('calculate', search.result_container.answers)
def test_alpha_true(self): def test_alpha_true(self):
request = Mock(remote_addr='127.0.0.1') with self.webapp.app.test_request_context():
flask.request.preferences = self.preferences
search = get_search_mock(query=random_string(10), pageno=1) search = get_search_mock(query=random_string(10), pageno=1)
result = self.store.call(self.store.plugins, 'post_search', request, search) result = self.store.call(self.store.plugins, 'post_search', flask.request, search)
self.assertTrue(result) self.assertTrue(result)
self.assertNotIn('calculate', search.result_container.answers) self.assertNotIn('calculate', search.result_container.answers)
@parameterized.expand( @parameterized.expand(
[ [
("1+1", "2", "en-US"), ("1+1", "2", "en"),
("1-1", "0", "en-US"), ("1-1", "0", "en"),
("1*1", "1", "en-US"), ("1*1", "1", "en"),
("1/1", "1", "en-US"), ("1/1", "1", "en"),
("1**1", "1", "en-US"), ("1**1", "1", "en"),
("1^1", "1", "en-US"), ("1^1", "1", "en"),
("1,000.0+1,000.0", "2,000", "en-US"), ("1,000.0+1,000.0", "2,000", "en"),
("1.0+1.0", "2", "en-US"), ("1.0+1.0", "2", "en"),
("1.0-1.0", "0", "en-US"), ("1.0-1.0", "0", "en"),
("1.0*1.0", "1", "en-US"), ("1.0*1.0", "1", "en"),
("1.0/1.0", "1", "en-US"), ("1.0/1.0", "1", "en"),
("1.0**1.0", "1", "en-US"), ("1.0**1.0", "1", "en"),
("1.0^1.0", "1", "en-US"), ("1.0^1.0", "1", "en"),
("1.000,0+1.000,0", "2.000", "de-DE"), ("1.000,0+1.000,0", "2.000", "de"),
("1,0+1,0", "2", "de-DE"), ("1,0+1,0", "2", "de"),
("1,0-1,0", "0", "de-DE"), ("1,0-1,0", "0", "de"),
("1,0*1,0", "1", "de-DE"), ("1,0*1,0", "1", "de"),
("1,0/1,0", "1", "de-DE"), ("1,0/1,0", "1", "de"),
("1,0**1,0", "1", "de-DE"), ("1,0**1,0", "1", "de"),
("1,0^1,0", "1", "de-DE"), ("1,0^1,0", "1", "de"),
] ]
) )
def test_localized_query(self, operation: str, contains_result: str, lang: str): def test_localized_query(self, operation: str, contains_result: str, lang: str):
request = Mock(remote_addr='127.0.0.1') with self.webapp.app.test_request_context():
self.preferences.parse_dict({"locale": lang})
flask.request.preferences = self.preferences
search = get_search_mock(query=operation, lang=lang, pageno=1) search = get_search_mock(query=operation, lang=lang, pageno=1)
result = self.store.call(self.store.plugins, 'post_search', request, search) result = self.store.call(self.store.plugins, 'post_search', flask.request, search)
self.assertTrue(result) self.assertTrue(result)
self.assertIn('calculate', search.result_container.answers) self.assertIn('calculate', search.result_container.answers)
self.assertIn(contains_result, search.result_container.answers['calculate']['answer']) self.assertIn(contains_result, search.result_container.answers['calculate']['answer'])
@ -78,8 +94,10 @@ class PluginCalculator(SearxTestCase): # pylint: disable=missing-class-docstrin
] ]
) )
def test_invalid_operations(self, operation): def test_invalid_operations(self, operation):
request = Mock(remote_addr='127.0.0.1') with self.webapp.app.test_request_context():
flask.request.preferences = self.preferences
search = get_search_mock(query=operation, pageno=1) search = get_search_mock(query=operation, pageno=1)
result = self.store.call(self.store.plugins, 'post_search', request, search) result = self.store.call(self.store.plugins, 'post_search', flask.request, search)
self.assertTrue(result) self.assertTrue(result)
self.assertNotIn('calculate', search.result_container.answers) self.assertNotIn('calculate', search.result_container.answers)