kopia lustrzana https://github.com/FacilMap/facilmap
Add first i18n infrastructure and translate utils and about-dialog
rodzic
0ca3fa9356
commit
f4c6a2f0cd
|
@ -27,7 +27,6 @@ module.exports = {
|
|||
"import/no-extraneous-dependencies": ["error"],
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "args": "none" }],
|
||||
"import/no-named-as-default": ["warn"],
|
||||
"import/no-named-as-default-member": ["warn"],
|
||||
"import/no-duplicates": ["warn"],
|
||||
"import/namespace": ["error"],
|
||||
"import/default": ["error"],
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
"facilmap-utils": "workspace:^",
|
||||
"file-saver": "^2.0.5",
|
||||
"hammerjs": "^2.0.8",
|
||||
"i18next": "^23.10.1",
|
||||
"jquery": "^3.7.1",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet-draggable-lines": "^2.0.0",
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
const messagesDe = {
|
||||
"about-dialog": {
|
||||
"header": `Über FacilMap {{version}}`,
|
||||
"license-text": `{{facilmap}} is unter der {{license}} verfügbar.`,
|
||||
"license-text-facilmap": `FacilMap`,
|
||||
"license-text-license": `GNU Affero General Public License, Version 3`,
|
||||
"issues-text": `Bitte melden Sie Fehler und Verbesserungsvorschläge auf {{tracker}}.`,
|
||||
"issues-text-tracker": `GitHub`,
|
||||
"help-text": `Wenn Sie Fragen haben, schauen Sie sich die {{documentation}} an, schreiben Sie ins {{discussions}} oder fragen im {{chat}}.`,
|
||||
"help-text-documentation": `Dokumentation`,
|
||||
"help-text-discussions": `Forum`,
|
||||
"help-text-chat": `Matrix-Chat`,
|
||||
"privacy-information": `Informationen zum Datenschutz`,
|
||||
"map-data": `Kartendaten`,
|
||||
"map-data-search": `Suche`,
|
||||
"map-data-pois": `POIs`,
|
||||
"map-data-directions": `Routenberechnung`,
|
||||
"map-data-geoip": `GeoIP`,
|
||||
"map-data-geoip-description": `Dieses Produkt enthält GeoLine2-Daten von Maxmind, verfügbar unter {{maxmind}}.`,
|
||||
"attribution-osm-contributors": `OSM-Mitwirkende`,
|
||||
"programs-libraries": `Programme/Bibliotheken`,
|
||||
"icons": `Symbole`
|
||||
},
|
||||
|
||||
"modal-dialog": {
|
||||
"close": "Schließen",
|
||||
"cancel": "Abbrechen",
|
||||
"save": "Speichern"
|
||||
}
|
||||
};
|
||||
|
||||
export default messagesDe;
|
|
@ -0,0 +1,32 @@
|
|||
const messagesEn = {
|
||||
"about-dialog": {
|
||||
"header": `About FacilMap {{version}}`,
|
||||
"license-text": `{{facilmap}} is available under the {{license}}.`,
|
||||
"license-text-facilmap": `FacilMap`,
|
||||
"license-text-license": `GNU Affero General Public License, Version 3`,
|
||||
"issues-text": `If something does not work or you have a suggestion for improvement, please report on the {{tracker}}.`,
|
||||
"issues-text-tracker": `issue tracker`,
|
||||
"help-text": `If you have a question, please have a look at the {{documentation}}, raise a question in the {{discussions}} or ask in the {{chat}}.`,
|
||||
"help-text-documentation": `documentation`,
|
||||
"help-text-discussions": `discussion forum`,
|
||||
"help-text-chat": `Matrix chat`,
|
||||
"privacy-information": `Privacy information`,
|
||||
"map-data": `Map data`,
|
||||
"map-data-search": `Search`,
|
||||
"map-data-pois": `POIs`,
|
||||
"map-data-directions": `Directions`,
|
||||
"map-data-geoip": `GeoIP`,
|
||||
"map-data-geoip-description": `This product includes GeoLite2 data created by MaxMind, available from {{maxmind}}.`,
|
||||
"attribution-osm-contributors": `OSM Contributors`,
|
||||
"programs-libraries": `Programs/libraries`,
|
||||
"icons": `Icons`
|
||||
},
|
||||
|
||||
"modal-dialog": {
|
||||
"close": "Close",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save"
|
||||
}
|
||||
};
|
||||
|
||||
export default messagesEn;
|
|
@ -4,6 +4,9 @@
|
|||
import { computed } from "vue";
|
||||
import ModalDialog from "./ui/modal-dialog.vue";
|
||||
import { injectContextRequired, requireMapContext } from "./facil-map-context-provider/facil-map-context-provider.vue";
|
||||
import { T, useI18n } from "../utils/i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const context = injectContextRequired();
|
||||
const mapContext = requireMapContext(context);
|
||||
|
@ -18,21 +21,49 @@
|
|||
});
|
||||
|
||||
const fmVersion = __FM_VERSION__;
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ModalDialog
|
||||
:title="`About FacilMap ${fmVersion}`"
|
||||
:title="t('about-dialog.header', { version: fmVersion })"
|
||||
class="fm-about"
|
||||
size="lg"
|
||||
@hidden="emit('hidden')"
|
||||
>
|
||||
<p><a href="https://github.com/facilmap/facilmap" target="_blank"><strong>FacilMap</strong></a> is available under the <a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">GNU Affero General Public License, Version 3</a>.</p>
|
||||
<p>If something does not work or you have a suggestion for improvement, please report on the <a href="https://github.com/FacilMap/facilmap/issues" target="_blank">issue tracker</a>.</p>
|
||||
<p>If you have a question, please have a look at the <a href="https://docs.facilmap.org/users/" target="_blank">documentation</a>, raise a question in the <a href="https://github.com/FacilMap/facilmap/discussions" target="_blank">discussion forum</a> or ask in the <a href="https://matrix.to/#/#facilmap:rankenste.in" target="_blank">Matrix chat</a>.</p>
|
||||
<p><a href="https://docs.facilmap.org/users/privacy/" target="_blank">Privacy information</a></p>
|
||||
<h4>Map data</h4>
|
||||
<p>
|
||||
<T k="about-dialog.license-text">
|
||||
<template #facilmap>
|
||||
<a href="https://github.com/facilmap/facilmap" target="_blank"><strong>{{t('about-dialog.license-text-facilmap')}}</strong></a>
|
||||
</template>
|
||||
<template #license>
|
||||
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html" target="_blank">{{t('about-dialog.license-text-license')}}</a>
|
||||
</template>
|
||||
</T>
|
||||
</p>
|
||||
<p>
|
||||
<T k="about-dialog.issues-text">
|
||||
<template #tracker>
|
||||
<a href="https://github.com/FacilMap/facilmap/issues" target="_blank">{{t('about-dialog.issues-text-tracker')}}</a>
|
||||
</template>
|
||||
</T>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<T k="about-dialog.help-text">
|
||||
<template #documentation>
|
||||
<a href="https://docs.facilmap.org/users/" target="_blank">{{t('about-dialog.help-text-documentation')}}</a>
|
||||
</template>
|
||||
<template #discussions>
|
||||
<a href="https://github.com/FacilMap/facilmap/discussions" target="_blank">{{t('about-dialog.help-text-discussions')}}</a>
|
||||
</template>
|
||||
<template #chat>
|
||||
<a href="https://matrix.to/#/#facilmap:rankenste.in" target="_blank">{{t('about-dialog.help-text-chat')}}</a>
|
||||
</template>
|
||||
</T>
|
||||
</p>
|
||||
|
||||
<p><a href="https://docs.facilmap.org/users/privacy/" target="_blank">{{t('about-dialog.privacy-information')}}</a></p>
|
||||
<h4>{{t('about-dialog.map-data')}}</h4>
|
||||
<dl class="row">
|
||||
<template v-for="layer in layers">
|
||||
<template v-if="layer.options.attribution">
|
||||
|
@ -41,19 +72,25 @@
|
|||
</template>
|
||||
</template>
|
||||
|
||||
<dt class="col-sm-3">Search</dt>
|
||||
<dd class="col-sm-9"><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
|
||||
<dt class="col-sm-3">{{t('about-dialog.map-data-search')}}</dt>
|
||||
<dd class="col-sm-9"><a href="https://nominatim.openstreetmap.org/" target="_blank">Nominatim</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{t('about-dialog.attribution-osm-contributors')}}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">POIs</dt>
|
||||
<dd class="col-sm-9"><a href="https://overpass-api.de/" target="_blank">Overpass API</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
|
||||
<dt class="col-sm-3">{{t('about-dialog.map-data-pois')}}</dt>
|
||||
<dd class="col-sm-9"><a href="https://overpass-api.de/" target="_blank">Overpass API</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{t('about-dialog.attribution-osm-contributors')}}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">Directions</dt>
|
||||
<dd class="col-sm-9"><a href="https://www.mapbox.com/api-documentation/#directions">Mapbox Directions API</a> / <a href="https://openrouteservice.org/">OpenRouteService</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">OSM Contributors</a></dd>
|
||||
<dt class="col-sm-3">{{t('about-dialog.map-data-directions')}}</dt>
|
||||
<dd class="col-sm-9"><a href="https://www.mapbox.com/api-documentation/#directions">Mapbox Directions API</a> / <a href="https://openrouteservice.org/">OpenRouteService</a> / <a href="https://www.openstreetmap.org/copyright" target="_blank">{{t('about-dialog.attribution-osm-contributors')}}</a></dd>
|
||||
|
||||
<dt class="col-sm-3">GeoIP</dt>
|
||||
<dd class="col-sm-9">This product includes GeoLite2 data created by MaxMind, available from <a href="https://www.maxmind.com">https://www.maxmind.com</a>.</dd>
|
||||
<dt class="col-sm-3">{{t('about-dialog.map-data-geoip')}}</dt>
|
||||
<dd class="col-sm-9">
|
||||
<T k="about-dialog.map-data-geoip-description">
|
||||
<template #maxmind>
|
||||
<a href="https://www.maxmind.com">https://www.maxmind.com</a>
|
||||
</template>
|
||||
</T>
|
||||
</dd>
|
||||
</dl>
|
||||
<h4>Programs/libraries</h4>
|
||||
<h4>{{t('about-dialog.programs-libraries')}}</h4>
|
||||
<ul>
|
||||
<li><a href="https://nodejs.org/" target="_blank">Node.js</a></li>
|
||||
<li><a href="https://sequelize.org/" target="_blank">Sequelize</a></li>
|
||||
|
@ -72,7 +109,7 @@
|
|||
<li><a href="https://expressjs.com/" target="_blank">Express</a></li>
|
||||
<li><a href="https://vuepress.vuejs.org/" target="_blank">Vuepress</a></li>
|
||||
</ul>
|
||||
<h4>Icons</h4>
|
||||
<h4>{{t('about-dialog.icons')}}</h4>
|
||||
<ul>
|
||||
<li><a href="https://github.com/twain47/Open-SVG-Map-Icons/" target="_blank">Open SVG Map Icons</a></li>
|
||||
<li><a href="https://glyphicons.com/" target="_blank">Glyphicons</a></li>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import { getZoomDestinationForLine } from "../../utils/zoom";
|
||||
import RouteForm from "../route-form/route-form.vue";
|
||||
import vTooltip from "../../utils/tooltip";
|
||||
import { formatField, formatRouteMode, formatTime, normalizeLineName, round } from "facilmap-utils";
|
||||
import { formatField, formatRouteTime, normalizeLineName, round } from "facilmap-utils";
|
||||
import { computed, ref } from "vue";
|
||||
import { useToasts } from "../ui/toasts/toasts.vue";
|
||||
import { showConfirm } from "../ui/alert.vue";
|
||||
|
@ -160,7 +160,7 @@
|
|||
<div class="fm-search-box-collapse-point" v-if="!isMoving">
|
||||
<dl class="fm-search-box-dl">
|
||||
<dt class="distance">Distance</dt>
|
||||
<dd class="distance">{{round(line.distance, 2)}} km <span v-if="line.time != null">({{formatTime(line.time)}} h {{formatRouteMode(line.mode)}})</span></dd>
|
||||
<dd class="distance">{{round(line.distance, 2)}} km <span v-if="line.time != null">({{formatRouteTime(line.time, line.mode)}})</span></dd>
|
||||
|
||||
<template v-if="line.ascent != null">
|
||||
<dt class="elevation">Climb/drop</dt>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, markRaw, onBeforeUnmount, onMounted, ref, watch, watchEffect } from "vue";
|
||||
import Icon from "../ui/icon.vue";
|
||||
import { formatRouteMode, formatTime, isSearchId, normalizeMarkerName, round, splitRouteQuery } from "facilmap-utils";
|
||||
import { formatRouteTime, isSearchId, normalizeMarkerName, round, splitRouteQuery } from "facilmap-utils";
|
||||
import { useToasts } from "../ui/toasts/toasts.vue";
|
||||
import type { ExportFormat, FindOnMapResult, SearchResult } from "facilmap-types";
|
||||
import { getMarkerIcon, type HashQuery, MarkerLayer, RouteLayer } from "facilmap-leaflet";
|
||||
|
@ -670,7 +670,7 @@
|
|||
|
||||
<dl class="fm-search-box-dl">
|
||||
<dt>Distance</dt>
|
||||
<dd>{{round(routeObj.distance, 2)}} km <span v-if="routeObj.time != null">({{formatTime(routeObj.time)}} h {{formatRouteMode(routeObj.mode)}})</span></dd>
|
||||
<dd>{{round(routeObj.distance, 2)}} km <span v-if="routeObj.time != null">({{formatRouteTime(routeObj.time, routeObj.mode)}})</span></dd>
|
||||
|
||||
<template v-if="routeObj.ascent != null">
|
||||
<dt>Climb/drop</dt>
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
import type { ThemeColour } from "../../utils/bootstrap";
|
||||
import { useUnloadHandler } from "../../utils/utils";
|
||||
import AttributePreservingElement from "./attribute-preserving-element.vue";
|
||||
import { useI18n } from "../../utils/i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
title?: string;
|
||||
|
@ -108,7 +111,7 @@
|
|||
@click="modal.hide()"
|
||||
type="button"
|
||||
class="btn-close"
|
||||
aria-label="Close"
|
||||
:aria-label="t('modal-dialog.close')"
|
||||
></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
@ -125,7 +128,7 @@
|
|||
class="btn btn-secondary"
|
||||
@click="modal.hide()"
|
||||
:disabled="isSubmitting || props.isBusy"
|
||||
>Cancel</button>
|
||||
>{{t('modal-dialog.cancel')}}</button>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
|
@ -134,7 +137,7 @@
|
|||
:disabled="isSubmitting || props.isBusy"
|
||||
>
|
||||
<div v-if="isSubmitting" class="spinner-border spinner-border-sm"></div>
|
||||
{{props.okLabel ?? (isCloseButton ? 'Close' : 'Save')}}
|
||||
{{props.okLabel ?? (isCloseButton ? t('modal-dialog.close') : t('modal-dialog.save'))}}
|
||||
</button>
|
||||
</div>
|
||||
</ValidatedForm>
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
/// <reference types="vite/client" />
|
||||
import { type i18n } from "i18next";
|
||||
import { defineComponent, ref } from "vue";
|
||||
import messagesEn from "../../i18n/en";
|
||||
import messagesDe from "../../i18n/de";
|
||||
import { getRawI18n, onI18nReady } from "facilmap-utils";
|
||||
|
||||
const namespace = "facilmap-frontend";
|
||||
|
||||
onI18nReady((i18n) => {
|
||||
i18n.addResourceBundle("en", namespace, messagesEn);
|
||||
i18n.addResourceBundle("de", namespace, messagesDe);
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept("../../i18n/en", (m) => {
|
||||
onI18nReady((i18n) => {
|
||||
i18n.addResourceBundle("en", namespace, m!.default);
|
||||
});
|
||||
});
|
||||
|
||||
import.meta.hot.accept("../../i18n/de", (m) => {
|
||||
onI18nReady((i18n) => {
|
||||
i18n.addResourceBundle("de", namespace, m!.default);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const rerenderCounter = ref(0);
|
||||
const rerender = () => {
|
||||
rerenderCounter.value++;
|
||||
};
|
||||
|
||||
onI18nReady((i18n) => {
|
||||
i18n.store.on("added", rerender);
|
||||
i18n.store.on("removed", rerender);
|
||||
i18n.on("languageChanged", rerender);
|
||||
i18n.on("loaded", rerender);
|
||||
});
|
||||
|
||||
export function useI18n(): Pick<i18n, "t"> {
|
||||
return {
|
||||
t: new Proxy(getRawI18n().getFixedT(null, namespace), {
|
||||
apply: (target, thisArg, argumentsList) => {
|
||||
rerenderCounter.value;
|
||||
return target.apply(thisArg, argumentsList as any);
|
||||
}
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
export const T = defineComponent({
|
||||
props: {
|
||||
k: { type: String, required: true }
|
||||
},
|
||||
setup(props, { slots }) {
|
||||
const i18n = useI18n();
|
||||
|
||||
return () => {
|
||||
const mappedSlots = Object.entries(slots).map(([name, slot], i) => ({ name, placeholder: `%___SLOT_${i}___%`, slot }));
|
||||
const placeholderByName = Object.fromEntries(mappedSlots.map(({ name, placeholder }) => [name, placeholder]));
|
||||
const slotByPlaceholder = Object.fromEntries(mappedSlots.map(({ placeholder, slot }) => [placeholder, slot]));
|
||||
const message = i18n.t(props.k, placeholderByName);
|
||||
return message.split(/(%___SLOT_\d+___%)/g).map((v, i) => {
|
||||
if (i % 2 === 0) {
|
||||
return v;
|
||||
} else {
|
||||
return slotByPlaceholder[v]!();
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
});
|
|
@ -5,7 +5,7 @@ import type { SelectedItem } from "./selection";
|
|||
import type { FindOnMapLine, FindOnMapMarker, FindOnMapResult, Line, Marker, SearchResult } from "facilmap-types";
|
||||
import type { Geometry } from "geojson";
|
||||
import { isMapResult } from "./search";
|
||||
import { decodeLonLatUrl, normalizeLineName, normalizeMarkerName } from "facilmap-utils";
|
||||
import { decodeLonLatUrl, normalizeLineName, normalizeMarkerName, splitRouteQuery } from "facilmap-utils";
|
||||
import type { ClientContext } from "../components/facil-map-context-provider/client-context";
|
||||
import type { FacilMapContext } from "../components/facil-map-context-provider/facil-map-context";
|
||||
import { requireClientContext, requireMapContext } from "../components/facil-map-context-provider/facil-map-context-provider.vue";
|
||||
|
@ -140,10 +140,13 @@ export async function openSpecialQuery(query: string, context: FacilMapContext,
|
|||
const searchBoxContext = toRef(() => context.components.searchBox);
|
||||
const routeFormTabContext = toRef(() => context.components.routeFormTab);
|
||||
|
||||
if(searchBoxContext.value && routeFormTabContext.value && query.match(/ to /i)) {
|
||||
routeFormTabContext.value.setQuery(query, zoom, smooth);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-route-form-tab`, { autofocus: true });
|
||||
return true;
|
||||
if(searchBoxContext.value && routeFormTabContext.value) {
|
||||
const split = splitRouteQuery(query);
|
||||
if (split.queries.length >= 2) {
|
||||
routeFormTabContext.value.setQuery(query, zoom, smooth);
|
||||
searchBoxContext.value.activateTab(`fm${context.id}-route-form-tab`, { autofocus: true });
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const lonlat = decodeLonLatUrl(query);
|
||||
|
|
|
@ -43,6 +43,7 @@
|
|||
"cheerio": "^1.0.0-rc.12",
|
||||
"compression": "^1.7.4",
|
||||
"compressjs": "^1.0.3",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"csv-stringify": "^6.4.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"ejs": "^3.1.9",
|
||||
|
@ -53,6 +54,8 @@
|
|||
"facilmap-types": "workspace:^",
|
||||
"facilmap-utils": "workspace:^",
|
||||
"find-cache-dir": "^5.0.0",
|
||||
"i18next": "^23.10.1",
|
||||
"i18next-http-middleware": "^3.5.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"maxmind": "^4.3.18",
|
||||
"md5-file": "^5.0.0",
|
||||
|
@ -69,6 +72,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/compression": "^1.7.5",
|
||||
"@types/cookie-parser": "^1.4.7",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/ejs": "^3.1.5",
|
||||
"@types/express": "^4.17.21",
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { defaultRawI18nGetter, getRawI18n, onI18nReady, setLanguageDetector, setRawI18nGetter } from "facilmap-utils";
|
||||
import messagesEn from "./i18n/en";
|
||||
import messagesDe from "facilmap-utils/src/i18n/de";
|
||||
import type { i18n } from "i18next";
|
||||
import type { Domain } from "domain";
|
||||
import type { RequestHandler } from "express";
|
||||
import i18nextHttpMiddleware from "i18next-http-middleware";
|
||||
|
||||
const namespace = "facilmap-server";
|
||||
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface Process {
|
||||
domain?: Domain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare module 'domain' {
|
||||
interface Domain {
|
||||
facilmap?: {
|
||||
i18n?: i18n
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onI18nReady((i18n) => {
|
||||
i18n.addResourceBundle("en", namespace, messagesEn);
|
||||
i18n.addResourceBundle("de", namespace, messagesDe);
|
||||
});
|
||||
|
||||
setLanguageDetector(i18nextHttpMiddleware.LanguageDetector);
|
||||
|
||||
export const i18nMiddleware: RequestHandler[] = [
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
(req, res, next) => {
|
||||
i18nextHttpMiddleware.handle(getRawI18n())(req, res, next);
|
||||
},
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||
(req, res, next) => {
|
||||
if ((req as any).i18n) {
|
||||
if (!process.domain!.facilmap) {
|
||||
process.domain!.facilmap = {};
|
||||
}
|
||||
|
||||
process.domain!.facilmap.i18n = (req as any).i18n;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
];
|
||||
|
||||
setRawI18nGetter(() => {
|
||||
if (process.domain?.facilmap?.i18n) {
|
||||
return process.domain?.facilmap?.i18n;
|
||||
} else {
|
||||
return defaultRawI18nGetter();
|
||||
}
|
||||
});
|
||||
|
||||
export function getI18n(): Pick<i18n, "t"> {
|
||||
return {
|
||||
t: getRawI18n().getFixedT(null, namespace)
|
||||
};
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
const messagesEn = {
|
||||
};
|
||||
|
||||
export default messagesEn;
|
|
@ -0,0 +1,4 @@
|
|||
const messagesEn = {
|
||||
};
|
||||
|
||||
export default messagesEn;
|
|
@ -14,6 +14,8 @@ import { paths } from "facilmap-frontend/build.js";
|
|||
import config from "./config";
|
||||
import { exportCsv } from "./export/csv.js";
|
||||
import * as z from "zod";
|
||||
import cookieParser from "cookie-parser";
|
||||
import { i18nMiddleware } from "./i18n.js";
|
||||
|
||||
function getBaseUrl(req: Request): string {
|
||||
return config.baseUrl ?? `${req.protocol}://${req.host}/`;
|
||||
|
@ -67,6 +69,9 @@ export async function initWebserver(database: Database, port: number, host?: str
|
|||
|
||||
app.use(domainMiddleware);
|
||||
app.use(compression());
|
||||
app.use(cookieParser());
|
||||
|
||||
app.use(i18nMiddleware);
|
||||
|
||||
app.get("/", padMiddleware);
|
||||
|
||||
|
|
|
@ -38,6 +38,8 @@
|
|||
"dompurify": "^3.0.9",
|
||||
"facilmap-types": "workspace:^",
|
||||
"filtrex": "^2.2.3",
|
||||
"i18next": "^23.10.1",
|
||||
"i18next-browser-languagedetector": "^7.2.1",
|
||||
"jquery": "^3.7.1",
|
||||
"jsdom": "^24.0.0",
|
||||
"linkify-string": "^4.1.3",
|
||||
|
|
|
@ -1,5 +1,52 @@
|
|||
import { expect, test } from "vitest";
|
||||
import { matchLonLat } from "../search";
|
||||
import { matchLonLat, splitRouteQuery } from "../search";
|
||||
|
||||
test("splitRouteQuery", () => {
|
||||
expect(splitRouteQuery("Hamburg to Berlin")).toEqual({
|
||||
queries: ["Hamburg", "Berlin"],
|
||||
mode: null
|
||||
});
|
||||
|
||||
expect(splitRouteQuery("from Hamburg to Berlin")).toEqual({
|
||||
queries: ["Hamburg", "Berlin"],
|
||||
mode: null
|
||||
});
|
||||
|
||||
expect(splitRouteQuery("to Berlin from Hamburg")).toEqual({
|
||||
queries: ["Hamburg", "Berlin"],
|
||||
mode: null
|
||||
});
|
||||
|
||||
expect(splitRouteQuery("Hamburg to Berlin via Hannover")).toEqual({
|
||||
queries: ["Hamburg", "Hannover", "Berlin"],
|
||||
mode: null
|
||||
});
|
||||
|
||||
expect(splitRouteQuery("via Hannover to Berlin from Hamburg")).toEqual({
|
||||
queries: ["Hamburg", "Hannover", "Berlin"],
|
||||
mode: null
|
||||
});
|
||||
|
||||
expect(splitRouteQuery("by walk to Berlin from Hamburg")).toEqual({
|
||||
queries: ["Hamburg", "Berlin"],
|
||||
mode: "foot"
|
||||
});
|
||||
|
||||
expect(splitRouteQuery("from Hamburg by walk to Berlin")).toEqual({
|
||||
queries: ["Hamburg", "Berlin"],
|
||||
mode: "foot"
|
||||
});
|
||||
|
||||
expect(splitRouteQuery("Hamburg to Berlin walking")).toEqual({
|
||||
queries: ["Hamburg", "Berlin"],
|
||||
mode: "foot"
|
||||
});
|
||||
|
||||
expect(splitRouteQuery("Hamburg")).toEqual({
|
||||
queries: ["Hamburg"],
|
||||
mode: null
|
||||
});
|
||||
});
|
||||
|
||||
test("matchLonLat", () => {
|
||||
// Simple coordinates
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import type { Point } from "facilmap-types";
|
||||
import { RetryError, throttledBatch } from "./utils";
|
||||
import { fetchAdapter, getConfig } from "./config";
|
||||
import { RetryError, throttledBatch } from "./utils.js";
|
||||
import { fetchAdapter, getConfig } from "./config.js";
|
||||
import { getI18n } from "./i18n.js";
|
||||
|
||||
const MAX_DELAY_MS = 60_000;
|
||||
|
||||
|
@ -18,7 +19,7 @@ export const getElevationForPoint = throttledBatch<[Point], number | undefined>(
|
|||
})
|
||||
});
|
||||
if (!res.ok) {
|
||||
let error = new Error(`Looking up elevations failed with status ${res.status}.`);
|
||||
let error = new Error(getI18n().t("elevation.http-error", { status: res.status }));
|
||||
if (res.status === 504) {
|
||||
// Probably caused by an overload on the server. Usually it goes away after a while. Let's exponentially increase delays
|
||||
// between requests until it succeeds again.
|
||||
|
|
|
@ -6,6 +6,7 @@ import createPurify from "dompurify";
|
|||
import { type Cheerio, load } from "cheerio";
|
||||
import { normalizeFieldValue } from "./objects.js";
|
||||
import { NodeWithChildren, Element, type Node, type ParentNode, Text, type AnyNode } from "domhandler";
|
||||
import { getI18n } from "./i18n.js";
|
||||
|
||||
const purify = createPurify(typeof window !== "undefined" ? window : new (await import("jsdom")).JSDOM("").window);
|
||||
|
||||
|
@ -115,7 +116,7 @@ export function formatTime(seconds: number): string {
|
|||
let minutes: string | number = Math.floor((seconds%3600)/60);
|
||||
if(minutes < 10)
|
||||
minutes = "0" + minutes;
|
||||
return hours + ":" + minutes;
|
||||
return getI18n().t("format.time", { hours, minutes });
|
||||
}
|
||||
|
||||
function applyMarkdownModifications($el: Cheerio<AnyNode>): void {
|
||||
|
|
|
@ -0,0 +1,61 @@
|
|||
import i18next, { type Module, type Newable, type i18n } from "i18next";
|
||||
import LanguageDetector from "i18next-browser-languagedetector";
|
||||
|
||||
let hasBeenUsed = false;
|
||||
const onFirstUse: Array<(i18n: i18n) => void> = [];
|
||||
|
||||
let languageDetector: Module | Newable<Module> | undefined = typeof window !== "undefined" ? LanguageDetector : undefined;
|
||||
|
||||
export function setLanguageDetector(detector: Module | Newable<Module> | undefined): void {
|
||||
languageDetector = detector;
|
||||
}
|
||||
|
||||
export const defaultRawI18nGetter = (): i18n => {
|
||||
// Initialize i18next on first usage if it is not initialized yet.
|
||||
// In apps that use i18next, this leaves enough time for them to initialize it with their own custom settings.
|
||||
// In apps that don't use i18next, this allows them to use our functions without having to care about initializing it.
|
||||
if (!i18next.isInitializing && !i18next.isInitialized) {
|
||||
if (languageDetector) {
|
||||
i18next.use(languageDetector);
|
||||
}
|
||||
|
||||
void i18next.init({
|
||||
initImmediate: false,
|
||||
...(languageDetector ? {} : { lng: "en" }),
|
||||
fallbackLng: "en"
|
||||
});
|
||||
}
|
||||
|
||||
return i18next;
|
||||
};
|
||||
|
||||
let rawI18nGetter = defaultRawI18nGetter;
|
||||
|
||||
export function setRawI18nGetter(getter: () => i18n): void {
|
||||
rawI18nGetter = getter;
|
||||
}
|
||||
|
||||
// Should be called by an individual getI18n() function in each package that makes sure that its translations are loaded
|
||||
export function getRawI18n(): i18n {
|
||||
const i18n = rawI18nGetter();
|
||||
|
||||
if (!hasBeenUsed) {
|
||||
for (const callback of onFirstUse) {
|
||||
callback(i18n);
|
||||
}
|
||||
hasBeenUsed = true;
|
||||
}
|
||||
|
||||
return i18n;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the given callback the first time getI18n() is called. If getI18n() has already been called, calls the callback immediately.
|
||||
*/
|
||||
export function onI18nReady(callback: (i18n: i18n) => void): void {
|
||||
if (hasBeenUsed) {
|
||||
callback(getRawI18n());
|
||||
} else {
|
||||
onFirstUse.push(callback);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
/// <reference types="vite/client" />
|
||||
import type { i18n } from "i18next";
|
||||
import { getRawI18n, onI18nReady } from "./i18n-utils";
|
||||
import messagesDe from "./i18n/de";
|
||||
import messagesEn from "./i18n/en";
|
||||
|
||||
const namespace = "facilmap-utils";
|
||||
|
||||
onI18nReady((i18n) => {
|
||||
i18n.addResourceBundle("en", namespace, messagesEn);
|
||||
i18n.addResourceBundle("de", namespace, messagesDe);
|
||||
});
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.accept("./i18n/en", (m) => {
|
||||
onI18nReady((i18n) => {
|
||||
i18n.addResourceBundle("en", namespace, m!.default);
|
||||
});
|
||||
});
|
||||
|
||||
import.meta.hot.accept("./i18n/de", (m) => {
|
||||
onI18nReady((i18n) => {
|
||||
i18n.addResourceBundle("de", namespace, m!.default);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getI18n(): Pick<i18n, "t"> {
|
||||
return {
|
||||
t: getRawI18n().getFixedT(null, namespace)
|
||||
};
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
const messagesDe = {
|
||||
"elevation": {
|
||||
"http-error": `Unerwarteter Fehler beim Laden der Höhe über NN (Status {{status}}).`
|
||||
},
|
||||
|
||||
"format": {
|
||||
"time": `{{hours}}:{{minutes}}`
|
||||
},
|
||||
|
||||
"objects": {
|
||||
"untitled-marker": `Unbenannter Marker`,
|
||||
"untitled-lined": `Unbenannte Linie`
|
||||
},
|
||||
|
||||
"pads": {
|
||||
"unnamed-map": `Unbenannte Karte`,
|
||||
"page-title": `{{padName}} – {{appName}}`,
|
||||
"fallback-page-description": `Eine vielseitige Karte auf OpenStreetMap-Basis, auf der Marker und Linien mit Live-Kollaboration gesetzt werden können.`
|
||||
},
|
||||
|
||||
"routing": {
|
||||
"route-mode-hgv": `mit dem LKW`,
|
||||
"route-mode-car": `mit dem Auto`,
|
||||
"route-mode-road-bike": `mit dem Rennrad`,
|
||||
"route-mode-mountain-bike": `mit dem Mountain Bike`,
|
||||
"route-mode-electric-bike": `mit dem E-Bike`,
|
||||
"route-mode-bicycle": `mit dem Fahrrad`,
|
||||
"route-mode-wheelchair": `mit dem Rollstuhl`,
|
||||
"route-mode-foot": `zu Fuß`,
|
||||
"route-time": `{{time}}\u202Fh {{mode}}`
|
||||
},
|
||||
|
||||
"search": {
|
||||
"http-error": `Unerwarteter Fehler bei der Suche (Status {{status}}).`,
|
||||
"from": `von`,
|
||||
"to": `nach`,
|
||||
"via": `über`,
|
||||
"hgv": `mit dem LKW`,
|
||||
"car": `mit dem Auto`,
|
||||
"road-bike": `mit dem Rennrad`,
|
||||
"mountain-bike": `mit dem Mountain Bike`,
|
||||
"electric-bike": `mit dem E-Bike|mit dem Pedelec`,
|
||||
"bicycle": `mit dem Rad|mit dem Fahrrad`,
|
||||
"wheelchair": `mit dem Rollstuhl`,
|
||||
"foot": `zu Fuß`,
|
||||
"straight": `Luftlinie`
|
||||
}
|
||||
};
|
||||
|
||||
export default messagesDe;
|
|
@ -0,0 +1,50 @@
|
|||
const messagesEn = {
|
||||
"elevation": {
|
||||
"http-error": `Looking up elevations failed with status {{status}}.`
|
||||
},
|
||||
|
||||
"format": {
|
||||
"time": `{{hours}}:{{minutes}}`
|
||||
},
|
||||
|
||||
"objects": {
|
||||
"untitled-marker": `Untitled marker`,
|
||||
"untitled-lined": `Untitled line`
|
||||
},
|
||||
|
||||
"pads": {
|
||||
"unnamed-map": `Unnamed map`,
|
||||
"page-title": `{{padName}} – {{appName}}`,
|
||||
"fallback-page-description": `A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration.`
|
||||
},
|
||||
|
||||
"routing": {
|
||||
"route-mode-hgv": `by HGV`,
|
||||
"route-mode-car": `by car`,
|
||||
"route-mode-road-bike": `by road bike`,
|
||||
"route-mode-mountain-bike": `by mountain bike`,
|
||||
"route-mode-electric-bike": `by electric bike`,
|
||||
"route-mode-bicycle": `by bicycle`,
|
||||
"route-mode-wheelchair": `by wheelchair`,
|
||||
"route-mode-foot": `on foot`,
|
||||
"route-time": `{{time}}\u202Fh {{mode}}`
|
||||
},
|
||||
|
||||
"search": {
|
||||
"http-error": `Search failed with status {{status}}.`,
|
||||
"from": `from`,
|
||||
"to": `to`,
|
||||
"via": `via`,
|
||||
"hgv": `by HGV`,
|
||||
"car": `by car`,
|
||||
"road-bike": `by road bike`,
|
||||
"mountain-bike": `by mountain bike`,
|
||||
"electric-bike": `by electric bike`,
|
||||
"bicycle": `by bicycle|by bike`,
|
||||
"wheelchair": `by wheelchair`,
|
||||
"foot": `on foot|by walk|by walking|walking`,
|
||||
"straight": `straight|by helicopter`
|
||||
}
|
||||
};
|
||||
|
||||
export default messagesEn;
|
|
@ -2,6 +2,7 @@ export * from "./config.js";
|
|||
export * from "./elevation.js";
|
||||
export * from "./filter.js";
|
||||
export * from "./format.js";
|
||||
export * from "./i18n-utils.js";
|
||||
export * from "./objects.js";
|
||||
export * from "./pads.js";
|
||||
export * from "./routing.js";
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { lineValidator, markerValidator, type CRU, type Field, type FieldOption, type Line, type LineTemplate, type Marker, type Type } from "facilmap-types";
|
||||
import { omit } from "lodash-es";
|
||||
import { getI18n } from "./i18n.js";
|
||||
|
||||
export function isMarker<Mode extends CRU.READ | CRU.CREATE>(object: Marker<Mode> | Line<Mode>): object is Marker<Mode> {
|
||||
return "lat" in object && object.lat != null;
|
||||
|
@ -168,9 +169,9 @@ export function getLineTemplate(type: Type): LineTemplate {
|
|||
}
|
||||
|
||||
export function normalizeMarkerName(name: string | undefined): string {
|
||||
return name || "Untitled marker";
|
||||
return name || getI18n().t("objects.untitled-marker");
|
||||
}
|
||||
|
||||
export function normalizeLineName(name: string | undefined): string {
|
||||
return name || "Untitled line";
|
||||
return name || getI18n().t("objects.untitled-line");
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import type { ID, Type, View } from "facilmap-types";
|
||||
import { getI18n } from "./i18n.js";
|
||||
|
||||
const LETTERS = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||
const LENGTH = 12;
|
||||
|
@ -12,15 +13,15 @@ export function generateRandomPadId(length: number = LENGTH): string {
|
|||
}
|
||||
|
||||
export function normalizePadName(name: string | undefined): string {
|
||||
return name || "Unnamed map";
|
||||
return name || getI18n().t("pads.unnamed-map");
|
||||
}
|
||||
|
||||
export function normalizePageTitle(padName: string | undefined, appName: string): string {
|
||||
return `${padName ? `${padName} – ` : ''}${appName}`;
|
||||
return padName ? getI18n().t("pads.page-title", { padName, appName }) : appName;
|
||||
}
|
||||
|
||||
export function normalizePageDescription(padDescription: string | undefined): string {
|
||||
return padDescription || "A fully-featured OpenStreetMap-based map where markers and lines can be added with live collaboration.";
|
||||
return padDescription || getI18n().t("pads.fallback-page-description");
|
||||
}
|
||||
|
||||
export function getOrderedTypes(types: Type[] | Record<ID, Type>): Type[] {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import type { RouteMode } from "facilmap-types";
|
||||
import { getI18n } from "./i18n.js";
|
||||
import { formatTime } from "./format.js";
|
||||
|
||||
export interface DecodedRouteMode {
|
||||
mode: "" | "car" | "bicycle" | "pedestrian" | "track";
|
||||
|
@ -92,29 +94,36 @@ export function formatRouteMode(encodedMode: RouteMode): string {
|
|||
case "car":
|
||||
switch(decodedMode.type) {
|
||||
case "hgv":
|
||||
return "by HGV";
|
||||
return getI18n().t("routing.route-mode-hgv");
|
||||
default:
|
||||
return "by car";
|
||||
return getI18n().t("routing.route-mode-car");
|
||||
}
|
||||
case "bicycle":
|
||||
switch(decodedMode.type) {
|
||||
case "road":
|
||||
return "by road bike";
|
||||
return getI18n().t("routing.route-mode-road-bike");
|
||||
case "mountain":
|
||||
return "by mountain bike";
|
||||
return getI18n().t("routing.route-mode-mountain-bike");
|
||||
case "electric":
|
||||
return "by electric bike";
|
||||
return getI18n().t("routing.route-mode-electric-bike");
|
||||
default:
|
||||
return "by bicycle";
|
||||
return getI18n().t("routing.route-mode-bicycle");
|
||||
}
|
||||
case "pedestrian":
|
||||
switch(decodedMode.type) {
|
||||
case "wheelchair":
|
||||
return "by wheelchair";
|
||||
return getI18n().t("routing.route-mode-wheelchair");
|
||||
default:
|
||||
return "on foot";
|
||||
return getI18n().t("routing.route-mode-foot");
|
||||
}
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export function formatRouteTime(time: number, encodedMode: RouteMode): string {
|
||||
return getI18n().t("routing.route-time", {
|
||||
time: formatTime(time),
|
||||
mode: formatRouteMode(encodedMode)
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import type { Point, SearchResult, ZoomLevel } from "facilmap-types";
|
||||
import throttle from "p-throttle";
|
||||
import type { Geometry } from "geojson";
|
||||
import { formatCoordinates } from "./format";
|
||||
import { fetchAdapter, getConfig } from "./config";
|
||||
import { formatCoordinates } from "./format.js";
|
||||
import { fetchAdapter, getConfig } from "./config.js";
|
||||
import { getI18n } from "./i18n.js";
|
||||
import { quoteRegExp } from "./utils.js";
|
||||
|
||||
interface NominatimResult {
|
||||
place_id: number;
|
||||
|
@ -77,22 +79,56 @@ interface PointWithZoom extends Point {
|
|||
const throttledFetch = throttle({ limit: 1, interval: 1000 })((...args: Parameters<typeof fetchAdapter>) => fetchAdapter(...args));
|
||||
|
||||
export function splitRouteQuery(query: string): { queries: string[], mode: string | null } {
|
||||
const splitQuery = query.split(/(^|\s+)(from|to|via|by)(\s+|$)/).filter((item, i) => (i%2 == 0)); // Filter out every second item (whitespace parantheses)
|
||||
const i18n = getI18n();
|
||||
|
||||
const routeModesByTranslation = Object.fromEntries(Object.entries({
|
||||
[i18n.t("search.hgv")]: "hgv",
|
||||
[i18n.t("search.car")]: "car",
|
||||
[i18n.t("search.road-bike")]: "road bike",
|
||||
[i18n.t("search.mountain-bike")]: "mountain bike",
|
||||
[i18n.t("search.electric-bike")]: "electric bike",
|
||||
[i18n.t("search.bicycle")]: "bicycle",
|
||||
[i18n.t("search.wheelchair")]: "wheelchair",
|
||||
[i18n.t("search.foot")]: "foot",
|
||||
[i18n.t("search.straight")]: "straight"
|
||||
}).flatMap(([k, v]) => k.split("|").map((k2) => [k2, v])));
|
||||
|
||||
let queryWithoutMode = query;
|
||||
let mode = null;
|
||||
for (const [translation, thisMode] of Object.entries(routeModesByTranslation)) {
|
||||
const m = query.match(new RegExp(`(?<= |^)${quoteRegExp(translation)}(?= |$)`, "di"));
|
||||
if (m) {
|
||||
queryWithoutMode = `${query.slice(0, m.indices![0][0])}${query.slice(m.indices![0][1])}`.trim();
|
||||
mode = thisMode;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const keywordsByTranslation = Object.fromEntries(Object.entries({
|
||||
[i18n.t("search.from")]: "from",
|
||||
[i18n.t("search.to")]: "to",
|
||||
[i18n.t("search.via")]: "via"
|
||||
}).flatMap(([k, v]) => k.split("|").map((k2) => [k2, v])));
|
||||
|
||||
const splitQuery = queryWithoutMode
|
||||
.split(new RegExp(`(?:^|\\s+)(from|to|via|${Object.keys(keywordsByTranslation).map((k) => quoteRegExp(k)).join("|")})(?:\\s+|$)`));
|
||||
|
||||
const queryParts = {
|
||||
from: [] as string[],
|
||||
via: [] as string[],
|
||||
to: [] as string[],
|
||||
by: [] as string[]
|
||||
to: [] as string[]
|
||||
};
|
||||
|
||||
for(let i=0; i<splitQuery.length; i+=2) {
|
||||
if(splitQuery[i])
|
||||
queryParts[splitQuery[i-1] as keyof typeof queryParts || "from"].push(splitQuery[i]);
|
||||
if(splitQuery[i]) {
|
||||
const thisKeyword = splitQuery[i - 1] ? (keywordsByTranslation[splitQuery[i - 1]] ?? splitQuery[i - 1]) : "from";
|
||||
queryParts[thisKeyword as keyof typeof queryParts].push(splitQuery[i]);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
queries: queryParts.from.concat(queryParts.via, queryParts.to),
|
||||
mode: queryParts.by[0] || null
|
||||
mode
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -245,7 +281,7 @@ async function _findLonLat(lonlatWithZoom: PointWithZoom): Promise<Array<SearchR
|
|||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Reverse geocoding failed with status ${res.status}`);
|
||||
throw new Error(getI18n().t("search.http-error", { status: res.status }));
|
||||
}
|
||||
|
||||
const body: NominatimResult | NominatimError = await res.json();
|
||||
|
@ -267,7 +303,7 @@ async function _findQuery(query: string): Promise<Array<SearchResult>> {
|
|||
);
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error(`Search failed with status ${res.status}.`);
|
||||
throw new Error(getI18n().t("search.http-error", { status: res.status }));
|
||||
}
|
||||
|
||||
const body: Array<NominatimResult> | NominatimError = await res.json();
|
||||
|
@ -281,12 +317,15 @@ async function _findQuery(query: string): Promise<Array<SearchResult>> {
|
|||
}
|
||||
|
||||
async function _findOsmObject(type: string, id: string): Promise<Array<SearchResult>> {
|
||||
const body: Array<NominatimResult> | NominatimError = await throttledFetch(
|
||||
const res = await throttledFetch(
|
||||
`${getConfig().nominatimUrl}/lookup?format=jsonv2&addressdetails=1&polygon_geojson=1&extratags=1&namedetails=1&osm_ids=${encodeURI(type.toUpperCase())}${encodeURI(id)}`
|
||||
).then((res) => res.json() as any);
|
||||
);
|
||||
|
||||
if(!body)
|
||||
throw new Error("Invalid response from name finder.");
|
||||
if (!res.ok) {
|
||||
throw new Error(getI18n().t("search.http-error", { status: res.status }));
|
||||
}
|
||||
|
||||
const body: Array<NominatimResult> | NominatimError = await res.json();
|
||||
|
||||
if('error' in body)
|
||||
throw new Error(typeof body.error === 'string' ? body.error : body.error.message);
|
||||
|
|
60
yarn.lock
60
yarn.lock
|
@ -44,6 +44,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/runtime@npm:^7.23.2":
|
||||
version: 7.24.1
|
||||
resolution: "@babel/runtime@npm:7.24.1"
|
||||
dependencies:
|
||||
regenerator-runtime: ^0.14.0
|
||||
checksum: 5c8f3b912ba949865f03b3cf8395c60e1f4ebd1033fbd835bdfe81b6cac8a87d85bc3c7aded5fcdf07be044c9ab8c818f467abe0deca50020c72496782639572
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@babel/types@npm:^7.8.3":
|
||||
version: 7.24.0
|
||||
resolution: "@babel/types@npm:7.24.0"
|
||||
|
@ -878,6 +887,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/cookie-parser@npm:^1.4.7":
|
||||
version: 1.4.7
|
||||
resolution: "@types/cookie-parser@npm:1.4.7"
|
||||
dependencies:
|
||||
"@types/express": "*"
|
||||
checksum: 7b87c59420598e686a57e240be6e0db53967c3c8814be9326bf86609ee2fc39c4b3b9f2263e1deba43526090121d1df88684b64c19f7b494a80a4437caf3d40b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/cookie@npm:^0.4.1":
|
||||
version: 0.4.1
|
||||
resolution: "@types/cookie@npm:0.4.1"
|
||||
|
@ -2476,6 +2494,16 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie-parser@npm:^1.4.6":
|
||||
version: 1.4.6
|
||||
resolution: "cookie-parser@npm:1.4.6"
|
||||
dependencies:
|
||||
cookie: 0.4.1
|
||||
cookie-signature: 1.0.6
|
||||
checksum: 1e5a63aa82e8eb4e02d2977c6902983dee87b02e87ec5ec43ac3cb1e72da354003716570cd5190c0ad9e8a454c9d3237f4ad6e2f16d0902205a96a1c72b77ba5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cookie-signature@npm:1.0.6":
|
||||
version: 1.0.6
|
||||
resolution: "cookie-signature@npm:1.0.6"
|
||||
|
@ -3658,6 +3686,7 @@ __metadata:
|
|||
file-saver: ^2.0.5
|
||||
hammerjs: ^2.0.8
|
||||
happy-dom: ^13.6.2
|
||||
i18next: ^23.10.1
|
||||
jquery: ^3.7.1
|
||||
leaflet: ^1.9.4
|
||||
leaflet-draggable-lines: ^2.0.0
|
||||
|
@ -3769,6 +3798,7 @@ __metadata:
|
|||
resolution: "facilmap-server@workspace:server"
|
||||
dependencies:
|
||||
"@types/compression": ^1.7.5
|
||||
"@types/cookie-parser": ^1.4.7
|
||||
"@types/debug": ^4.1.12
|
||||
"@types/ejs": ^3.1.5
|
||||
"@types/express": ^4.17.21
|
||||
|
@ -3780,6 +3810,7 @@ __metadata:
|
|||
cheerio: ^1.0.0-rc.12
|
||||
compression: ^1.7.4
|
||||
compressjs: ^1.0.3
|
||||
cookie-parser: ^1.4.6
|
||||
cpy-cli: ^5.0.0
|
||||
csv-stringify: ^6.4.6
|
||||
debug: ^4.3.4
|
||||
|
@ -3792,6 +3823,8 @@ __metadata:
|
|||
facilmap-types: "workspace:^"
|
||||
facilmap-utils: "workspace:^"
|
||||
find-cache-dir: ^5.0.0
|
||||
i18next: ^23.10.1
|
||||
i18next-http-middleware: ^3.5.0
|
||||
lodash-es: ^4.17.21
|
||||
maxmind: ^4.3.18
|
||||
md5-file: ^5.0.0
|
||||
|
@ -3843,6 +3876,8 @@ __metadata:
|
|||
dompurify: ^3.0.9
|
||||
facilmap-types: "workspace:^"
|
||||
filtrex: ^2.2.3
|
||||
i18next: ^23.10.1
|
||||
i18next-browser-languagedetector: ^7.2.1
|
||||
jquery: ^3.7.1
|
||||
jsdom: ^24.0.0
|
||||
linkify-string: ^4.1.3
|
||||
|
@ -4528,6 +4563,31 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"i18next-browser-languagedetector@npm:^7.2.1":
|
||||
version: 7.2.1
|
||||
resolution: "i18next-browser-languagedetector@npm:7.2.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.23.2
|
||||
checksum: 159958be2d8f19444e9378512c36c2bf13a8ab85eddac2fc0000198a03dbc28c73a6f44594ab040b242bdc82dfeabb7c1ab805884b5438ee0a48a8e2b52ca062
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"i18next-http-middleware@npm:^3.5.0":
|
||||
version: 3.5.0
|
||||
resolution: "i18next-http-middleware@npm:3.5.0"
|
||||
checksum: c3669f04efd8967729a50fa06be1c7449612e3f2555815a3399d35dcf9404e35182a12cc68c1ed3bbe5da73d868dbd2ac62553be8c1a7ce0d551018dc1e223fa
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"i18next@npm:^23.10.1":
|
||||
version: 23.10.1
|
||||
resolution: "i18next@npm:23.10.1"
|
||||
dependencies:
|
||||
"@babel/runtime": ^7.23.2
|
||||
checksum: 4aec10ddb0bb841f15b9b023daa59977052bc706ca4e94643b12b17640731862bde596c9797491638f6d9e7f125722ea9b1e87394c7aebbb72f45c20396f79d9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"iconv-lite@npm:0.4.24":
|
||||
version: 0.4.24
|
||||
resolution: "iconv-lite@npm:0.4.24"
|
||||
|
|
Ładowanie…
Reference in New Issue