Skip to content

Commit ad6346a

Browse files
authored
Merge pull request #2339 from geopython/openapi-i18n
ensure OpenAPI document metadata supports i18n
2 parents 9edce20 + d960d56 commit ad6346a

4 files changed

Lines changed: 243 additions & 24 deletions

File tree

pygeoapi/api/maps.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,6 @@ def get_oas_30(cfg: dict, locale: str) -> tuple[list[dict[str, str]], dict[str,
385385
'operationId': 'getMap',
386386
'parameters': [
387387
{'$ref': '#/components/parameters/bbox'},
388-
{'$ref': f"{OPENAPI_YAML['oapif-1']}#/components/parameters/datetime"}, # noqa
389388
{'$ref': f"{OPENAPI_YAML['oamaps']}#/components/parameters/subset"}, # noqa
390389
{
391390
'name': 'width',

pygeoapi/openapi.py

Lines changed: 36 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -134,12 +134,13 @@ def gen_response_object(description: str, media_type: str,
134134
return response
135135

136136

137-
def gen_contact(cfg: dict) -> dict:
137+
def gen_contact(cfg: dict, locale_: str) -> dict:
138138
"""
139139
Generates an OpenAPI contact object with OGC extensions
140140
based on OGC API - Records contact
141141
142142
:param cfg: `dict` of configuration
143+
:param locale_: `str` of locale
143144
144145
:returns: `dict` of OpenAPI contact object
145146
"""
@@ -148,40 +149,47 @@ def gen_contact(cfg: dict) -> dict:
148149
has_phones = False
149150

150151
contact = {
151-
'name': cfg['metadata']['provider']['name']
152+
'name': l10n.translate(cfg['metadata']['provider']['name'], locale_)
152153
}
153154

154155
for key in ['url', 'email']:
155156
if key in cfg['metadata']['provider']:
156-
contact[key] = cfg['metadata']['provider'][key]
157+
contact[key] = l10n.translate(cfg['metadata']['provider'][key],
158+
locale_)
157159

158160
contact['x-ogc-serviceContact'] = {
159-
'name': cfg['metadata']['contact']['name'],
161+
'name': l10n.translate(cfg['metadata']['contact']['name'], locale_),
160162
'addresses': []
161163
}
162164

163165
if 'position' in cfg['metadata']['contact']:
164-
contact['x-ogc-serviceContact']['position'] = cfg['metadata']['contact']['position'] # noqa
166+
contact['x-ogc-serviceContact']['position'] = l10n.translate(
167+
cfg['metadata']['contact']['position'], locale_)
165168

166169
if any(address in ['address', 'city', 'stateorprovince', 'postalcode', 'country'] for address in cfg['metadata']['contact']): # noqa
167170
has_addresses = True
168171

169172
if has_addresses:
170173
address = {}
171174
if 'address' in cfg['metadata']['contact']:
172-
address['deliveryPoint'] = [cfg['metadata']['contact']['address']]
175+
address['deliveryPoint'] = [l10n.translate(
176+
cfg['metadata']['contact']['address'], locale_)]
173177

174178
if 'city' in cfg['metadata']['contact']:
175-
address['city'] = cfg['metadata']['contact']['city']
179+
address['city'] = l10n.translate(
180+
cfg['metadata']['contact']['city'], locale_)
176181

177182
if 'stateorprovince' in cfg['metadata']['contact']:
178-
address['administrativeArea'] = cfg['metadata']['contact']['stateorprovince'] # noqa
183+
address['administrativeArea'] = l10n.translate(
184+
cfg['metadata']['contact']['stateorprovince'], locale_)
179185

180186
if 'postalCode' in cfg['metadata']['contact']:
181-
address['administrativeArea'] = cfg['metadata']['contact']['postalCode'] # noqa
187+
address['administrativeArea'] = l10n.translate(
188+
cfg['metadata']['contact']['postalCode'], locale_)
182189

183190
if 'country' in cfg['metadata']['contact']:
184-
address['administrativeArea'] = cfg['metadata']['contact']['country'] # noqa
191+
address['administrativeArea'] = l10n.translate(
192+
cfg['metadata']['contact']['country'], locale_)
185193

186194
contact['x-ogc-serviceContact']['addresses'].append(address)
187195

@@ -192,33 +200,39 @@ def gen_contact(cfg: dict) -> dict:
192200
if has_phones:
193201
if 'phone' in cfg['metadata']['contact']:
194202
contact['x-ogc-serviceContact']['phones'].append({
195-
'type': 'main', 'value': cfg['metadata']['contact']['phone']
203+
'type': 'main', 'value': l10n.translate(
204+
cfg['metadata']['contact']['phone'], locale_)
196205
})
197206

198207
if 'fax' in cfg['metadata']['contact']:
199208
contact['x-ogc-serviceContact']['phones'].append({
200-
'type': 'fax', 'value': cfg['metadata']['contact']['fax']
209+
'type': 'fax', 'value': l10n.translate(
210+
cfg['metadata']['contact']['fax'], locale_)
201211
})
202212

203213
if 'email' in cfg['metadata']['contact']:
204214
contact['x-ogc-serviceContact']['emails'] = [{
205-
'value': cfg['metadata']['contact']['email']
215+
'value': l10n.translate(
216+
cfg['metadata']['contact']['email'], locale_)
206217
}]
207218

208219
if 'url' in cfg['metadata']['contact']:
209220
contact['x-ogc-serviceContact']['links'] = [{
210221
'type': 'text/html',
211-
'href': cfg['metadata']['contact']['url']
222+
'href': l10n.translate(cfg['metadata']['contact']['url'], locale_)
212223
}]
213224

214225
if 'instructions' in cfg['metadata']['contact']:
215-
contact['x-ogc-serviceContact']['contactInstructions'] = cfg['metadata']['contact']['instructions'] # noqa
226+
contact['x-ogc-serviceContact']['contactInstructions'] = l10n.translate( # noqa
227+
cfg['metadata']['contact']['instructions'], locale_)
216228

217229
if 'hours' in cfg['metadata']['contact']:
218-
contact['x-ogc-serviceContact']['hoursOfService'] = cfg['metadata']['contact']['hours'] # noqa
230+
contact['x-ogc-serviceContact']['hoursOfService'] = l10n.translate(
231+
cfg['metadata']['contact']['hours'], locale_)
219232

220233
if 'role' in cfg['metadata']['contact']:
221-
contact['x-ogc-serviceContact']['hoursOfService'] = cfg['metadata']['contact']['role'] # noqa
234+
contact['x-ogc-serviceContact']['hoursOfService'] = l10n.translate(
235+
cfg['metadata']['contact']['role'], locale_)
222236

223237
return contact
224238

@@ -255,11 +269,11 @@ def get_oas_30(cfg: dict, fail_on_invalid_collection: bool = True) -> dict:
255269
'description': l10n.translate(cfg['metadata']['identification']['description'], locale_), # noqa
256270
'x-keywords': l10n.translate(cfg['metadata']['identification']['keywords'], locale_), # noqa
257271
'termsOfService':
258-
cfg['metadata']['identification']['terms_of_service'],
259-
'contact': gen_contact(cfg),
272+
l10n.translate(cfg['metadata']['identification']['terms_of_service'], locale_), # noqa
273+
'contact': gen_contact(cfg, locale_),
260274
'license': {
261-
'name': cfg['metadata']['license']['name'],
262-
'url': cfg['metadata']['license']['url']
275+
'name': l10n.translate(cfg['metadata']['license']['name'], locale_), # noqa
276+
'url': l10n.translate(cfg['metadata']['license']['url'], locale_)
263277
},
264278
'version': api_rules.api_version
265279
}
@@ -360,7 +374,7 @@ def get_oas_30(cfg: dict, fail_on_invalid_collection: bool = True) -> dict:
360374
'description': l10n.translate(cfg['metadata']['identification']['description'], locale_), # noqa
361375
'externalDocs': {
362376
'description': 'information',
363-
'url': cfg['metadata']['identification']['url']}
377+
'url': l10n.translate(cfg['metadata']['identification']['url'], locale_)} # noqa
364378
}
365379
)
366380

tests/other/test_openapi.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,20 @@ def config_hidden_resources():
5858
return yaml_load(fh)
5959

6060

61+
@pytest.fixture()
62+
def config_i18n():
63+
filename = 'pygeoapi-test-config-i18n.yml'
64+
with open(get_test_file_path(filename)) as fh:
65+
return yaml_load(fh)
66+
67+
6168
@pytest.fixture()
6269
def openapi():
6370
with open(get_test_file_path('pygeoapi-test-openapi.yml')) as fh:
6471
return yaml_load(fh)
6572

6673

67-
def test_str2bool():
74+
def test_get_ogc_schemas_location():
6875

6976
default = {
7077
'url': 'http://localhost:5000'
@@ -141,6 +148,13 @@ def test_hidden_resources(config_hidden_resources):
141148
assert '/collections/obs/items' not in openapi_doc['paths']
142149

143150

151+
def test_i18n(config_i18n):
152+
openapi_doc = get_oas(config_i18n)
153+
154+
assert isinstance(openapi_doc['info']['contact']['name'], str)
155+
assert openapi_doc['info']['contact']['name'] == 'Organization Name'
156+
157+
144158
def test_admin_empty_resources(config_admin_empty_resources):
145159
openapi_doc = get_oas(config_admin_empty_resources)
146160
assert '/admin/config' in openapi_doc['paths']
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# =================================================================
2+
#
3+
# Authors: Tom Kralidis <tomkralidis@gmail.com>
4+
#
5+
# Copyright (c) 2026 Tom Kralidis
6+
#
7+
# Permission is hereby granted, free of charge, to any person
8+
# obtaining a copy of this software and associated documentation
9+
# files (the "Software"), to deal in the Software without
10+
# restriction, including without limitation the rights to use,
11+
# copy, modify, merge, publish, distribute, sublicense, and/or sell
12+
# copies of the Software, and to permit persons to whom the
13+
# Software is furnished to do so, subject to the following
14+
# conditions:
15+
#
16+
# The above copyright notice and this permission notice shall be
17+
# included in all copies or substantial portions of the Software.
18+
#
19+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
21+
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
23+
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
24+
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
25+
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
26+
# OTHER DEALINGS IN THE SOFTWARE.
27+
#
28+
# =================================================================
29+
30+
server:
31+
bind:
32+
host: 0.0.0.0
33+
port: 5000
34+
url: http://localhost:5000/
35+
mimetype: application/json; charset=UTF-8
36+
encoding: utf-8
37+
gzip: false
38+
languages:
39+
# First language is the default language
40+
- en-US
41+
- fr-CA
42+
cors: true
43+
pretty_print: true
44+
limits:
45+
default_items: 10
46+
max_items: 10
47+
# templates: /path/to/templates
48+
map:
49+
url: https://tile.openstreetmap.org/{z}/{x}/{y}.png
50+
attribution: '&copy; <a href="https://openstreetmap.org/copyright">OpenStreetMap contributors</a>'
51+
manager:
52+
name: TinyDB
53+
connection: /tmp/pygeoapi-test-process-manager.db
54+
output_dir: /tmp
55+
56+
logging:
57+
level: DEBUG
58+
#logfile: /tmp/pygeoapi.log
59+
60+
metadata:
61+
identification:
62+
title:
63+
en: pygeoapi default instance
64+
fr: instance par défaut de pygeoapi
65+
description:
66+
en: pygeoapi provides an API to geospatial data
67+
fr: pygeoapi fournit une API aux données géospatiales
68+
keywords:
69+
en:
70+
- geospatial
71+
- data
72+
- api
73+
fr:
74+
- géospatiale
75+
- données
76+
- api
77+
keywords_type: theme
78+
terms_of_service: https://creativecommons.org/licenses/by/4.0/
79+
url: http://example.org
80+
license:
81+
name:
82+
en: CC-BY 4.0 license
83+
fr: license CC-BY 4.0 license
84+
url:
85+
en: https://creativecommons.org/licenses/by/4.0/
86+
fr: https://creativecommons.org/licenses/by/4.0/
87+
provider:
88+
name:
89+
en: Organization Name
90+
fr: nom d'organisation
91+
url:
92+
en: https://pygeoapi.io
93+
fr: https://pygeoapi.io
94+
contact:
95+
name:
96+
en: Lastname, Firstname
97+
fr: nom de famille, nom
98+
position:
99+
en: Position Title
100+
fr: titre du poste
101+
address:
102+
en: Mailing Address
103+
fr: address postale
104+
city:
105+
en: City
106+
fr: ville
107+
stateorprovince:
108+
en: Administrative Area
109+
fr: zone administrative
110+
postalcode:
111+
en: Zip or Postal Code
112+
fr: code postale
113+
country:
114+
en: Country
115+
fr: pays
116+
phone:
117+
en: +xx-xxx-xxx-xxxx
118+
fr: +xx-xxx-xxx-xxxx
119+
fax:
120+
en: +xx-xxx-xxx-xxxx
121+
fr: +xx-xxx-xxx-xxxx
122+
email: you@example.org
123+
url:
124+
en: Contact URL
125+
fr: URL de contact
126+
hours:
127+
en: Hours of Service
128+
fr: heures de service
129+
instructions:
130+
en: During hours of service. Off on weekends.
131+
fr: pendant les heures de service. Fermé le week-end.
132+
role: pointOfContact
133+
134+
resources:
135+
obs:
136+
type: collection
137+
title:
138+
en: Observations
139+
fr: Observations
140+
description:
141+
en: My cool observations
142+
fr: Mes belles observations
143+
keywords:
144+
- observations
145+
- monitoring
146+
links:
147+
- type: text/csv
148+
rel: canonical
149+
title: data
150+
href: https://github.com/mapserver/mapserver/blob/branch-7-0/msautotest/wxs/data/obs.csv
151+
hreflang: en-US
152+
- type: text/csv
153+
rel: alternate
154+
title: data
155+
href: https://raw.githubusercontent.com/mapserver/mapserver/branch-7-0/msautotest/wxs/data/obs.csv
156+
hreflang: en-US
157+
linked-data:
158+
context:
159+
- schema: https://schema.org/
160+
stn_id:
161+
"@id": schema:identifier
162+
"@type": schema:Text
163+
datetime:
164+
"@type": schema:DateTime
165+
"@id": schema:observationDate
166+
value:
167+
"@type": schema:Number
168+
"@id": schema:QuantitativeValue
169+
extents:
170+
spatial:
171+
bbox: [-180,-90,180,90]
172+
crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
173+
temporal:
174+
begin: 2000-10-30T18:24:39Z
175+
end: 2007-10-30T08:57:29Z
176+
trs: http://www.opengis.net/def/uom/ISO-8601/0/Gregorian
177+
resolution: P1D
178+
default: 2000-10-30T18:24:39Z
179+
providers:
180+
- type: feature
181+
name: CSV
182+
data: tests/data/obs.csv
183+
crs:
184+
- http://www.opengis.net/def/crs/OGC/1.3/CRS84
185+
- http://www.opengis.net/def/crs/EPSG/0/4326
186+
- http://www.opengis.net/def/crs/EPSG/0/3857
187+
- http://www.opengis.net/def/crs/EPSG/0/28992
188+
storage_crs: http://www.opengis.net/def/crs/OGC/1.3/CRS84
189+
id_field: id
190+
geometry:
191+
x_field: long
192+
y_field: lat

0 commit comments

Comments
 (0)