]> git.openstreetmap.org Git - nominatim.git/blob - src/nominatim_api/localization.py
release 5.2.0.post7
[nominatim.git] / src / nominatim_api / localization.py
1 # SPDX-License-Identifier: GPL-3.0-or-later
2 #
3 # This file is part of Nominatim. (https://nominatim.org)
4 #
5 # Copyright (C) 2024 by the Nominatim developer community.
6 # For a full list of authors see the git log.
7 """
8 Helper functions for localizing names of results.
9 """
10 from typing import Mapping, List, Optional
11 from .results import AddressLines, BaseResultT
12
13 import re
14
15
16 class Locales:
17     """ Helper class for localization of names.
18
19         It takes a list of language prefixes in their order of preferred
20         usage and comma separated name keys (Configuration.OUTPUT_NAMES).
21     """
22
23     def __init__(self, langs: Optional[List[str]] = None,
24                  names: str = 'name:XX,name') -> None:
25         self.languages = langs or []
26         self.name_tags: List[str] = []
27
28         parts = names.split(',') if names else []
29
30         for part in parts:
31             part = part.strip()
32             if part.endswith(":XX"):
33                 self._add_lang_tags(part[:-3])
34             else:
35                 self._add_tags(part)
36
37     def __bool__(self) -> bool:
38         return len(self.languages) > 0
39
40     def _add_tags(self, *tags: str) -> None:
41         for tag in tags:
42             self.name_tags.append(tag)
43             self.name_tags.append(f"_place_{tag}")
44
45     def _add_lang_tags(self, *tags: str) -> None:
46         for tag in tags:
47             for lang in self.languages:
48                 self.name_tags.append(f"{tag}:{lang}")
49                 self.name_tags.append(f"_place_{tag}:{lang}")
50
51     def display_name(self, names: Optional[Mapping[str, str]]) -> str:
52         """ Return the best matching name from a dictionary of names
53             containing different name variants.
54
55             If 'names' is null or empty, an empty string is returned. If no
56             appropriate localization is found, the first name is returned.
57         """
58         if not names:
59             return ''
60
61         if len(names) > 1:
62             for tag in self.name_tags:
63                 if tag in names:
64                     return names[tag]
65
66         # Nothing? Return any of the other names as a default.
67         return next(iter(names.values()))
68
69     @staticmethod
70     def from_accept_languages(langstr: str, names: str = 'name:XX,name') -> 'Locales':
71         """ Create a localization object from a language list in the
72             format of HTTP accept-languages header.
73
74             The functions tries to be forgiving of format errors by first splitting
75             the string into comma-separated parts and then parsing each
76             description separately. Badly formatted parts are then ignored.
77         """
78         # split string into languages
79         candidates = []
80         for desc in langstr.split(','):
81             m = re.fullmatch(r'\s*([a-z_-]+)(?:;\s*q\s*=\s*([01](?:\.\d+)?))?\s*',
82                              desc, flags=re.I)
83             if m:
84                 candidates.append((m[1], float(m[2] or 1.0)))
85
86         # sort the results by the weight of each language (preserving order).
87         candidates.sort(reverse=True, key=lambda e: e[1])
88
89         # If a language has a region variant, also add the language without
90         # variant but only if it isn't already in the list to not mess up the weight.
91         languages = []
92         for lid, _ in candidates:
93             languages.append(lid)
94             parts = lid.split('-', 1)
95             if len(parts) > 1 and all(c[0] != parts[0] for c in candidates):
96                 languages.append(parts[0])
97
98         return Locales(languages, names)
99
100     def localize(self, lines: AddressLines) -> None:
101         """ Sets the local name of address parts according to the chosen
102             locale.
103
104             Only address parts that are marked as isaddress are localized.
105
106             AddressLines should be modified in place.
107         """
108         for line in lines:
109             if line.isaddress and line.names:
110                 line.local_name = self.display_name(line.names)
111
112     def localize_results(self, results: List[BaseResultT]) -> None:
113         """ Set the local name of results according to the chosen
114             locale.
115         """
116         for result in results:
117             result.locale_name = self.display_name(result.names)
118             if result.address_rows:
119                 self.localize(result.address_rows)