diff --git a/src/nominatim_api/localization.py b/src/nominatim_api/localization.py index 3c786a20..eccabf98 100644 --- a/src/nominatim_api/localization.py +++ b/src/nominatim_api/localization.py @@ -8,7 +8,6 @@ Helper functions for localizing names of results. """ from typing import Mapping, List, Optional -from .config import Configuration from .results import AddressLines, BaseResultT import re @@ -18,15 +17,15 @@ class Locales: """ Helper class for localization of names. It takes a list of language prefixes in their order of preferred - usage. + usage and comma separated name keys (Configuration.OUTPUT_NAMES). """ - def __init__(self, langs: Optional[List[str]] = None): - self.config = Configuration(None) + def __init__(self, langs: Optional[List[str]] = None, + names: str = 'name:XX,name') -> None: self.languages = langs or [] self.name_tags: List[str] = [] - parts = self.config.OUTPUT_NAMES.split(',') + parts = names.split(',') if names else [] for part in parts: part = part.strip() @@ -68,7 +67,7 @@ class Locales: return next(iter(names.values())) @staticmethod - def from_accept_languages(langstr: str) -> 'Locales': + def from_accept_languages(langstr: str, names: str = 'name:XX,name') -> 'Locales': """ Create a localization object from a language list in the format of HTTP accept-languages header. @@ -96,7 +95,7 @@ class Locales: if len(parts) > 1 and all(c[0] != parts[0] for c in candidates): languages.append(parts[0]) - return Locales(languages) + return Locales(languages, names) def localize(self, lines: AddressLines) -> None: """ Sets the local name of address parts according to the chosen diff --git a/src/nominatim_api/v1/server_glue.py b/src/nominatim_api/v1/server_glue.py index 871358ef..41d88f23 100644 --- a/src/nominatim_api/v1/server_glue.py +++ b/src/nominatim_api/v1/server_glue.py @@ -174,7 +174,8 @@ async def details_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any: if result is None: params.raise_error('No place with that OSM ID found.', status=404) - locales = Locales.from_accept_languages(get_accepted_languages(params)) + locales = Locales.from_accept_languages(get_accepted_languages(params), + params.config().OUTPUT_NAMES) locales.localize_results([result]) output = params.formatting().format_result( @@ -215,8 +216,8 @@ async def reverse_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any: query = '' if result: - Locales.from_accept_languages(get_accepted_languages(params)).localize_results( - [result]) + Locales.from_accept_languages(get_accepted_languages(params), + params.config().OUTPUT_NAMES).localize_results([result]) fmt_options = {'query': query, 'extratags': params.get_bool('extratags', False), @@ -255,7 +256,8 @@ async def lookup_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any: if debug: return build_response(params, loglib.get_and_disable(), num_results=len(results)) - Locales.from_accept_languages(get_accepted_languages(params)).localize_results(results) + Locales.from_accept_languages(get_accepted_languages(params), + params.config().OUTPUT_NAMES).localize_results(results) fmt_options = {'extratags': params.get_bool('extratags', False), 'namedetails': params.get_bool('namedetails', False), @@ -348,7 +350,8 @@ async def search_endpoint(api: NominatimAPIAsync, params: ASGIAdaptor) -> Any: except UsageError as err: params.raise_error(str(err)) - Locales.from_accept_languages(get_accepted_languages(params)).localize_results(results) + Locales.from_accept_languages(get_accepted_languages(params), + params.config().OUTPUT_NAMES).localize_results(results) if details['dedupe'] and len(results) > 1: results = helpers.deduplicate_results(results, max_results) diff --git a/src/nominatim_db/clicmd/api.py b/src/nominatim_db/clicmd/api.py index 36b84acd..3644a2f5 100644 --- a/src/nominatim_db/clicmd/api.py +++ b/src/nominatim_db/clicmd/api.py @@ -19,6 +19,7 @@ import nominatim_api as napi from nominatim_api.v1.helpers import zoom_to_rank, deduplicate_results from nominatim_api.server.content_types import CONTENT_JSON import nominatim_api.logging as loglib +from ..config import Configuration from ..errors import UsageError from .args import NominatimArgs @@ -91,13 +92,14 @@ def _get_geometry_output(args: NominatimArgs) -> napi.GeometryFormat: raise UsageError(f"Unknown polygon output format '{args.polygon_output}'.") from exp -def _get_locales(args: NominatimArgs, default: Optional[str]) -> napi.Locales: +def _get_locales(args: NominatimArgs, config: Configuration) -> napi.Locales: """ Get the locales from the language parameter. """ - if args.lang: - return napi.Locales.from_accept_languages(args.lang) - if default: - return napi.Locales.from_accept_languages(default) + language = args.lang or config.DEFAULT_LANGUAGE + output_names = config.OUTPUT_NAMES + + if language: + return napi.Locales.from_accept_languages(language, output_names) return napi.Locales() @@ -214,7 +216,7 @@ class APISearch: except napi.UsageError as ex: raise UsageError(ex) from ex - locales = _get_locales(args, api.config.DEFAULT_LANGUAGE) + locales = _get_locales(args, api.config) locales.localize_results(results) if args.dedupe and len(results) > 1: @@ -287,7 +289,7 @@ class APIReverse: raise UsageError(ex) from ex if result is not None: - locales = _get_locales(args, api.config.DEFAULT_LANGUAGE) + locales = _get_locales(args, api.config) locales.localize_results([result]) if args.format == 'debug': @@ -352,7 +354,7 @@ class APILookup: except napi.UsageError as ex: raise UsageError(ex) from ex - locales = _get_locales(args, api.config.DEFAULT_LANGUAGE) + locales = _get_locales(args, api.config) locales.localize_results(results) if args.format == 'debug': @@ -452,7 +454,7 @@ class APIDetails: raise UsageError(ex) from ex if result is not None: - locales = _get_locales(args, api.config.DEFAULT_LANGUAGE) + locales = _get_locales(args, api.config) locales.localize_results([result]) if args.format == 'debug': diff --git a/src/nominatim_db/clicmd/export.py b/src/nominatim_db/clicmd/export.py index 7bf17d2b..c05eac88 100644 --- a/src/nominatim_db/clicmd/export.py +++ b/src/nominatim_db/clicmd/export.py @@ -154,7 +154,7 @@ async def dump_results(conn: napi.SearchConnection, await add_result_details(conn, results, LookupDetails(address_details=True)) - locale = napi.Locales([lang] if lang else None) + locale = napi.Locales([lang] if lang else None, conn.config.OUTPUT_NAMES) locale.localize_results(results) for result in results: diff --git a/test/python/api/test_localization.py b/test/python/api/test_localization.py index c3e02596..6901ee72 100644 --- a/test/python/api/test_localization.py +++ b/test/python/api/test_localization.py @@ -31,20 +31,15 @@ def test_output_names_none_localized(): loc = Locales() expected_tags = [ - 'name', '_place_name', 'brand', '_place_brand', 'official_name', '_place_official_name', - 'short_name', '_place_short_name', 'ref', '_place_ref' + 'name', '_place_name' ] assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_none_localized_and_custom_output_names(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name:XX,entrance:XX,name,brand,test_tag,' - 'official_name:XX,short_name:XX,alt_name:XX' - ) - loc = Locales() +def test_output_names_none_localized_and_custom_output_names(): + loc = Locales(names='name:XX,entrance:XX,name,brand,test_tag,' + 'official_name:XX,short_name:XX,alt_name:XX') expected_tags = [ 'name', '_place_name', 'brand', '_place_brand', 'test_tag', '_place_test_tag' @@ -53,13 +48,9 @@ def test_output_names_none_localized_and_custom_output_names(monkeypatch): assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_none_localized_and_custom_output_names_more_than_two_changes(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name:XX,brand,test_tag:XX,official_name,short_name:XX,' - 'alt_name,another_tag_with:XX,another_tag_withoutXX' - ) - loc = Locales() +def test_output_names_none_localized_and_custom_output_names_more_than_two_changes(): + loc = Locales(names='name:XX,brand,test_tag:XX,official_name,short_name:XX,' + 'alt_name,another_tag_with:XX,another_tag_withoutXX') expected_tags = [ 'brand', '_place_brand', 'official_name', '_place_official_name', 'alt_name', @@ -69,12 +60,8 @@ def test_output_names_none_localized_and_custom_output_names_more_than_two_chang assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_none_localized_and_custom_output_names_including_space(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name:XX,name ,short_name:XX, short_name' - ) - loc = Locales() +def test_output_names_none_localized_and_custom_output_names_including_space(): + loc = Locales(names='name:XX,name ,short_name:XX, short_name') expected_tags = [ 'name', '_place_name', 'short_name', '_place_short_name' @@ -95,22 +82,32 @@ def test_output_names_localized(): loc = Locales(['en', 'es']) expected_tags = [ - 'name:en', '_place_name:en', 'name:es', '_place_name:es', 'name', '_place_name', 'brand', - '_place_brand', 'official_name:en', '_place_official_name:en', 'official_name:es', - '_place_official_name:es', 'short_name:en', '_place_short_name:en', 'short_name:es', - '_place_short_name:es', 'official_name', '_place_official_name', 'short_name', - '_place_short_name', 'ref', '_place_ref' + 'name:en', '_place_name:en', 'name:es', '_place_name:es', 'name', '_place_name' ] assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_localized_and_custom_output_names_including_space(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name:XX,name ,short_name:XX, short_name' - ) - loc = Locales(['en', 'es']) +def test_output_names_localized_and_empty_names(): + loc = Locales(['en'], "") + expected_tags = [] + + assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' + + +def test_output_names_localized_and_custom_names_ordering_logic(): + loc = Locales(['en', 'fr'], "name:XX,ref") + + expected = [ + 'name:en', '_place_name:en', + 'name:fr', '_place_name:fr', + 'ref', '_place_ref' + ] + assert loc.name_tags == expected + + +def test_output_names_localized_and_custom_output_names_including_space(): + loc = Locales(['en', 'es'], names='name:XX,name ,short_name:XX, short_name') expected_tags = [ 'name:en', '_place_name:en', 'name:es', '_place_name:es', @@ -122,12 +119,9 @@ def test_output_names_localized_and_custom_output_names_including_space(monkeypa assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_localized_and_custom_output_names(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name:XX,entrance:XX,name,brand,test_tag,official_name:XX,short_name:XX,alt_name:XX' - ) - loc = Locales(['en', 'es']) +def test_output_names_localized_and_custom_output_names(): + loc = Locales(['en', 'es'], names='name:XX,entrance:XX,name,brand,test_tag,' + 'official_name:XX,short_name:XX,alt_name:XX') expected_tags = [ 'name:en', '_place_name:en', 'name:es', '_place_name:es', 'entrance:en', @@ -141,12 +135,9 @@ def test_output_names_localized_and_custom_output_names(monkeypatch): assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_localized_and_custom_output_names_start_with_tag_that_has_no_XX(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name,brand,test_tag,official_name:XX,short_name:XX,alt_name:XX' - ) - loc = Locales(['en', 'es']) +def test_output_names_localized_and_custom_output_names_start_with_tag_that_has_no_XX(): + loc = Locales(['en', 'es'], + names='name,brand,test_tag,official_name:XX,short_name:XX,alt_name:XX') expected_tags = [ 'name', '_place_name', 'brand', '_place_brand', 'test_tag', '_place_test_tag', @@ -159,12 +150,8 @@ def test_output_names_localized_and_custom_output_names_start_with_tag_that_has_ assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_localized_and_custom_output_names_no_named_tags(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name,brand,test_tag' - ) - loc = Locales(['en', 'es']) +def test_output_names_localized_and_custom_output_names_no_named_tags(): + loc = Locales(['en', 'es'], names='name,brand,test_tag') expected_tags = [ 'name', '_place_name', 'brand', '_place_brand', 'test_tag', '_place_test_tag' @@ -173,12 +160,9 @@ def test_output_names_localized_and_custom_output_names_no_named_tags(monkeypatc assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_localized_and_custom_output_names_only_named_tags(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name:XX,entrance:XX,official_name:XX,short_name:XX,alt_name:XX' - ) - loc = Locales(['en', 'es']) +def test_output_names_localized_and_custom_output_names_only_named_tags(): + loc = Locales(['en', 'es'], + names='name:XX,entrance:XX,official_name:XX,short_name:XX,alt_name:XX') expected_tags = [ 'name:en', '_place_name:en', 'name:es', '_place_name:es', 'entrance:en', @@ -191,13 +175,9 @@ def test_output_names_localized_and_custom_output_names_only_named_tags(monkeypa assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_localized_and_custom_output_names_more_than_two_changes(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name:XX,brand,test_tag:XX,official_name,short_name:XX,' - 'alt_name,another_tag_with:XX,another_tag_withoutXX' - ) - loc = Locales(['en', 'es']) +def test_output_names_localized_and_custom_output_names_more_than_two_changes(): + loc = Locales(['en', 'es'], names='name:XX,brand,test_tag:XX,official_name,' + 'short_name:XX,alt_name,another_tag_with:XX,another_tag_withoutXX') expected_tags = [ 'name:en', '_place_name:en', 'name:es', '_place_name:es', 'brand', '_place_brand', @@ -211,13 +191,9 @@ def test_output_names_localized_and_custom_output_names_more_than_two_changes(mo assert loc.name_tags == expected_tags, f'Expected {expected_tags}, but got {loc.name_tags}' -def test_output_names_localized_and_custom_output_names_XX_in_the_middle(monkeypatch): - monkeypatch.setenv( - 'NOMINATIM_OUTPUT_NAMES', - 'name:XX,br:XXand,test_tag:XX,official_name,sh:XXort_name:XX,' - 'alt_name,another_tag_with:XX,another_tag_withoutXX' - ) - loc = Locales(['en', 'es']) +def test_output_names_localized_and_custom_output_names_XX_in_the_middle(): + loc = Locales(['en', 'es'], names='name:XX,br:XXand,test_tag:XX,official_name,' + 'sh:XXort_name:XX,alt_name,another_tag_with:XX,another_tag_withoutXX') expected_tags = [ 'name:en', '_place_name:en', 'name:es', '_place_name:es', 'br:XXand', '_place_br:XXand',