feat(easel): implement multiple weather providers
This commit is contained in:
@@ -2,4 +2,6 @@ DOCKER_REPO=git.shmyga.ru
|
|||||||
DOCKER_GROUP=infernalgames
|
DOCKER_GROUP=infernalgames
|
||||||
DOCKER_ROOT="$DOCKER_REPO/$DOCKER_GROUP"
|
DOCKER_ROOT="$DOCKER_REPO/$DOCKER_GROUP"
|
||||||
VERSION=$(grep -m 1 'version' ./pyproject.toml | grep -oP 'version\s*=\s*"\K[^"]+')
|
VERSION=$(grep -m 1 'version' ./pyproject.toml | grep -oP 'version\s*=\s*"\K[^"]+')
|
||||||
DOCKER_PROJECTS=("gallery")
|
DOCKER_PROJECTS=("gallery")
|
||||||
|
|
||||||
|
OPENWEATHER_KEY="<EMPTY>"
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,4 +4,5 @@
|
|||||||
.venv
|
.venv
|
||||||
#.vscode
|
#.vscode
|
||||||
static/node_modules
|
static/node_modules
|
||||||
static/dist
|
static/dist
|
||||||
|
.env
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
"python.testing.pytestArgs": ["tests", "-s"],
|
"python.testing.pytestArgs": ["tests", "-s"],
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
|
"python.terminal.useEnvFile": true,
|
||||||
"python-envs.pythonProjects": [
|
"python-envs.pythonProjects": [
|
||||||
{
|
{
|
||||||
"path": ".",
|
"path": ".",
|
||||||
|
|||||||
@@ -50,7 +50,13 @@ def build_weather_response(request: AppRequest, response: WeatherResponse):
|
|||||||
router = APIRouter(prefix="/weather")
|
router = APIRouter(prefix="/weather")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_class=HTMLResponse)
|
@router.get("/", response_class=RedirectResponse)
|
||||||
|
async def get_weather(request: AppRequest):
|
||||||
|
default_provider = request.app.state.api.get_api_providers(WeatherApi.TYPE)[0]
|
||||||
|
return RedirectResponse(default_provider)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{provider}", response_class=HTMLResponse)
|
||||||
async def get_weather_index(request: AppRequest, weather_api: WeatherApiDepends, query: str | None = None):
|
async def get_weather_index(request: AppRequest, weather_api: WeatherApiDepends, query: str | None = None):
|
||||||
locations = (await weather_api.find_locations(query)) if query else []
|
locations = (await weather_api.find_locations(query)) if query else []
|
||||||
return templates.TemplateResponse(
|
return templates.TemplateResponse(
|
||||||
@@ -62,12 +68,12 @@ async def get_weather_index(request: AppRequest, weather_api: WeatherApiDepends,
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{location}", response_class=RedirectResponse)
|
@router.get("/{provider}/{location}", response_class=RedirectResponse)
|
||||||
async def get_weather_default(location: str):
|
async def get_weather_default(location: str):
|
||||||
return RedirectResponse(f"{location}/tag/today")
|
return RedirectResponse(f"{location}/tag/today")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{location}/day/{date}", response_class=HTMLResponse)
|
@router.get("/{provider}/{location}/day/{date}", response_class=HTMLResponse)
|
||||||
async def get_weather_day(
|
async def get_weather_day(
|
||||||
request: AppRequest,
|
request: AppRequest,
|
||||||
weather_api: WeatherApiDepends,
|
weather_api: WeatherApiDepends,
|
||||||
@@ -78,13 +84,13 @@ async def get_weather_day(
|
|||||||
return build_weather_response(request, response)
|
return build_weather_response(request, response)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{location}/days/{days}", response_class=HTMLResponse)
|
@router.get("/{provider}/{location}/days/{days}", response_class=HTMLResponse)
|
||||||
async def get_weather_days(request: AppRequest, weather_api: WeatherApiDepends, location: str, days: int):
|
async def get_weather_days(request: AppRequest, weather_api: WeatherApiDepends, location: str, days: int):
|
||||||
response = await weather_api.get_days(location, days)
|
response = await weather_api.get_days(location, days)
|
||||||
return build_weather_response(request, response)
|
return build_weather_response(request, response)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{location}/tag/{tag}", response_class=HTMLResponse)
|
@router.get("/{provider}/{location}/tag/{tag}", response_class=HTMLResponse)
|
||||||
async def get_weather_tag(request: AppRequest, weather_api: WeatherApiDepends, location: str, tag: str):
|
async def get_weather_tag(request: AppRequest, weather_api: WeatherApiDepends, location: str, tag: str):
|
||||||
tag_value = TagUtil.parse_tag(tag)
|
tag_value = TagUtil.parse_tag(tag)
|
||||||
if tag_value.type == TagType.DAY:
|
if tag_value.type == TagType.DAY:
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
<ul id="locations"
|
<ul id="locations"
|
||||||
class="list-group mb-5">
|
class="list-group mb-5">
|
||||||
{% for location in locations %}
|
{% for location in locations %}
|
||||||
<a href="{{location.id}}"
|
<a href="{{ url_for('get_weather_default', provider=request.path_params.provider, location=location.id) }}"
|
||||||
class="list-group-item list-group-item-action px-4"
|
class="list-group-item list-group-item-action px-4"
|
||||||
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
|
onclick="saveLocation({id:'{{location.id}}', name:'{{location.name}}'});">
|
||||||
<span class="fi fi-{{location.country_code}} me-1"></span>
|
<span class="fi fi-{{location.country_code}} me-1"></span>
|
||||||
@@ -50,13 +50,20 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
|
const getStorageKey = () => {
|
||||||
|
const provider = window.location.pathname.split('/').pop();
|
||||||
|
return `locations:${provider}`;
|
||||||
|
}
|
||||||
|
|
||||||
document.loadLocations = () => {
|
document.loadLocations = () => {
|
||||||
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
|
const provider = window.location.pathname.split('/').pop();
|
||||||
|
console.log('!', provider);
|
||||||
|
const locations = JSON.parse(window.localStorage.getItem(getStorageKey()) || '{}');
|
||||||
const container = document.querySelector('#locations');
|
const container = document.querySelector('#locations');
|
||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
for (const [id, name] of Object.entries(locations)) {
|
for (const [id, name] of Object.entries(locations)) {
|
||||||
const element = document.createElement('a');
|
const element = document.createElement('a');
|
||||||
element.href = `${id}`;
|
element.href = `${window.location.pathname}/${id}`;
|
||||||
element.className = 'list-group-item list-group-item-action px-4 d-flex justify-content-between align-items-start';
|
element.className = 'list-group-item list-group-item-action px-4 d-flex justify-content-between align-items-start';
|
||||||
element.innerHTML = `
|
element.innerHTML = `
|
||||||
<span class="text-primary me-auto">${name}</span>
|
<span class="text-primary me-auto">${name}</span>
|
||||||
@@ -67,15 +74,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.saveLocation = (location) => {
|
document.saveLocation = (location) => {
|
||||||
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
|
const locations = JSON.parse(window.localStorage.getItem(getStorageKey()) || '{}');
|
||||||
locations[location.id] = location.name;
|
locations[location.id] = location.name;
|
||||||
window.localStorage.setItem('locations', JSON.stringify(locations));
|
window.localStorage.setItem(getStorageKey(), JSON.stringify(locations));
|
||||||
}
|
}
|
||||||
|
|
||||||
document.removeLocation = (id) => {
|
document.removeLocation = (id) => {
|
||||||
const locations = JSON.parse(window.localStorage.getItem('locations') || '{}');
|
const locations = JSON.parse(window.localStorage.getItem(getStorageKey()) || '{}');
|
||||||
delete locations[id];
|
delete locations[id];
|
||||||
window.localStorage.setItem('locations', JSON.stringify(locations));
|
window.localStorage.setItem(getStorageKey(), JSON.stringify(locations));
|
||||||
document.loadLocations();
|
document.loadLocations();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -89,7 +89,9 @@ class GismeteoApi(WeatherApi):
|
|||||||
lon=item["coordinates"]["longitude"],
|
lon=item["coordinates"]["longitude"],
|
||||||
country=item["translations"]["kk"]["country"]["name"],
|
country=item["translations"]["kk"]["country"]["name"],
|
||||||
country_code=item["country"]["code"].lower(),
|
country_code=item["country"]["code"].lower(),
|
||||||
district=item["translations"]["kk"]["district"]["name"],
|
district=(
|
||||||
|
item["translations"]["kk"]["district"]["name"] if item["translations"]["kk"]["district"] else ""
|
||||||
|
),
|
||||||
subdistrict=(
|
subdistrict=(
|
||||||
item["translations"]["kk"]["subdistrict"]["name"]
|
item["translations"]["kk"]["subdistrict"]["name"]
|
||||||
if "subdistrict" in item["translations"]["kk"]
|
if "subdistrict" in item["translations"]["kk"]
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import datetime
|
import datetime
|
||||||
import logging
|
import logging
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from os import environ
|
||||||
|
|
||||||
from aiocache import cached
|
from aiocache import cached
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ logger = logging.getLogger("openweather")
|
|||||||
|
|
||||||
class OpenWeatherApi(WeatherApi):
|
class OpenWeatherApi(WeatherApi):
|
||||||
PROVIDER = "openweather"
|
PROVIDER = "openweather"
|
||||||
SOURCE = OpenWeather("517a6bccceaa1c48127f6199ec3fb7cf")
|
SOURCE = OpenWeather(environ["OPENWEATHER_KEY"])
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _parse_location(cls, location_id: str) -> tuple[float, float]:
|
def _parse_location(cls, location_id: str) -> tuple[float, float]:
|
||||||
@@ -32,7 +33,20 @@ class OpenWeatherApi(WeatherApi):
|
|||||||
return await self.SOURCE.get_forecast(*self._parse_location(location_id))
|
return await self.SOURCE.get_forecast(*self._parse_location(location_id))
|
||||||
|
|
||||||
async def find_locations(self, query: str) -> list[Location]:
|
async def find_locations(self, query: str) -> list[Location]:
|
||||||
raise NotImplementedError
|
result = await self.SOURCE.get_locations(query)
|
||||||
|
return [
|
||||||
|
Location(
|
||||||
|
id=f"{item.lat}:{item.lon}",
|
||||||
|
name=item.name,
|
||||||
|
lat=item.lat,
|
||||||
|
lon=item.lon,
|
||||||
|
country=item.country,
|
||||||
|
country_code=item.country.lower(),
|
||||||
|
district=item.state,
|
||||||
|
subdistrict="",
|
||||||
|
)
|
||||||
|
for item in result
|
||||||
|
]
|
||||||
|
|
||||||
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
async def get_day(self, location_id: str, date: datetime.date) -> WeatherResponse:
|
||||||
data: Forecast = await self._get_location_forecast(location_id)
|
data: Forecast = await self._get_location_forecast(location_id)
|
||||||
|
|||||||
@@ -67,6 +67,15 @@ class Forecast(Model):
|
|||||||
list: list[ForecastItem]
|
list: list[ForecastItem]
|
||||||
|
|
||||||
|
|
||||||
|
class Location(Model):
|
||||||
|
name: str
|
||||||
|
local_names: dict[str, str]
|
||||||
|
lat: float
|
||||||
|
lon: float
|
||||||
|
country: str
|
||||||
|
state: str
|
||||||
|
|
||||||
|
|
||||||
class OpenWeather:
|
class OpenWeather:
|
||||||
BASE_URL = "https://api.openweathermap.org"
|
BASE_URL = "https://api.openweathermap.org"
|
||||||
|
|
||||||
@@ -78,4 +87,10 @@ class OpenWeather:
|
|||||||
endpoint = f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric"
|
endpoint = f"data/2.5/forecast?lat={lat}&lon={lon}&appid={self._api_key}&units=metric"
|
||||||
response = await self._source.request(endpoint)
|
response = await self._source.request(endpoint)
|
||||||
response_data = json.loads(response)
|
response_data = json.loads(response)
|
||||||
return Forecast(**response_data)
|
return Forecast.model_validate(response_data)
|
||||||
|
|
||||||
|
async def get_locations(self, query: str, limit: int = 5) -> list[Location]:
|
||||||
|
endpoint = f"geo/1.0/direct?q={query}&limit={limit}&appid={self._api_key}"
|
||||||
|
response = await self._source.request(endpoint)
|
||||||
|
response_data = json.loads(response)
|
||||||
|
return [Location.model_validate(item) for item in response_data]
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ class ApiBundle:
|
|||||||
if isinstance(value, api_type):
|
if isinstance(value, api_type):
|
||||||
if provider is None or provider == value.provider:
|
if provider is None or provider == value.provider:
|
||||||
return value
|
return value
|
||||||
raise ValueError(api_type)
|
raise ValueError(api_type, provider)
|
||||||
|
|
||||||
def get_weather(self, provider: str | None = None) -> WeatherApi:
|
def get_weather(self, provider: str | None = None) -> WeatherApi:
|
||||||
return self.get_api(WeatherApi, provider)
|
return self.get_api(WeatherApi, provider)
|
||||||
|
|||||||
254
tests/data/openweather/locations.json
Normal file
254
tests/data/openweather/locations.json
Normal file
@@ -0,0 +1,254 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"name": "London",
|
||||||
|
"local_names": {
|
||||||
|
"fo": "London",
|
||||||
|
"nn": "London",
|
||||||
|
"tg": "Лондон",
|
||||||
|
"uk": "Лондон",
|
||||||
|
"lt": "Londonas",
|
||||||
|
"zu": "ILondon",
|
||||||
|
"eo": "Londono",
|
||||||
|
"os": "Лондон",
|
||||||
|
"pa": "ਲੰਡਨ",
|
||||||
|
"jv": "London",
|
||||||
|
"hu": "London",
|
||||||
|
"gd": "Lunnainn",
|
||||||
|
"hy": "Լոնդոն",
|
||||||
|
"to": "Lonitoni",
|
||||||
|
"sr": "Лондон",
|
||||||
|
"sv": "London",
|
||||||
|
"ku": "London",
|
||||||
|
"te": "లండన్",
|
||||||
|
"tw": "London",
|
||||||
|
"co": "Londra",
|
||||||
|
"eu": "Londres",
|
||||||
|
"et": "London",
|
||||||
|
"ca": "Londres",
|
||||||
|
"nl": "Londen",
|
||||||
|
"kl": "London",
|
||||||
|
"fi": "Lontoo",
|
||||||
|
"az": "London",
|
||||||
|
"mr": "लंडन",
|
||||||
|
"km": "ឡុងដ៍",
|
||||||
|
"af": "Londen",
|
||||||
|
"hi": "लंदन",
|
||||||
|
"gl": "Londres",
|
||||||
|
"ka": "ლონდონი",
|
||||||
|
"ff": "London",
|
||||||
|
"de": "London",
|
||||||
|
"sl": "London",
|
||||||
|
"th": "ลอนดอน",
|
||||||
|
"bn": "লন্ডন",
|
||||||
|
"pl": "Londyn",
|
||||||
|
"an": "Londres",
|
||||||
|
"bg": "Лондон",
|
||||||
|
"ht": "Lonn",
|
||||||
|
"so": "London",
|
||||||
|
"mn": "Лондон",
|
||||||
|
"id": "London",
|
||||||
|
"ko": "런던",
|
||||||
|
"yo": "Lọndọnu",
|
||||||
|
"sa": "लन्डन्",
|
||||||
|
"nv": "Tooh Dineʼé Bikin Haalʼá",
|
||||||
|
"br": "Londrez",
|
||||||
|
"rm": "Londra",
|
||||||
|
"ascii": "London",
|
||||||
|
"en": "London",
|
||||||
|
"ba": "Лондон",
|
||||||
|
"ga": "Londain",
|
||||||
|
"tr": "Londra",
|
||||||
|
"om": "Landan",
|
||||||
|
"bh": "लंदन",
|
||||||
|
"gu": "લંડન",
|
||||||
|
"ee": "London",
|
||||||
|
"se": "London",
|
||||||
|
"vi": "Luân Đôn",
|
||||||
|
"sk": "Londýn",
|
||||||
|
"lb": "London",
|
||||||
|
"cs": "Londýn",
|
||||||
|
"io": "London",
|
||||||
|
"ab": "Лондон",
|
||||||
|
"ug": "لوندۇن",
|
||||||
|
"ha": "Landan",
|
||||||
|
"wo": "Londar",
|
||||||
|
"bi": "London",
|
||||||
|
"st": "London",
|
||||||
|
"fj": "Lodoni",
|
||||||
|
"tt": "Лондон",
|
||||||
|
"mt": "Londra",
|
||||||
|
"vo": "London",
|
||||||
|
"bs": "London",
|
||||||
|
"or": "ଲଣ୍ଡନ",
|
||||||
|
"is": "London",
|
||||||
|
"tk": "London",
|
||||||
|
"he": "לונדון",
|
||||||
|
"ia": "London",
|
||||||
|
"kn": "ಲಂಡನ್",
|
||||||
|
"ie": "London",
|
||||||
|
"es": "Londres",
|
||||||
|
"my": "လန်ဒန်မြို့",
|
||||||
|
"sn": "London",
|
||||||
|
"zh": "伦敦",
|
||||||
|
"wa": "Londe",
|
||||||
|
"fy": "Londen",
|
||||||
|
"sq": "Londra",
|
||||||
|
"be": "Лондан",
|
||||||
|
"oc": "Londres",
|
||||||
|
"hr": "London",
|
||||||
|
"da": "London",
|
||||||
|
"bm": "London",
|
||||||
|
"ru": "Лондон",
|
||||||
|
"ne": "लन्डन",
|
||||||
|
"pt": "Londres",
|
||||||
|
"lo": "ລອນດອນ",
|
||||||
|
"ro": "Londra",
|
||||||
|
"cu": "Лондонъ",
|
||||||
|
"ce": "Лондон",
|
||||||
|
"ny": "London",
|
||||||
|
"am": "ለንደን",
|
||||||
|
"gv": "Lunnin",
|
||||||
|
"no": "London",
|
||||||
|
"it": "Londra",
|
||||||
|
"cv": "Лондон",
|
||||||
|
"ta": "இலண்டன்",
|
||||||
|
"ar": "لندن",
|
||||||
|
"av": "Лондон",
|
||||||
|
"sm": "Lonetona",
|
||||||
|
"ja": "ロンドン",
|
||||||
|
"ky": "Лондон",
|
||||||
|
"si": "ලන්ඩන්",
|
||||||
|
"tl": "Londres",
|
||||||
|
"na": "London",
|
||||||
|
"sw": "London",
|
||||||
|
"mi": "Rānana",
|
||||||
|
"lv": "Londona",
|
||||||
|
"gn": "Lóndyre",
|
||||||
|
"su": "London",
|
||||||
|
"mg": "Lôndôna",
|
||||||
|
"ml": "ലണ്ടൻ",
|
||||||
|
"fr": "Londres",
|
||||||
|
"ur": "علاقہ لندن",
|
||||||
|
"kw": "Loundres",
|
||||||
|
"yi": "לאנדאן",
|
||||||
|
"ig": "London",
|
||||||
|
"sc": "Londra",
|
||||||
|
"ln": "Lóndɛlɛ",
|
||||||
|
"kk": "Лондон",
|
||||||
|
"el": "Λονδίνο",
|
||||||
|
"sd": "لنڊن",
|
||||||
|
"li": "Londe",
|
||||||
|
"cy": "Llundain",
|
||||||
|
"ms": "London",
|
||||||
|
"fa": "لندن",
|
||||||
|
"sh": "London",
|
||||||
|
"feature_name": "London",
|
||||||
|
"uz": "London",
|
||||||
|
"bo": "ལོན་ཊོན།",
|
||||||
|
"mk": "Лондон",
|
||||||
|
"qu": "London",
|
||||||
|
"ay": "London",
|
||||||
|
"kv": "Лондон",
|
||||||
|
"ps": "لندن"
|
||||||
|
},
|
||||||
|
"lat": 51.5073219,
|
||||||
|
"lon": -0.1276474,
|
||||||
|
"country": "GB",
|
||||||
|
"state": "England"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "City of London",
|
||||||
|
"local_names": {
|
||||||
|
"lt": "Londono Sitis",
|
||||||
|
"zh": "倫敦市",
|
||||||
|
"hi": "सिटी ऑफ़ लंदन",
|
||||||
|
"ko": "시티 오브 런던",
|
||||||
|
"es": "City de Londres",
|
||||||
|
"ur": "لندن شہر",
|
||||||
|
"uk": "Лондонське Сіті",
|
||||||
|
"he": "הסיטי של לונדון",
|
||||||
|
"en": "City of London",
|
||||||
|
"pt": "Cidade de Londres",
|
||||||
|
"fr": "Cité de Londres",
|
||||||
|
"ru": "Сити"
|
||||||
|
},
|
||||||
|
"lat": 51.5156177,
|
||||||
|
"lon": -0.0919983,
|
||||||
|
"country": "GB",
|
||||||
|
"state": "England"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "London",
|
||||||
|
"local_names": {
|
||||||
|
"ga": "Londain",
|
||||||
|
"ka": "ლონდონი",
|
||||||
|
"yi": "לאנדאן",
|
||||||
|
"cr": "ᓬᐊᐣᑕᐣ",
|
||||||
|
"th": "ลอนดอน",
|
||||||
|
"he": "לונדון",
|
||||||
|
"el": "Λόντον",
|
||||||
|
"lt": "Londonas",
|
||||||
|
"ja": "ロンドン",
|
||||||
|
"fa": "لندن",
|
||||||
|
"bn": "লন্ডন",
|
||||||
|
"iu": "ᓚᓐᑕᓐ",
|
||||||
|
"ru": "Лондон",
|
||||||
|
"oj": "Baketigweyaang",
|
||||||
|
"ug": "لوندۇن",
|
||||||
|
"en": "London",
|
||||||
|
"be": "Лондан",
|
||||||
|
"ar": "لندن",
|
||||||
|
"fr": "London",
|
||||||
|
"lv": "Landona",
|
||||||
|
"ko": "런던",
|
||||||
|
"hy": "Լոնտոն"
|
||||||
|
},
|
||||||
|
"lat": 42.9832406,
|
||||||
|
"lon": -81.243372,
|
||||||
|
"country": "CA",
|
||||||
|
"state": "Ontario"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Chelsea",
|
||||||
|
"local_names": {
|
||||||
|
"id": "Chelsea, London",
|
||||||
|
"ru": "Челси",
|
||||||
|
"ga": "Chelsea",
|
||||||
|
"ur": "چیلسی، لندن",
|
||||||
|
"et": "Chelsea",
|
||||||
|
"pl": "Chelsea",
|
||||||
|
"da": "Chelsea",
|
||||||
|
"ar": "تشيلسي",
|
||||||
|
"fa": "چلسی",
|
||||||
|
"ko": "첼시",
|
||||||
|
"sv": "Chelsea, London",
|
||||||
|
"es": "Chelsea",
|
||||||
|
"eu": "Chelsea",
|
||||||
|
"nl": "Chelsea",
|
||||||
|
"sk": "Chelsea",
|
||||||
|
"no": "Chelsea",
|
||||||
|
"af": "Chelsea, Londen",
|
||||||
|
"uk": "Челсі",
|
||||||
|
"he": "צ'לסי",
|
||||||
|
"it": "Chelsea",
|
||||||
|
"hu": "Chelsea",
|
||||||
|
"ja": "チェルシー",
|
||||||
|
"zh": "車路士",
|
||||||
|
"hi": "चेल्सी, लंदन",
|
||||||
|
"pt": "Chelsea",
|
||||||
|
"az": "Çelsi",
|
||||||
|
"el": "Τσέλσι",
|
||||||
|
"vi": "Chelsea, Luân Đôn",
|
||||||
|
"en": "Chelsea",
|
||||||
|
"de": "Chelsea",
|
||||||
|
"tr": "Chelsea, Londra",
|
||||||
|
"fr": "Chelsea",
|
||||||
|
"sh": "Chelsea, London"
|
||||||
|
},
|
||||||
|
"lat": 51.4875167,
|
||||||
|
"lon": -0.1687007,
|
||||||
|
"country": "GB",
|
||||||
|
"state": "England"
|
||||||
|
},
|
||||||
|
{ "name": "London", "lat": 37.1289771, "lon": -84.0832646, "country": "US", "state": "Kentucky" }
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user