Add more translations

pull/260/head
Candid Dauth 2024-04-07 13:49:21 +02:00
rodzic 4cb2f52928
commit 14c3ab1a4a
11 zmienionych plików z 270 dodań i 97 usunięć

Wyświetl plik

@ -37,6 +37,22 @@
"disconnected-header": "Verbindung unterbrochen",
"disconnected": "Die Verbindung zum Server ist verloren gegangen. Verbindung wird wiederhergestellt…"
},
"custom-import-dialog": {
"existing-type": "Existierender Objekttyp „{{name}}“",
"import-type": "Objekttyp „{{name}}“ importieren",
"no-import": "Nicht importieren",
"import-error": "Fehler beim Importieren",
"dialog-title": "Benutzerdefinierter Import",
"ok-label": "Importieren",
"type": "Objekttyp",
"map-to": "Importieren als…",
"markers": "Marker des Typs {{typeName}} ({{count}})",
"lines_one": "Linie des Typs {{typeName}} ({{count}})",
"lines_other": "Linien des Typs {{typeName}} ({{count}})",
"untyped-markers": "Marker ohne Typ ({{count}})",
"untyped-lines_one": "Linie/Polygon ohne Typ ({{count}})",
"untyped-lines_other": "Linien/Polygone ohne Typ ({{count}})"
},
"edit-filter-dialog": {
"title": "Filter",
"apply": "Anwenden",
@ -358,6 +374,9 @@
"units-us": "US customary (Meilen und Füße)"
},
"route-form": {
"route-description-outer": "Route von {{inner}}",
"route-description-inner": "{{destinations}} {{mode}}",
"route-description-inner-joiner": " nach ",
"find-destination-error": "Fehler bei der Suche nach dem Wegpunkt „{{query}}“",
"some-destinations-not-found": "Manche Wegpunkte konnten nicht gefunden werden.",
"route-calculation-error": "Error calculating route",
@ -381,6 +400,60 @@
"route-form-tab": {
"tab-label": "Route"
},
"route-mode": {
"car-alt": "Auto",
"bicycle-alt": "Fahrrad",
"pedestrian-alt": "Zu Fuß",
"straight-alt": "Luftlinie",
"car-title": "Mit dem Auto",
"bicycle-title": "Mit dem Fahrrad",
"pedestrian-title": "Zu Fuß",
"straight-title": "Luftlinie",
"car": "Auto",
"hgv": "LKW",
"bicycle": "Fahrrad",
"road-bike": "Rennrad",
"mountain-bike": "Mountainbike",
"electric-bike": "Pedelec",
"walking": "Zu Fuß",
"hiking": "Wandern",
"wheelchair": "Rollstuhl",
"straight": "Luftlinie",
"fastest": "Schnellste Route",
"shortest": "Kürzteste Route",
"avoid-highways": "Autobahnen vermeiden",
"avoid-toll-roads": "Mautstraßen vermeiden",
"avoid-ferries": "Fähren vermeiden",
"avoid-fords": "Furten vermeiden",
"avoid-steps": "Treppen vermeiden",
"custom-alt": "Benutzerdefiniert",
"load-details": "Details laden (Höhenprofil, Straßenqualität, …)"
},
"search-box": {
"close-alt": "Schließen",
"resize-tooltip": "Größe durch Ziehen verändern, durch Klick zurücksetzen"
},
"search-form": {
"search-description": "Suche nach {{query}}",
"search-error": "Fehler bei der Suche",
"search-alt": "Suchen",
"clear-alt": "Verstecken",
"auto-zoom": "Automatisch zu den Ergebnissen zoomen",
"zoom-to-all": "Zur Übersicht aller Ergebnisse zoomen"
},
"search-form-tab": {
"tab-label": "Suche"
},
"search-results": {
"no-results": "Es wurden keine Ergebnisse gefunden.",
"zoom-to-result-tooltip": "Zum Suchergebnis zoomen",
"zoom-to-result-alt": "Zoomen",
"show-details-tooltip": "Details anzeigen",
"show-details-alt": "Details",
"select-all": "Alle auswählen",
"add-to-map-label": "Zur Karte hinzufügen",
"custom-type-mapping": "Benutzerdefiniert…"
},
"toolbox-add-dropdown": {
"label": "Erstellen",
"manage-types": "Objekttypen verwalten"

Wyświetl plik

@ -37,6 +37,24 @@
"disconnected-header": "Disconnected",
"disconnected": "The connection to the server was lost. Trying to reconnect…"
},
"custom-import-dialog": {
"existing-type": "Existing type “{{name}}”",
"import-type": "Import type “{{name}}”",
"no-import": "Do not import",
"import-error": "Error importing to map",
"dialog-title": "Custom Import",
"ok-label": "Import",
"type": "Type",
"map-to": "Map to…",
"markers_one": "Marker of type {{typeName}} ({{count}})",
"markers_other": "Markers of type {{typeName}} ({{count}})",
"lines_one": "Line of type {{typeName}} ({{count}})",
"lines_other": "Lines of type {{typeName}} ({{count}})",
"untyped-markers_one": "Untyped marker ({{count}})",
"untyped-markers_other": "Untyped markers ({{count}})",
"untyped-lines_one": "Untyped line/polygon ({{count}})",
"untyped-lines_other": "Untyped lines/polygons ({{count}})"
},
"edit-filter-dialog": {
"title": "Filter",
"apply": "Apply",
@ -358,6 +376,9 @@
"units-us": "US customary (miles, feet)"
},
"route-form": {
"route-description-outer": "Route from {{inner}}",
"route-description-inner": "{{destinations}} {{mode}}",
"route-description-inner-joiner": " to ",
"find-destination-error": "Error finding destination “{{query}}”",
"some-destinations-not-found": "Some destinations could not be found.",
"route-calculation-error": "Error calculating route",
@ -381,6 +402,61 @@
"route-form-tab": {
"tab-label": "Route"
},
"route-mode": {
"car-alt": "Car",
"bicycle-alt": "Bicycle",
"pedestrian-alt": "Foot",
"straight-alt": "Straight",
"car-title": "Go by car",
"bicycle-title": "Go by bicycle",
"pedestrian-title": "Go on foot",
"straight-title": "Go in a straight line",
"car": "Car",
"hgv": "HGV",
"bicycle": "Bicycle",
"road-bike": "Road bike",
"mountain-bike": "Mountain bike",
"electric-bike": "Electric bike",
"walking": "Walking",
"hiking": "Hiking",
"wheelchair": "Wheelchair",
"straight": "Straight line",
"fastest": "Fastest",
"shortest": "Shortest",
"avoid-highways": "Avoid highways",
"avoid-toll-roads": "Avoid toll roads",
"avoid-ferries": "Avoid ferries",
"avoid-fords": "Avoid fords",
"avoid-steps": "Avoid steps",
"custom-alt": "Custom",
"load-details": "Load route details (elevation, road types, …)"
},
"search-box": {
"close-alt": "Close",
"resize-tooltip": "Drag to resize, click to reset"
},
"search-form": {
"search-description": "Search for {{query}}",
"search-error": "Search error",
"search-alt": "Search",
"clear-alt": "Clear",
"auto-zoom": "Auto-zoom to results",
"zoom-to-all": "Zoom to all results"
},
"search-form-tab": {
"tab-label": "Search"
},
"search-results": {
"no-results": "No results have been found.",
"zoom-to-result-tooltip": "Zoom to result",
"zoom-to-result-alt": "Zoom",
"show-details-tooltip": "Show details",
"show-details-alt": "Details",
"select-all": "Select all",
"add-to-map-label_one": "Add selected item to map",
"add-to-map-label_other": "Add selected items to map",
"custom-type-mapping": "Custom type mapping…"
},
"toolbox-add-dropdown": {
"label": "Add",
"manage-types": "Manage types"

Wyświetl plik

@ -6,9 +6,11 @@
import type { SearchBoxEventMap, SearchBoxTab, WritableSearchBoxContext } from "../facil-map-context-provider/search-box-context";
import mitt from "mitt";
import { injectContextRequired, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const i18n = useI18n();
const tabs = reactive(new Map<string, SearchBoxTab>());
const activeTabId = ref<string | undefined>();
@ -228,7 +230,7 @@
href="javascript:"
@click="tab.onClose()"
draggable="false"
><Icon icon="remove" alt="Close"></Icon></a>
><Icon icon="remove" :alt="i18n.t('search-box.close-alt')"></Icon></a>
</li>
</ul>
</div>
@ -243,7 +245,7 @@
v-show="!context.isNarrow"
href="javascript:"
class="fm-search-box-resize"
v-tooltip.right="'Drag to resize, click to reset'"
v-tooltip.right="i18n.t('search-box.resize-tooltip')"
ref="resizeHandleRef"
><Icon icon="resize-horizontal"></Icon></a>
</div>

Wyświetl plik

@ -7,10 +7,12 @@
import SearchBoxTab from "../search-box/search-box-tab.vue";
import { injectContextRequired, requireMapContext, requireSearchBoxContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import type { WritableSearchFormTabContext } from "../facil-map-context-provider/search-form-tab-context";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const mapContext = requireMapContext(context);
const searchBoxContext = requireSearchBoxContext(context);
const i18n = useI18n();
const searchForm = ref<InstanceType<typeof SearchForm>>();
@ -38,7 +40,7 @@
<template>
<SearchBoxTab
:id="`fm${context.id}-search-form-tab`"
title="Search"
:title="i18n.t('search-form-tab.tab-label')"
:hashQuery="hashQuery"
class="fm-search-form-tab"
>

Wyświetl plik

@ -14,6 +14,7 @@
import { computed, reactive, ref, watch } from "vue";
import DropdownMenu from "../ui/dropdown-menu.vue";
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { useI18n } from "../../utils/i18n";
const emit = defineEmits<{
"hash-query-change": [query: HashQuery | undefined];
@ -24,6 +25,7 @@
const mapContext = requireMapContext(context);
const toasts = useToasts();
const i18n = useI18n();
const layerId = Util.stamp(mapContext.value.components.searchResultsLayer);
@ -49,10 +51,10 @@
return {
query: loadedSearchString.value,
...(zoomDestination.value && normalizeZoomDestination(mapContext.value.components.map, zoomDestination.value)),
description: `Search for ${loadedSearchString.value}`
description: i18n.t("search-form.search-description", { query: loadedSearchString.value })
};
} else if (loadingSearchString.value)
return { query: loadingSearchString.value, description: `Search for ${loadedSearchString.value}` };
return { query: loadingSearchString.value, description: i18n.t("search-form.search-description", { query: loadedSearchString.value }) };
else
return undefined;
});
@ -132,7 +134,7 @@
}
}
} catch(err) {
toasts.showErrorToast(`fm${context.id}-search-form-error`, "Search error", err);
toasts.showErrorToast(`fm${context.id}-search-form-error`, i18n.t("search-form.search-error"), err);
return;
}
}
@ -194,7 +196,7 @@
type="submit"
class="btn btn-secondary"
>
<Icon icon="search" alt="Search"></Icon>
<Icon icon="search" :alt="i18n.t('search-form.search-alt')"></Icon>
</button>
<button
v-if="searchResults || mapResults || fileResult"
@ -202,7 +204,7 @@
class="btn btn-secondary"
@click="reset()"
>
<Icon icon="remove" alt="Clear"></Icon>
<Icon icon="remove" :alt="i18n.t('search-form.clear-alt')"></Icon>
</button>
<DropdownMenu noWrapper>
<li>
@ -211,7 +213,7 @@
class="dropdown-item"
@click.capture.stop.prevent="storage.autoZoom = !storage.autoZoom"
>
<Icon :icon="storage.autoZoom ? 'check' : 'unchecked'"></Icon> Auto-zoom to results
<Icon :icon="storage.autoZoom ? 'check' : 'unchecked'"></Icon> {{i18n.t("search-form.auto-zoom")}}
</a>
</li>
@ -221,7 +223,7 @@
class="dropdown-item"
@click.capture.stop.prevent="storage.zoomToAll = !storage.zoomToAll"
>
<Icon :icon="storage.zoomToAll ? 'check' : 'unchecked'"></Icon> Zoom to all results
<Icon :icon="storage.zoomToAll ? 'check' : 'unchecked'"></Icon> {{i18n.t("search-form.zoom-to-all")}}
</a>
</li>
</DropdownMenu>

Wyświetl plik

@ -10,10 +10,12 @@
import { injectContextRequired, requireClientContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import { type LineWithTags, type MarkerWithTags, addToMap, searchResultToLineWithTags, searchResultToMarkerWithTags } from "../../utils/add";
import { formatTypeName, getOrderedTypes } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const toasts = useToasts();
const i18n = useI18n();
const props = withDefaults(defineProps<{
customTypes?: FileResultObject["types"];
@ -43,25 +45,25 @@
for (const type of orderedTypes.value) {
if (type.name == customType.name && type.type == customType.type)
recommendedOptions.push({ key: `e${type.id}`, value: `e${type.id}`, text: `Existing type “${formatTypeName(type.name)}` });
recommendedOptions.push({ key: `e${type.id}`, value: `e${type.id}`, text: i18n.t("custom-import-dialog.existing-type", { name: formatTypeName(type.name) }) });
}
if (client.value.writable == 2 && !typeExists(client.value, customType))
recommendedOptions.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: `Import type “${customType.name}` });
recommendedOptions.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: i18n.t("custom-import-dialog.import-type", { name: customType.name }) });
recommendedOptions.push({ key: "false1", value: false, text: "Do not import" });
recommendedOptions.push({ key: "false1", value: false, text: i18n.t("custom-import-dialog.no-import") });
const otherOptions: Option[] = [];
for (const type of orderedTypes.value) {
if (type.name != customType.name && type.type == customType.type)
otherOptions.push({ key: `e${type.id}`, value: `e${type.id}`, text: `Existing type “${formatTypeName(type.name)}` });
otherOptions.push({ key: `e${type.id}`, value: `e${type.id}`, text: i18n.t("custom-import-dialog.existing-type", { name: formatTypeName(type.name) }) });
}
for (const [customTypeId2, customType2] of Object.entries(props.customTypes)) {
if (client.value.writable == 2 && customType2.type == customType.type && customTypeId2 != customTypeId && !typeExists(client.value, customType2))
otherOptions.push({ key: `i${customTypeId2}`, value: `i${customTypeId2}`, text: `Import type “${customType2.name}` });
otherOptions.push({ key: `i${customTypeId2}`, value: `i${customTypeId2}`, text: i18n.t("custom-import-dialog.import-type", { name: customType2.name }) });
}
@ -89,17 +91,17 @@
const untypedMarkerMappingOptions = computed(() => {
const options: Array<{ key: string; value: string | false; text: string }> = [];
options.push({ key: "false", value: false, text: "Do not import" });
options.push({ key: "false", value: false, text: i18n.t("custom-import-dialog.no-import") });
for (const customTypeId of Object.keys(props.customTypes)) {
const customType = props.customTypes[customTypeId as any];
if (client.value.writable && customType.type == "marker" && !typeExists(client.value, customType))
options.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: `Import type “${customType.name}` });
options.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: i18n.t("custom-import-dialog.import-type", { name: customType.name }) });
}
for (const type of orderedTypes.value) {
if (type.type == "marker")
options.push({ key: `e${type.id}`, value: `e${type.id}`, text: `Existing type “${formatTypeName(type.name)}` });
options.push({ key: `e${type.id}`, value: `e${type.id}`, text: i18n.t("custom-import-dialog.existing-type", { name: formatTypeName(type.name) }) });
}
return options;
@ -107,17 +109,17 @@
const untypedLineMappingOptions = computed(() => {
const options: Array<{ key: string; value: string | false; text: string }> = [];
options.push({ key: "false", value: false, text: "Do not import" });
options.push({ key: "false", value: false, text: i18n.t("custom-import-dialog.no-import") });
for (const customTypeId of Object.keys(props.customTypes)) {
const customType = props.customTypes[customTypeId as any];
if (client.value.writable && customType.type == "line")
options.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: `Import type “${customType.name}` });
options.push({ key: `i${customTypeId}`, value: `i${customTypeId}`, text: i18n.t("custom-import-dialog.import-type", { name: customType.name }) });
}
for (const type of orderedTypes.value) {
if (type.type == "line")
options.push({ key: `e${type.id}`, value: `e${type.id}`, text: `Existing type “${formatTypeName(type.name)}` });
options.push({ key: `e${type.id}`, value: `e${type.id}`, text: i18n.t("custom-import-dialog.existing-type", { name: formatTypeName(type.name) }) });
}
return options;
@ -155,17 +157,17 @@
modalRef.value?.modal.hide();
} catch(err) {
toasts.showErrorToast(`fm${context.id}-search-result-info-add-error`, "Error importing to map", err);
toasts.showErrorToast(`fm${context.id}-search-result-info-add-error`, i18n.t("custom-import-dialog.import-error"), err);
}
}
</script>
<template>
<ModalDialog
title="Custom Import"
:title="i18n.t('custom-import-dialog.dialog-title')"
class="fm-search-results-custom-import"
isCreate
okLabel="Import"
:okLabel="i18n.t('custom-import-dialog.ok-label')"
@submit="$event.waitUntil(save())"
ref="modalRef"
@hidden="emit('hidden')"
@ -173,14 +175,27 @@
<table class="table table-striped table-hover">
<thead>
<tr>
<th>Type</th>
<th>Map to</th>
<th>{{i18n.t("custom-import-dialog.type")}}</th>
<th>{{i18n.t("custom-import-dialog.map-to")}}</th>
</tr>
</thead>
<tbody>
<!-- eslint-disable-next-line vue/require-v-for-key -->
<tr v-for="(options, importTypeId) in customMappingOptions">
<td><label :for="`${id}-map-type-${importTypeId}`">{{customTypes[importTypeId as number].type == 'marker' ? 'Markers' : 'Lines'}} of type {{customTypes[importTypeId as number].name}} ({{activeFileResultsByType[importTypeId as number].length}})</label></td>
<td>
<label :for="`${id}-map-type-${importTypeId}`">
{{customTypes[importTypeId as number].type == 'marker'
? i18n.t("custom-import-dialog.markers", {
typeName: customTypes[importTypeId as number].name,
count: activeFileResultsByType[importTypeId as number].length
})
: i18n.t("custom-import-dialog.lines", {
typeName: customTypes[importTypeId as number].name,
count: activeFileResultsByType[importTypeId as number].length
})
}}
</label>
</td>
<td>
<select :id="`${id}-map-type-${importTypeId}`" v-model="customMapping[importTypeId as number]">
<option v-for="option in options" :key="option.key" :value="option.value">{{option.text}}</option>
@ -188,7 +203,7 @@
</td>
</tr>
<tr v-if="untypedMarkers.length > 0">
<td><label :for="`${id}-map-untyped-markers`">Untyped markers ({{untypedMarkers.length}})</label></td>
<td><label :for="`${id}-map-untyped-markers`">{{i18n.t("custom-import-dialog.untyped-markers", { count: untypedMarkers.length })}}</label></td>
<td>
<select :id="`${id}-map-untyped-markers`" v-model="untypedMarkerMapping">
<option v-for="option in untypedMarkerMappingOptions" :key="option.key" :value="option.value">{{option.text}}</option>
@ -196,7 +211,7 @@
</td>
</tr>
<tr v-if="untypedLines.length > 0">
<td><label :for="`${id}-map-untyped-lines`">Untyped lines/polygons ({{untypedLines.length}})</label></td>
<td><label :for="`${id}-map-untyped-lines`">{{i18n.t("custom-import-dialog.untyped-lines", { count: untypedLines.length })}}</label></td>
<td>
<select :id="`${id}-map-untyped-lines`" v-model="untypedLineMapping">
<option v-for="option in untypedLineMappingOptions" :key="option.key" :value="option.value">{{option.text}}</option>

Wyświetl plik

@ -15,11 +15,13 @@
import { injectContextRequired, requireClientContext, requireMapContext } from "../facil-map-context-provider/facil-map-context-provider.vue";
import AddToMapDropdown from "../ui/add-to-map-dropdown.vue";
import { formatTypeName, normalizeLineName, normalizeMarkerName } from "facilmap-utils";
import { useI18n } from "../../utils/i18n";
const context = injectContextRequired();
const client = requireClientContext(context);
const mapContext = requireMapContext(context);
const searchBoxContext = toRef(() => context.components.searchBox);
const i18n = useI18n();
const props = withDefaults(defineProps<{
searchResults?: Array<SearchResult | FileResult>;
@ -168,7 +170,7 @@
v-if="(!searchResults || searchResults.length == 0) && (!mapResults || mapResults.length == 0)"
class="alert alert-danger"
>
No results have been found.
{{i18n.t("search-results.no-results")}}
</div>
<ul v-if="mapResults && mapResults.length > 0" class="list-group">
@ -184,8 +186,8 @@
{{" "}}
<span class="result-type">({{formatTypeName(client.types[result.typeId].name)}})</span>
</span>
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.hover.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.left="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.hover.left="i18n.t('search-results.zoom-to-result-tooltip')"><Icon icon="zoom-in" :alt="i18n.t('search-results.zoom-to-result-alt')"></Icon></a>
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.left="i18n.t('search-results.show-details-tooltip')"><Icon icon="arrow-right" :alt="i18n.t('search-results.show-details-alt')"></Icon></a>
</li>
</ul>
@ -204,8 +206,8 @@
{{" "}}
<span class="result-type" v-if="result.type">({{result.type}})</span>
</span>
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.left="'Zoom to result'"><Icon icon="zoom-in" alt="Zoom"></Icon></a>
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.right="'Show details'"><Icon icon="arrow-right" alt="Details"></Icon></a>
<a v-if="showZoom" href="javascript:" @click="zoomToResult(result)" v-tooltip.left="i18n.t('search-results.zoom-to-result-tooltip')"><Icon icon="zoom-in" :alt="i18n.t('search-results.zoom-to-result-alt')"></Icon></a>
<a href="javascript:" @click="handleOpen(result, $event)" v-tooltip.right="i18n.t('search-results.show-details-tooltip')"><Icon icon="arrow-right" :alt="i18n.t('search-results.show-details-alt')"></Icon></a>
</li>
</ul>
@ -218,10 +220,10 @@
class="btn btn-secondary btn-sm"
:class="{ active: isAllSelected }"
@click="toggleSelectAll"
>Select all</button>
>{{i18n.t("search-results.select-all")}}</button>
<AddToMapDropdown
:label="`Add selected item${activeSearchResults.length == 1 ? '' : 's'} to map`"
:label="i18n.t('search-results.add-to-map-label', { count: activeSearchResults.length })"
:markers="activeMarkersWithTags"
:lines="activeLinesWithTags"
size="sm"
@ -233,7 +235,7 @@
href="javascript:"
class="dropdown-item"
@click="customImport = true"
>Custom type mapping</a>
>{{i18n.t("search-results.custom-type-mapping")}}</a>
</li>
</template>
</AddToMapDropdown>

Wyświetl plik

@ -1,6 +1,6 @@
<script setup lang="ts">
import { type SlotsType, computed, defineComponent, h, ref, shallowRef, useSlots, watch, watchEffect } from "vue";
import { maxSizeModifiers, type ButtonSize, type ButtonVariant, useMaxBreakpoint } from "../../utils/bootstrap";
import { getMaxSizeModifiers, type ButtonSize, type ButtonVariant, useMaxBreakpoint } from "../../utils/bootstrap";
import Dropdown from "bootstrap/js/dist/dropdown";
import vLinkDisabled from "../../utils/link-disabled";
import type { TooltipPlacement } from "../../utils/tooltip";
@ -26,6 +26,7 @@
tabindex?: number;
tooltip?: string; // TODO
tooltipPlacement?: TooltipPlacement;
maxWidth?: string;
}>(), {
isOpen: undefined,
noWrapper: false,
@ -67,7 +68,7 @@
...defaultConfig,
modifiers: [
...(defaultConfig.modifiers ?? []),
...maxSizeModifiers
...getMaxSizeModifiers({ maxWidth: props.maxWidth })
],
strategy: "fixed"
})

Wyświetl plik

@ -4,7 +4,7 @@
import Tooltip from "bootstrap/js/dist/tooltip";
import { useResizeObserver } from "../../utils/vue";
import { useDomEventListener } from "../../utils/utils";
import { maxSizeModifiers } from "../../utils/bootstrap";
import { getMaxSizeModifiers } from "../../utils/bootstrap";
/**
* Like Bootstrap Popover, but uses an existing popover element rather than creating a new one. This way, the popover
@ -70,7 +70,7 @@
...defaultConfig,
modifiers: [
...(defaultConfig.modifiers ?? []),
...maxSizeModifiers
...getMaxSizeModifiers()
],
strategy: "fixed"
})

Wyświetl plik

@ -1,4 +1,4 @@
<script lang="ts">
<script setup lang="ts">
import type { RouteMode as RouteModeType } from "facilmap-types";
import { type DecodedRouteMode, decodeRouteMode, encodeRouteMode } from "facilmap-utils";
import Icon from "./icon.vue";
@ -6,11 +6,29 @@
import { getUniqueId } from "../../utils/utils";
import vTooltip, { type TooltipPlacement } from "../../utils/tooltip";
import DropdownMenu from "../ui/dropdown-menu.vue";
import { useI18n } from "../../utils/i18n";
type Mode = Exclude<DecodedRouteMode['mode'], 'track'>;
type Type = DecodedRouteMode['type'];
const constants: {
const i18n = useI18n();
const props = withDefaults(defineProps<{
modelValue: RouteModeType;
tabindex?: number;
disabled?: boolean;
tooltipPlacement?: TooltipPlacement;
}>(), {
tooltipPlacement: "top"
});
const emit = defineEmits<{
"update:modelValue": [value: RouteModeType];
}>();
const id = getUniqueId("fm-route-mode");
const constants = computed((): {
modes: Array<Mode>;
modeIcon: Record<Mode, string>;
modeAlt: Record<Mode, string>;
@ -22,7 +40,7 @@
avoid: DecodedRouteMode['avoid'];
avoidAllowed: Record<DecodedRouteMode['avoid'][0], (mode: DecodedRouteMode['mode'], type: Type) => boolean>;
avoidText: Record<DecodedRouteMode['avoid'][0], string>;
} = {
} => ({
modes: ["car", "bicycle", "pedestrian", ""],
modeIcon: {
@ -33,17 +51,17 @@
},
modeAlt: {
car: "Car",
bicycle: "Bicycle",
pedestrian: "Foot",
"": "Straight"
car: i18n.t("route-mode.car-alt"),
bicycle: i18n.t("route-mode.bicycle-alt"),
pedestrian: i18n.t("route-mode.pedestrian-alt"),
"": i18n.t("route-mode.straight-alt")
},
modeTitle: {
car: "by car",
bicycle: "by bicycle",
pedestrian: "on foot",
"": "in a straight line"
car: i18n.t("route-mode.car-title"),
bicycle: i18n.t("route-mode.bicycle-title"),
pedestrian: i18n.t("route-mode.pedestrian-title"),
"": i18n.t("route-mode.straight-title")
},
types: {
@ -55,30 +73,30 @@
typeText: {
car: {
"": "Car",
"hgv": "HGV"
"": i18n.t("route-mode.car"),
"hgv": i18n.t("route-mode.hgv")
},
bicycle: {
"": "Bicycle",
road: "Road bike",
mountain: "Mountain bike",
electric: "Electric bike"
"": i18n.t("route-mode.bicycle"),
road: i18n.t("route-mode.road-bike"),
mountain: i18n.t("route-mode.mountain-bike"),
electric: i18n.t("route-mode.electric-bike")
},
pedestrian: {
"": "Walking",
hiking: "Hiking",
wheelchair: "Wheelchair"
"": i18n.t("route-mode.walking"),
hiking: i18n.t("route-mode.hiking"),
wheelchair: i18n.t("route-mode.wheelchair")
},
"": {
"": "Straight line"
"": i18n.t("route-mode.straight")
}
},
preferences: ["fastest", "shortest"],
preferenceText: {
fastest: "Fastest",
shortest: "Shortest"
fastest: i18n.t("route-mode.fastest"),
shortest: i18n.t("route-mode.shortest")
},
avoid: ["highways", "tollways", "ferries", "fords", "steps"],
@ -96,30 +114,13 @@
},
avoidText: {
highways: "highways",
tollways: "toll roads",
ferries: "ferries",
fords: "fords",
steps: "steps"
highways: i18n.t("route-mode.avoid-highways"),
tollways: i18n.t("route-mode.avoid-toll-roads"),
ferries: i18n.t("route-mode.avoid-ferries"),
fords: i18n.t("route-mode.avoid-fords"),
steps: i18n.t("route-mode.avoid-steps")
}
}
</script>
<script setup lang="ts">
const props = withDefaults(defineProps<{
modelValue: RouteModeType;
tabindex?: number;
disabled?: boolean;
tooltipPlacement?: TooltipPlacement;
}>(), {
tooltipPlacement: "top"
});
const emit = defineEmits<{
"update:modelValue": [value: RouteModeType];
}>();
const id = getUniqueId("fm-route-mode");
}));
const decodedMode = ref(decodeRouteMode(props.modelValue));
@ -134,7 +135,7 @@
}
}, { deep: true });
const types = computed(() => (Object.keys(constants.types) as Mode[]).map((mode) => constants.types[mode].map((type) => ([mode, type] as [Mode, Type]))).flat());
const types = computed(() => (Object.keys(constants.value.types) as Mode[]).map((mode) => constants.value.types[mode].map((type) => ([mode, type] as [Mode, Type]))).flat());
function isTypeActive(mode: DecodedRouteMode['mode'], type: DecodedRouteMode['type']): boolean {
return (!mode && !decodedMode.value.mode || mode == decodedMode.value.mode) && (!type && !decodedMode.value.type || type == decodedMode.value.type);
@ -146,7 +147,7 @@
if(decodedMode.value.avoid) {
for(let i=0; i < decodedMode.value.avoid.length; i++) {
if(!constants.avoidAllowed[decodedMode.value.avoid[i]](decodedMode.value.mode, decodedMode.value.type))
if(!constants.value.avoidAllowed[decodedMode.value.avoid[i]](decodedMode.value.mode, decodedMode.value.type))
decodedMode.value.avoid.splice(i--, 1);
}
}
@ -176,10 +177,9 @@
v-model="decodedMode.mode"
:value="mode"
:tabindex="tabindex != null ? tabindex + idx : undefined"
v-tooltip:[tooltipPlacement]="`Go ${constants.modeTitle[mode]}`"
:disabled="disabled"
/>
<label class="btn btn-secondary" :for="`${id}-mode-${mode}`">
<label class="btn btn-secondary" :for="`${id}-mode-${mode}`" v-tooltip:[tooltipPlacement]="constants.modeTitle[mode]">
<Icon :icon="constants.modeIcon[mode]" :alt="constants.modeAlt[mode]"></Icon>
</label>
</template>
@ -192,9 +192,10 @@
:isDisabled="disabled"
noWrapper
menuClass="fm-route-mode-customize"
maxWidth="32rem"
>
<template #label>
<Icon icon="cog" alt="Custom"/>
<Icon icon="cog" :alt="i18n.t('route-mode.custom-alt')"/>
</template>
<template #default>
@ -204,7 +205,7 @@
href="javascript:"
@click.capture.stop.prevent="decodedMode.details = !decodedMode.details"
>
<Icon :icon="decodedMode.details ? 'check' : 'unchecked'"></Icon> Load route details (elevation, road types, )
<Icon :icon="decodedMode.details ? 'check' : 'unchecked'"></Icon> {{i18n.t("route-mode.load-details")}}
</a>
</li>
@ -248,7 +249,7 @@
href="javascript:"
@click.capture.stop.prevent="toggleAvoid(avoid)"
>
<Icon :icon="decodedMode.avoid.includes(avoid) ? 'check' : 'unchecked'"></Icon> Avoid {{constants.avoidText[avoid]}}
<Icon :icon="decodedMode.avoid.includes(avoid) ? 'check' : 'unchecked'"></Icon> {{constants.avoidText[avoid]}}
</a>
</li>
</template>
@ -269,7 +270,6 @@
}
.fm-route-mode-customize {
width: 380px;
font-size: 0; /* https://stackoverflow.com/a/5647640/242365 */
li {

Wyświetl plik

@ -57,7 +57,7 @@ export type ButtonSize = "lg" | "sm";
* An array of popper modifiers that uses popper-max-size-modifier to shrink the popover to prevent overflow
* rather than move it, as is the default in Bootstrap.
*/
export const maxSizeModifiers: Array<Partial<Modifier<any, any>>> = [
export const getMaxSizeModifiers = ({ maxWidth = "30rem" }: { maxWidth?: string } = {}): Array<Partial<Modifier<any, any>>> => [
{
...maxSize,
options: {
@ -75,7 +75,7 @@ export const maxSizeModifiers: Array<Partial<Modifier<any, any>>> = [
state.styles.popper = {
...state.styles.popper,
maxWidth: `min(30rem, ${width}px)`,
maxWidth: `min(${maxWidth}, ${width}px)`,
maxHeight: `${height}px`
}
}