Internationalization
Sonarqube Scanner / Prepare environment (push) Successful in 20s
Sonarqube Scanner / Build and test (push) Failing after 36s
Sonarqube Scanner / SonarQube analysis (push) Skipped
Sonarqube Scanner / Deploy to Server (push) Successful in 9s

This commit is contained in:
Florian Zumpe
2026-10-01 09:41:01 +02:00
parent 607e5450e1
commit aff88eba1b
41 changed files with 1368 additions and 636 deletions
+229 -277
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#315d3a">
<title>Wegwichtel</title>
<title data-i18n="html.title.wegwichtel"></title>
<link rel="stylesheet" href="css/bootstrap.css?v=0.12.14">
<link rel="stylesheet" href="vendor/jquery-ui/jquery-ui-1.14.2.min.css?v=0.12.14">
<link rel="stylesheet" href="css/app.css?v=0.12.14">
@@ -12,51 +12,51 @@
</head>
<body>
<div class="info-control">
<button id="info-button" class="info-button" type="button" aria-label="Informationsmenü öffnen" title="Informationen" aria-haspopup="true" aria-expanded="false" aria-controls="info-dropdown">
<span aria-hidden="true">i</span>
<button id="info-button" class="info-button" type="button" aria-label="" title="" aria-haspopup="true" aria-expanded="false" aria-controls="info-dropdown" data-i18n-title="legal-ui.informationen" data-i18n-aria-label="legal-ui.hilfe.und.informationen.offnen">
<span aria-hidden="true" data-i18n="html.span.i"></span>
</button>
<nav id="info-dropdown" class="info-dropdown" aria-label="Rechtliche und technische Informationen" hidden>
<button type="button" data-info-target="privacy">Datenschutzerklärung</button>
<button type="button" data-info-target="imprint">Impressum</button>
<button type="button" data-info-target="technology">Cookies und verwendete JavaScripts</button>
<nav id="info-dropdown" class="info-dropdown" aria-label="" hidden data-i18n-aria-label="html.info-dropdown.hilfe.und.informationen">
<button type="button" data-info-target="privacy" data-i18n="legal-ui.datenschutzerklarung"></button>
<button type="button" data-info-target="imprint" data-i18n="legal-ui.impressum"></button>
<button type="button" data-info-target="technology" data-i18n="legal-ui.speichern.und.berechtigungen"></button>
</nav>
</div>
<div id="bootstrap-screen" aria-live="polite" aria-busy="true">
<section class="bootstrap-card">
<header class="bootstrap-brand">
<svg class="bootstrap-logo" viewBox="0 0 96 96" aria-hidden="true" focusable="false">
<circle cx="48" cy="48" r="39" fill="#fff7d7" stroke="#f4b400" stroke-width="6"/>
<path d="M48 15 57 39 81 48 57 57 48 81 39 57 15 48 39 39Z" fill="#d9ddd8"/>
<path d="m48 48 24-23-13 31Z" fill="#e53935"/>
<path d="m48 48-24 23 13-31Z" fill="#2d9cdb"/>
<circle cx="48" cy="48" r="6" fill="#f8fbf7" stroke="#315d3a" stroke-width="3"/>
<circle cx="80" cy="16" r="8" fill="#fff" stroke="#f4b400" stroke-width="5"/>
<svg class="bootstrap-logo" viewbox="0 0 96 96" aria-hidden="true" focusable="false">
<circle cx="48" cy="48" r="39" fill="#fff7d7" stroke="#f4b400" stroke-width="6"></circle>
<path d="M48 15 57 39 81 48 57 57 48 81 39 57 15 48 39 39Z" fill="#d9ddd8"></path>
<path d="m48 48 24-23-13 31Z" fill="#e53935"></path>
<path d="m48 48-24 23 13-31Z" fill="#2d9cdb"></path>
<circle cx="48" cy="48" r="6" fill="#f8fbf7" stroke="#315d3a" stroke-width="3"></circle>
<circle cx="80" cy="16" r="8" fill="#fff" stroke="#f4b400" stroke-width="5"></circle>
</svg>
<div>
<h1>Wegwichtel</h1>
<p>Der Wegwichtel bereitet Routen und Medien vor.</p>
<h1 data-i18n="html.title.wegwichtel"></h1>
<p data-i18n="html.p.gleich.kannst.du.mit.dem.wegwichtel.losziehen"></p>
</div>
</header>
<div class="bootstrap-hero" aria-hidden="true">
<svg viewBox="0 0 720 300" preserveAspectRatio="xMidYMid slice" focusable="false">
<rect width="720" height="300" rx="32" fill="#eef7f1"/>
<circle cx="255" cy="68" r="27" fill="#ffd66b"/>
<path d="M0 150C92 84 172 113 250 156c81 44 139-14 228-36 82-20 149 2 242 62v118H0Z" fill="#a9d4ca"/>
<path d="M0 191c111-67 217-16 306 25 89 40 179-50 283-49 53 0 92 15 131 43v90H0Z" fill="#74aa62"/>
<path d="M0 224c93-51 194-19 283 17 85 34 189-35 279-31 60 3 110 18 158 51v39H0Z" fill="#5e914e"/>
<svg viewbox="0 0 720 300" preserveaspectratio="xMidYMid slice" focusable="false">
<rect width="720" height="300" rx="32" fill="#eef7f1"></rect>
<circle cx="255" cy="68" r="27" fill="#ffd66b"></circle>
<path d="M0 150C92 84 172 113 250 156c81 44 139-14 228-36 82-20 149 2 242 62v118H0Z" fill="#a9d4ca"></path>
<path d="M0 191c111-67 217-16 306 25 89 40 179-50 283-49 53 0 92 15 131 43v90H0Z" fill="#74aa62"></path>
<path d="M0 224c93-51 194-19 283 17 85 34 189-35 279-31 60 3 110 18 158 51v39H0Z" fill="#5e914e"></path>
<g fill="#477d49">
<circle cx="75" cy="200" r="34"/><circle cx="118" cy="210" r="26"/><circle cx="610" cy="210" r="31"/><circle cx="654" cy="218" r="25"/>
<circle cx="75" cy="200" r="34"></circle><circle cx="118" cy="210" r="26"></circle><circle cx="610" cy="210" r="31"></circle><circle cx="654" cy="218" r="25"></circle>
</g>
<g fill="#607d8b" opacity=".9">
<path d="M305 174h28v36h-28zM339 161h35v49h-35zM381 180h34v30h-34z"/>
<path d="m350 160 8-24 8 24Z"/>
<path d="M305 174h28v36h-28zM339 161h35v49h-35zM381 180h34v30h-34z"></path>
<path d="m350 160 8-24 8 24Z"></path>
</g>
<path d="M250 300c34-52 88-69 137-74 53-5 113 5 153 22-65 9-127 29-155 52Z" fill="#f6dfb7"/>
<path d="M250 300c34-52 88-69 137-74 53-5 113 5 153 22-65 9-127 29-155 52Z" fill="#f6dfb7"></path>
<g transform="translate(538 117)">
<rect x="36" y="24" width="13" height="121" rx="6" fill="#7b5231"/>
<path d="M0 17h105l25 21-25 21H0Z" fill="#f5a332"/>
<path d="M14 65h91l22 19-22 19H14Z" fill="#4f914b"/>
<rect x="36" y="24" width="13" height="121" rx="6" fill="#7b5231"></rect>
<path d="M0 17h105l25 21-25 21H0Z" fill="#f5a332"></path>
<path d="M14 65h91l22 19-22 19H14Z" fill="#4f914b"></path>
</g>
</svg>
</div>
@@ -64,49 +64,49 @@
<div class="bootstrap-action-slot">
<output id="bootstrap-action-status" class="bootstrap-action-status" aria-live="polite">
<span class="bootstrap-loading-dot" aria-hidden="true"></span>
<span id="bootstrap-status-text" class="bootstrap-status-text">Wegwichtel wird vorbereitet …</span>
<span id="bootstrap-status-text" class="bootstrap-status-text" data-i18n="html.bootstrap-status-text.wegwichtel.wird.vorbereitet"></span>
</output>
<button id="bootstrap-enter" class="bootstrap-enter" type="button" hidden>
<span>Los geht’s</span>
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 5 7 7-7 7"/></svg>
<span data-i18n="html.span.los.gehts"></span>
<svg viewbox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 5 7 7-7 7"></path></svg>
</button>
</div>
<p id="bootstrap-error" class="bootstrap-error" hidden></p>
<button id="bootstrap-retry" class="bootstrap-retry" type="button" hidden>Erneut versuchen</button>
<p id="bootstrap-ready" class="bootstrap-ready" hidden>Alles ist bereit.</p>
<button id="bootstrap-retry" class="bootstrap-retry" type="button" hidden data-i18n="html.bootstrap-retry.erneut.versuchen"></button>
<p id="bootstrap-ready" class="bootstrap-ready" hidden data-i18n="html.bootstrap-ready.alles.ist.bereit"></p>
<div class="bootstrap-features" aria-label="Wegwichtel Funktionen">
<div class="bootstrap-features" aria-label="" data-i18n-aria-label="html.div.wegwichtel.funktionen">
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m3 7 8-3 10 4 8-3v20l-8 3-10-4-8 3Zm8-3v20m10-16v20"/></svg>
<span>Routen<br>entdecken</span>
<svg viewbox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m3 7 8-3 10 4 8-3v20l-8 3-10-4-8 3Zm8-3v20m10-16v20"></path></svg>
<span><span data-i18n="html.span.strecken"></span><br><span data-i18n="html.span.entdecken"></span></span>
</div>
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="M7 18v-4a9 9 0 0 1 18 0v4M7 18H4v7h5v-7Zm18 0h3v7h-5v-7Z"/></svg>
<span>Spannende<br>Geschichten</span>
<svg viewbox="0 0 32 32" aria-hidden="true" focusable="false"><path d="M7 18v-4a9 9 0 0 1 18 0v4M7 18H4v7h5v-7Zm18 0h3v7h-5v-7Z"></path></svg>
<span><span data-i18n="html.span.spannende"></span><br><span data-i18n="html.span.geschichten"></span></span>
</div>
<div>
<svg viewBox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m16 3 3.9 8 8.8 1.3-6.4 6.2 1.5 8.8-7.8-4.1-7.8 4.1 1.5-8.8-6.4-6.2 8.8-1.3Z"/></svg>
<span>Kindgerechte<br>Erlebnisse</span>
<svg viewbox="0 0 32 32" aria-hidden="true" focusable="false"><path d="m16 3 3.9 8 8.8 1.3-6.4 6.2 1.5 8.8-7.8-4.1-7.8 4.1 1.5-8.8-6.4-6.2 8.8-1.3Z"></path></svg>
<span><span data-i18n="html.span.kindgerechte"></span><br><span data-i18n="html.span.erlebnisse"></span></span>
</div>
</div>
<p id="android-browser-hint" class="android-browser-hint" hidden></p>
<div class="privacy-summary">
<p><strong>Cookies:</strong> Diese Anwendung setzt derzeit keine Cookies und verwendet weder Werbung noch Tracking.</p>
<p data-i18n="html.p.hier.gibt.es.keine.werbung.wir.verfolgen"></p>
<label class="switch-row switch-disabled">
<span>
<strong>Optionale Cookies</strong>
<small>Nicht verwendet und deshalb dauerhaft ausgeschaltet.</small>
<strong data-i18n="html.strong.optionale.cookies"></strong>
<small data-i18n="html.small.diese.funktion.wird.nicht.verwendet"></small>
</span>
<input class="cookie-toggle" type="checkbox" role="switch" aria-checked="false" disabled>
<span class="switch-track" aria-hidden="true"></span>
</label>
<label class="switch-row">
<span>
<strong>Streckenfortschritt lokal speichern</strong>
<small>Speichert ausgewählte Routen und besuchte Streckenpunkte ausschließlich in diesem Browser.</small>
<strong data-i18n="html.strong.meinen.fortschritt.merken"></strong>
<small data-i18n="html.small.merkt.sich.auf.diesem.gerat.welche.stationen"></small>
</span>
<input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span>
@@ -114,156 +114,151 @@
<div class="offline-option-row">
<label class="switch-row">
<span>
<strong>App offline speichern</strong>
<small>Speichert nur die Dateien zur Anzeige der App. Strecken werden separat ausgewählt und gespeichert.</small>
<strong data-i18n="html.strong.app.ohne.internet.offnen"></strong>
<small data-i18n="html.small.damit.kannst.du.wegwichtel.auch.ohne.internet"></small>
</span>
<input id="offline-cache-toggle" type="checkbox" role="switch" aria-checked="false" aria-describedby="offline-cache-status">
<span class="switch-track" aria-hidden="true"></span>
</label>
<button id="offline-cache-help-button" class="offline-help-button" type="button" aria-label="Hilfe zur Offline-Speicherung" title="Was wird offline gespeichert?" aria-expanded="false" aria-controls="offline-cache-help">?</button>
<button id="offline-cache-help-button" class="offline-help-button" type="button" aria-label="" title="" aria-expanded="false" aria-controls="offline-cache-help" data-i18n-title="html.offline-cache-help-button.was.wird.fur.unterwegs.gespeichert" data-i18n-aria-label="html.offline-cache-help-button.hilfe.zum.speichern.fur.unterwegs">?</button>
</div>
<section id="offline-cache-help" class="offline-help" aria-labelledby="offline-cache-help-title" hidden>
<h2 id="offline-cache-help-title">Was wird offline gespeichert?</h2>
<p>Dieser Schalter speichert nur die App-Dateien: Startseite, Darstellung, Programme, mitgelieferte Bibliotheken, Symbole und Wichtel-Grafiken. Strecken, GPX-Dateien und Streckenmedien werden dabei nicht gespeichert.</p>
<p>Öffne eine Strecke und wähle „Strecke offline speichern“, um genau diese Strecke mit GPX, Wegpunkten, Bildern und Audio für die App zu speichern. „GPX herunterladen“ legt eine normale GPX-Datei im Download-Ordner deines Geräts ab.</p>
<p>Die Offline-Kopien für die App liegen im geschützten Speicher dieses Browsers auf diesem Gerät. Dafür musst du keinen Ordner auswählen. Dieser Schalter speichert keine GPS-Aufzeichnung.</p>
<p>Warte vor dem Losgehen, bis die Downloads abgeschlossen sind. Aktualisieren braucht Internet. Beim Ausschalten dieses Schalters werden nur die App-Kopien entfernt; separat gespeicherte Strecken, lokale Entwürfe und Streckenfortschritt bleiben erhalten.</p>
<p>Die Speicheranzeige schätzt die Belegung dieser Website. Local Storage für Entwürfe hat zusätzlich eine eigene Grenze. Der Browser kann Offline-Dateien bei knappem Speicher entfernen; beim Löschen der Website-Daten gehen sie ebenfalls verloren. Externe Karten werden nicht automatisch gespeichert.</p>
<h2 id="offline-cache-help-title" data-i18n="html.offline-cache-help-button.was.wird.fur.unterwegs.gespeichert"></h2>
<p data-i18n="html.p.mit.diesem.schalter.kannst.du.wegwichtel.auch"></p>
<p data-i18n="html.p.offne.eine.strecke.und.tippe.auf.fur"></p>
<p data-i18n="html.p.warte.vor.dem.losgehen.bis.alles.gespeichert"></p>
<p data-i18n="html.p.alles.bleibt.auf.diesem.gerat.der.schalter"></p>
<p data-i18n="html.p.wenn.du.die.gespeicherten.daten.von.wegwichtel"></p>
</section>
<output id="offline-cache-status" aria-live="polite"></output>
<button id="offline-cache-refresh" type="button" hidden>App-Dateien aktualisieren</button>
<button id="offline-cache-refresh" type="button" hidden data-i18n="html.offline-cache-refresh.gespeicherte.app.erneuern"></button>
</div>
<section class="storage-usage" data-storage-usage aria-label="Lokaler Speicher">
<h3>Lokaler Speicher</h3>
<meter data-storage-site-meter min="0" max="1" value="0" aria-label="Belegung des Website-Speichers" hidden></meter>
<p data-storage-site-text>Speicherbelegung wird ermittelt …</p>
<meter data-storage-local-meter min="0" max="5242880" value="0" aria-label="Local Storage im Verhältnis zum Richtwert von 5 MiB"></meter>
<section class="storage-usage" data-storage-usage aria-label="" data-i18n-aria-label="html.section.platz.fur.wegwichtel">
<h3 data-i18n="html.section.platz.fur.wegwichtel"></h3>
<meter data-storage-site-meter min="0" max="1" value="0" aria-label="" hidden data-i18n-aria-label="html.meter.belegter.platz.fur.app.bilder.und.ton"></meter>
<p data-storage-site-text data-i18n="html.p.wir.schauen.wie.viel.platz.belegt.ist"></p>
<meter data-storage-local-meter min="0" max="5242880" value="0" aria-label="" data-i18n-aria-label="html.meter.belegter.platz.fur.texte.und.einstellungen.der"></meter>
<p data-storage-local-text></p>
<p>Website-Speicher: Browser-Schätzung für App-Dateien, Offline-Strecken und Medien. Local Storage: separater Speicher für Entwürfe und Einstellungen. Beide Anzeigen beziehen sich nur auf diese Website, nicht auf die gesamte Festplatte.</p>
<p data-i18n="html.p.der.erste.balken.zeigt.den.platz.fur"></p>
<output data-storage-warning hidden></output>
<button data-storage-refresh type="button">Speicheranzeige aktualisieren</button>
<button data-storage-refresh type="button" data-i18n="html.button.platz.prufen"></button>
</section>
<p id="storage-cleanup-status" class="storage-cleanup-status" hidden></p>
<progress id="bootstrap-progress" class="bootstrap-state-model" max="7" value="0" hidden></progress>
<ol class="bootstrap-state-model" hidden>
<li data-step="storage">Lokalen Speicher prüfen</li>
<li data-step="jquery">jQuery laden</li>
<li data-step="jquery-ui">jQuery UI laden</li>
<li data-step="modules">Anwendungsmodule laden</li>
<li data-step="server">Server prüfen</li>
<li data-step="routes">Routen laden</li>
<li data-step="location">Standort ermitteln</li>
<li data-step="storage" data-i18n="html.li.gespeicherte.inhalte.prufen"></li>
<li data-step="jquery" data-i18n="html.li.wegwichtel.vorbereiten"></li>
<li data-step="jquery-ui" data-i18n="html.li.bedienung.vorbereiten"></li>
<li data-step="modules" data-i18n="html.li.app.vorbereiten"></li>
<li data-step="server" data-i18n="html.li.verbindung.prufen"></li>
<li data-step="routes" data-i18n="html.li.strecken.laden"></li>
<li data-step="location" data-i18n="html.li.deinen.standort.suchen"></li>
</ol>
</section>
</div>
<main id="app-shell" hidden>
<div class="route-control">
<div class="route-control-summary" aria-live="polite">
<strong id="route-control-title">Route wählen</strong>
<small id="route-control-next">Aktive und lokale Strecken</small>
<strong id="route-control-title" data-i18n="app.strecke.wahlen"></strong>
<small id="route-control-next" data-i18n="app.strecken.fur.alle.und.meine.entwurfe"></small>
</div>
<button id="route-control-button" class="route-control-button" type="button" aria-label="Strecke auswählen oder wechseln" title="Strecke auswählen oder wechseln" aria-haspopup="true" aria-expanded="false" aria-controls="route-control-dropdown">
<button id="route-control-button" class="route-control-button" type="button" aria-label="" title="" aria-haspopup="true" aria-expanded="false" aria-controls="route-control-dropdown" data-i18n-title="html.route-control-button.strecke.auswahlen.oder.wechseln" data-i18n-aria-label="html.route-control-button.strecke.auswahlen.oder.wechseln">
<img src="images/icons/route-management.svg" alt="" aria-hidden="true">
</button>
<nav id="route-control-dropdown" class="route-control-dropdown" aria-label="Streckenauswahl und Streckenverwaltung" hidden>
<nav id="route-control-dropdown" class="route-control-dropdown" aria-label="" hidden data-i18n-aria-label="html.route-control-dropdown.strecken.auswahlen.und.bearbeiten">
<section aria-labelledby="route-control-server-title">
<h2 id="route-control-server-title">Aktive Strecken</h2>
<h2 id="route-control-server-title" data-i18n="editor-ui.strecken.fur.alle"></h2>
<ul id="route-control-server-list" class="route-control-list" aria-live="polite"></ul>
</section>
<section aria-labelledby="route-control-local-title">
<h2 id="route-control-local-title">Lokale Strecken</h2>
<h2 id="route-control-local-title" data-i18n="editor-ui.meine.entwurfe"></h2>
<ul id="route-control-local-list" class="route-control-list" aria-live="polite"></ul>
</section>
<div class="route-control-actions">
<button id="editor-button" class="editor-button" type="button" aria-label="Strecken verwalten" title="Strecken verwalten" aria-haspopup="dialog" aria-expanded="false" aria-controls="editor-dialog">Strecken verwalten</button>
<button id="route-control-new" class="editor-primary-button" type="button">Neue lokale Strecke</button>
<button id="editor-button" class="editor-button" type="button" aria-label="" title="" aria-haspopup="dialog" aria-expanded="false" aria-controls="editor-dialog" data-i18n-title="html.editor-button.strecken.bearbeiten" data-i18n-aria-label="html.editor-button.strecken.bearbeiten" data-i18n="html.editor-button.strecken.bearbeiten"></button>
<button id="route-control-new" class="editor-primary-button" type="button" data-i18n="editor-ui.neue.strecke"></button>
</div>
</nav>
</div>
<section id="routes-page" class="app-page">
<header>
<h1>Wegwichtel</h1>
<h1 data-i18n="html.title.wegwichtel"></h1>
</header>
<div class="content">
<p id="location-hint" class="notice">Standort wird für nahe Routen lokal auf diesem Gerät ermittelt.</p>
<p id="location-hint" class="notice" data-i18n="html.location-hint.wir.suchen.strecken.in.deiner.nahe.dein"></p>
<section>
<h2>Routen in deiner Nähe</h2>
<h2 data-i18n="html.h2.strecken.in.deiner.nahe"></h2>
<ul id="nearby-route-list" class="route-list" aria-live="polite">
<li><p>Routen werden geladen …</p></li>
<li><p data-i18n="editor-ui.strecken.werden.geladen"></p></li>
</ul>
</section>
<section>
<h2>Alle Routen</h2>
<label for="route-search">Routen durchsuchen</label>
<h2 data-i18n="html.h2.alle.strecken"></h2>
<label for="route-search" data-i18n="html.label.strecken.durchsuchen"></label>
<div class="search-row">
<input
id="route-search"
type="search"
autocomplete="off"
inputmode="search"
placeholder="Name, Schule oder Beschreibung">
<button id="clear-route-search" type="button">Suche leeren</button>
<input id="route-search" type="search" autocomplete="off" inputmode="search" placeholder="" data-i18n-placeholder="html.route-search.name.schule.oder.beschreibung">
<button id="clear-route-search" type="button" data-i18n="html.clear-route-search.suche.leeren"></button>
</div>
<p id="all-routes-summary" aria-live="polite">Routen werden geladen …</p>
<p id="all-routes-summary" aria-live="polite" data-i18n="editor-ui.strecken.werden.geladen"></p>
<ul id="all-route-list" class="route-list" aria-live="polite">
<li><p>Routen werden geladen …</p></li>
<li><p data-i18n="editor-ui.strecken.werden.geladen"></p></li>
</ul>
</section>
<section aria-labelledby="local-route-title">
<h2 id="local-route-title">Lokale Strecken</h2>
<p>Diese Entwürfe bestehen aus GPX- und JSON-Daten und sind ausschließlich in diesem Browser gespeichert.</p>
<h2 id="local-route-title" data-i18n="editor-ui.meine.entwurfe"></h2>
<p data-i18n="html.p.diese.strecken.hast.du.auf.diesem.gerat"></p>
<ul id="local-route-list" class="route-list" aria-live="polite">
<li><p>Lokale Strecken werden geladen …</p></li>
<li><p data-i18n="html.p.meine.entwurfe.werden.geladen"></p></li>
</ul>
</section>
</div>
</section>
<section id="route-page" class="app-page" hidden>
<header class="has-back-button">
<button class="back-button" type="button" data-page-target="#routes-page" aria-label="Zurück zur Routenauswahl" title="Zurück zur Routenauswahl">
<button class="back-button" type="button" data-page-target="#routes-page" aria-label="" title="" data-i18n-title="html.button.zuruck.zur.streckenauswahl" data-i18n-aria-label="html.button.zuruck.zur.streckenauswahl">
<img src="images/icons/back.svg" alt="">
</button>
<h1 id="route-title">Route</h1>
<h1 id="route-title" data-i18n="app.strecke"></h1>
</header>
<div class="content">
<p id="route-description"></p>
<section id="route-offline-controls" class="route-offline-controls" aria-label="Strecke auf dem Gerät speichern" hidden>
<button id="route-offline-save" type="button">Strecke offline speichern</button>
<button id="route-offline-remove" type="button" hidden>Offline-Strecke entfernen</button>
<a id="route-gpx-download" download hidden>GPX herunterladen</a>
<section id="route-offline-controls" class="route-offline-controls" aria-label="" hidden data-i18n-aria-label="html.route-offline-controls.strecke.auf.dem.gerat.speichern">
<button id="route-offline-save" type="button" data-i18n="offline-cache.fur.unterwegs.speichern"></button>
<button id="route-offline-remove" type="button" hidden data-i18n="html.route-offline-remove.gespeicherte.strecke.entfernen"></button>
<a id="route-gpx-download" download hidden data-i18n="html.route-gpx-download.weg.als.datei.herunterladen"></a>
<output id="route-offline-status" aria-live="polite"></output>
<p>Die Offline-Funktion speichert diese Strecke samt GPX, Bildern und Audio im Browser. Der GPX-Download speichert nur die GPX-Datei im Download-Ordner.</p>
<p data-i18n="html.p.fur.unterwegs.speichern.nimmt.diese.strecke.mit"></p>
</section>
<dl>
<div><dt>Länge</dt><dd id="route-distance">–</dd></div>
<div><dt>Höhenmeter</dt><dd id="route-elevation">–</dd></div>
<div><dt>Streckenpunkte</dt><dd id="route-poi-count">–</dd></div>
<div><dt data-i18n="html.dt.lange"></dt><dd id="route-distance">–</dd></div>
<div><dt data-i18n="html.dt.hohenmeter"></dt><dd id="route-elevation">–</dd></div>
<div><dt data-i18n="html.dt.stationen"></dt><dd id="route-poi-count">–</dd></div>
</dl>
<fieldset>
<legend>Routensteuerung</legend>
<button id="start-route" type="button" aria-label="Route starten" title="Route starten">
<img src="images/icons/play.svg" alt="Route starten">
<legend data-i18n="html.legend.unterwegs"></legend>
<button id="start-route" type="button" aria-label="" title="" data-i18n-title="app.strecke.starten" data-i18n-aria-label="app.strecke.starten">
<img src="images/icons/play.svg" alt="" data-i18n-alt="app.strecke.starten">
</button>
<button id="pause-route" type="button" aria-label="Route pausieren" title="Route pausieren" disabled>
<img src="images/icons/pause.svg" alt="Route pausieren">
<button id="pause-route" type="button" aria-label="" title="" disabled data-i18n-title="html.pause-route.strecke.pausieren" data-i18n-aria-label="html.pause-route.strecke.pausieren">
<img src="images/icons/pause.svg" alt="" data-i18n-alt="html.pause-route.strecke.pausieren">
</button>
<button id="stop-route" type="button" aria-label="Route beenden" title="Route beenden" disabled>
<img src="images/icons/stop.svg" alt="Route beenden">
<button id="stop-route" type="button" aria-label="" title="" disabled data-i18n-title="html.stop-route.strecke.beenden" data-i18n-aria-label="html.stop-route.strecke.beenden">
<img src="images/icons/stop.svg" alt="" data-i18n-alt="html.stop-route.strecke.beenden">
</button>
</fieldset>
<p id="tracking-status" class="notice" hidden></p>
<h2>Streckenpunkte</h2>
<h2 data-i18n="html.dt.stationen"></h2>
<ul id="poi-list" class="route-list"></ul>
</div>
</section>
<section id="poi-page" class="app-page" hidden>
<header class="has-back-button">
<button class="back-button" type="button" data-page-target="#route-page" aria-label="Zurück zur Route" title="Zurück zur Route">
<button class="back-button" type="button" data-page-target="#route-page" aria-label="" title="" data-i18n-title="html.button.zuruck.zur.strecke" data-i18n-aria-label="html.button.zuruck.zur.strecke">
<img src="images/icons/back.svg" alt="">
</button>
<h1 id="poi-title">Streckenpunkt</h1>
<h1 id="poi-title" data-i18n="html.poi-title.station"></h1>
</header>
<div class="content">
<div id="slideshow" aria-live="polite">
@@ -271,29 +266,29 @@
<img id="slide-image" alt="">
</figure>
<fieldset>
<legend>Bildnavigation</legend>
<button id="slide-prev" type="button" aria-label="Vorheriges Bild" title="Vorheriges Bild">
<img src="images/icons/chevron-left.svg" alt="Vorheriges Bild">
<legend data-i18n="html.legend.bildnavigation"></legend>
<button id="slide-prev" type="button" aria-label="" title="" data-i18n-title="html.slide-prev.vorheriges.bild" data-i18n-aria-label="html.slide-prev.vorheriges.bild">
<img src="images/icons/chevron-left.svg" alt="" data-i18n-alt="html.slide-prev.vorheriges.bild">
</button>
<div>
<p id="slide-caption"></p>
<span id="slide-position">0 / 0</span>
</div>
<button id="slide-next" type="button" aria-label="Nächstes Bild" title="Nächstes Bild">
<img src="images/icons/chevron-right.svg" alt="Nächstes Bild">
<button id="slide-next" type="button" aria-label="" title="" data-i18n-title="html.slide-next.nachstes.bild" data-i18n-aria-label="html.slide-next.nachstes.bild">
<img src="images/icons/chevron-right.svg" alt="" data-i18n-alt="html.slide-next.nachstes.bild">
</button>
</fieldset>
</div>
<p id="poi-description"></p>
<audio id="poi-audio" controls preload="metadata" aria-label="Audiowiedergabe zum Streckenpunkt" hidden>Audio kann auf diesem Gerät nicht wiedergegeben werden.</audio>
<audio id="poi-audio" controls preload="metadata" aria-label="" hidden data-i18n-aria-label="html.poi-audio.ton.zur.station" data-i18n="html.poi-audio.dieses.gerat.kann.den.ton.leider.nicht"></audio>
</div>
</section>
<footer id="navigation-footer" hidden aria-live="polite">
<svg id="navigation-arrow" viewBox="0 0 64 64" aria-hidden="true" focusable="false">
<path d="M32 4 53 54 32 44 11 54Z"/>
<svg id="navigation-arrow" viewbox="0 0 64 64" aria-hidden="true" focusable="false">
<path d="M32 4 53 54 32 44 11 54Z"></path>
</svg>
<p>
<span id="navigation-mode">Richtung zum nächsten Streckenpunkt</span>
<span id="navigation-mode" data-i18n="html.navigation-mode.richtung.zum.nachsten.station"></span>
<strong id="navigation-distance">–</strong>
</p>
</footer>
@@ -301,140 +296,94 @@
<dialog id="info-dialog" class="info-dialog" aria-labelledby="info-dialog-title">
<div class="info-dialog-shell">
<header class="info-dialog-header">
<h2 id="info-dialog-title">Informationen</h2>
<button id="info-close" type="button" aria-label="Informationsfenster schließen" title="Schließen">×</button>
<h2 id="info-dialog-title" data-i18n="legal-ui.informationen"></h2>
<button id="info-close" type="button" aria-label="" title="" data-i18n-title="legal-ui.schliessen" data-i18n-aria-label="legal-ui.informationsfenster.schliessen">×</button>
</header>
<div class="info-dialog-content">
<article class="info-page" data-info-page="privacy" hidden>
<p class="legal-warning"><strong>Hinweis:</strong> Diese Datenschutzhinweise sind mit den öffentlich angegebenen Kontaktdaten der Staatlichen Grundschule Herpf vorbefüllt. Hostinganbieter, konkrete Serverprotokollierung und Speicherdauer müssen für die tatsächlich betriebene Wegwichtel-Instanz noch technisch geprüft und ergänzt werden.</p>
<h3>1. Verantwortliche Stelle und Datenschutzkontakt</h3>
<p class="legal-warning"><strong data-i18n="html.strong.hinweis"></strong><span data-i18n="html.p.diese.angaben.sind.noch.nicht.vollstandig.die"></span></p>
<h3 data-i18n="html.h3.1.verantwortliche.stelle.und.datenschutzkontakt"></h3>
<p>
<strong>Staatliche Grundschule Herpf</strong><br>
Zum Eichig 3<br>
98617 Meiningen / OT Herpf<br>
Telefon: (036943) 63722<br>
E-Mail: info@grundschule-herpf.de
</p>
<p>
Datenschutzangaben der Schule:<br>
<strong>Staatliches Schulamt Südthüringen</strong><br>
Datenschutzbeauftragter<br>
Hölderlinstraße 1<br>
98527 Suhl<br>
Telefon: 03681 / 7341 25<br>
Fax: 03681 / 7341 09<br>
E-Mail: datenschutz.suedthueriungen@schulamt.thueringen.de
</p>
<p>Der Datenschutzbeauftragte kann außerdem über die Postadresse der Schule mit dem Zusatz „der Datenschutzbeauftragte“ erreicht werden.</p>
<h3>2. Bereitstellung der Website</h3>
<p>Beim Aufruf der Website können durch den Webserver technisch erforderliche Verbindungsdaten verarbeitet werden. Dazu können IP-Adresse, Datum und Uhrzeit der Anfrage, Zeitzonendifferenz zur Greenwich Mean Time, aufgerufene Seite, Zugriffsstatus oder HTTP-Statuscode, übertragene Datenmenge, Referrer, Browser, Betriebssystem, Oberfläche sowie Sprache und Version der Browsersoftware gehören. Diese Verarbeitung dient der technischen Bereitstellung, Stabilität und Sicherheit der Website.</p>
<p>Hostinganbieter, konkrete Protokolldaten und Speicherdauer dieser Wegwichtel-Instanz: <strong>[Anbieter, Anschrift, Art der Serverlogs und Speicherdauer ergänzen]</strong></p>
<h3>3. Standortdaten</h3>
<p>Die Standortfreigabe ist freiwillig. Sie wird benötigt, um nahe Routen vorzuschlagen und während einer gestarteten Route Entfernungen und Richtungen zu berechnen. Die Anwendung verarbeitet die laufende Position im Browser. Sie legt daraus keinen serverseitigen Standortverlauf an.</p>
<h3>4. Lokaler Streckenfortschritt</h3>
<p>Nach ausdrücklicher Aktivierung speichert die Anwendung ein JSON-Objekt im Local Storage dieses Browsers. Es enthält die aktuell ausgewählte Strecke, besuchte und angesehene Streckenpunkte, abgespielte Audioeinträge, den Routenstatus sowie Zeitstempel. Diese Angaben bleiben auf dem verwendeten Gerät und werden nicht mit dem Server synchronisiert.</p>
<p>Gespeicherte Streckendaten werden beim nächsten Öffnen entfernt, sobald ihre letzte Aktualisierung mindestens 24 Stunden zurückliegt. Beim Ausschalten der Funktion werden die Streckendaten sofort gelöscht.</p>
<strong data-i18n="html.strong.staatliche.grundschule.herpf"></strong><br><span data-i18n="html.p.zum.eichig.3"></span><br><span data-i18n="html.p.98617.meiningen.ot.herpf"></span><br><span data-i18n="html.p.telefon.036943.63722"></span><br><span data-i18n="html.p.e.mail.info.grundschule.herpf.de"></span></p>
<p><span data-i18n="html.p.datenschutzangaben.der.schule"></span><br>
<strong data-i18n="html.strong.staatliches.schulamt.sudthuringen"></strong><br><span data-i18n="html.p.datenschutzbeauftragter"></span><br><span data-i18n="html.p.holderlinstrae.1"></span><br><span data-i18n="html.p.98527.suhl"></span><br><span data-i18n="html.p.telefon.03681.7341.25"></span><br><span data-i18n="html.p.fax.03681.7341.09"></span><br><span data-i18n="html.p.e.mail.datenschutz.suedthueriungen.schulamt.thueringen.de"></span></p>
<p data-i18n="html.p.der.datenschutzbeauftragte.kann.auerdem.uber.die.postadresse"></p>
<h3 data-i18n="html.h3.2.wenn.du.wegwichtel.offnest"></h3>
<p data-i18n="html.p.wenn.du.wegwichtel.offnest.konnen.verbindungsdaten.verarbeitet"></p>
<p><span data-i18n="html.p.wer.die.website.bereitstellt.und.welche.verbindungsdaten"></span><strong data-i18n="html.strong.anbieter.anschrift.gespeicherte.daten.und.speicherdauer.erganzen"></strong></p>
<h3 data-i18n="html.h3.3.standortdaten"></h3>
<p data-i18n="html.p.du.entscheidest.ob.wegwichtel.deinen.standort.nutzen"></p>
<h3 data-i18n="html.h3.4.deinen.fortschritt.merken"></h3>
<p data-i18n="html.p.wenn.du.meinen.fortschritt.merken.einschaltest.speichert"></p>
<p data-i18n="html.p.wenn.du.deinen.fortschritt.mindestens.einen.tag"></p>
<label class="switch-row compact-switch">
<span>
<strong>Streckenfortschritt lokal speichern</strong>
<small>Freiwillige Browser-Speicherung ein- oder ausschalten.</small>
<strong data-i18n="html.strong.meinen.fortschritt.merken"></strong>
<small data-i18n="html.small.du.entscheidest.ob.sich.wegwichtel.deinen.fortschritt"></small>
</span>
<input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span>
</label>
<h3>5. Lokale Streckenentwürfe</h3>
<p>Neu angelegte Strecken werden zunächst ausschließlich im Local Storage dieses Browsers gespeichert. Die GPX-Daten enthalten den Streckenverlauf und die Koordinaten der Streckenpunkte. Ergänzende JSON-Daten ordnen den Streckenpunkten Texte und Medienreferenzen zu. Bilder und Audiodateien werden platzsparend im lokalen Browser-Medienspeicher gespeichert.</p>
<p>Lokale Streckenentwürfe sind bewusst von der freiwilligen Fortschrittsspeicherung getrennt. Sie werden nicht durch die 24-Stunden-Bereinigung entfernt und beim Ausschalten der Fortschrittsspeicherung nicht gelöscht. Sie verbleiben im Browser, bis sie ausdrücklich gelöscht werden oder die Browserdaten entfernt werden. Eine Übertragung an den Server erfolgt nur nach ausdrücklicher Betätigung des Upload-Buttons im Editor.</p>
<h3>6. Cookies und Tracking</h3>
<p>Die Anwendung setzt derzeit keine Cookies. Sie verwendet keine Reichweitenmessung, keine personalisierte Werbung und keine Trackingdienste. Eine technisch notwendige Local-Storage-Einstellung merkt sich lediglich, ob die freiwillige Speicherung des Streckenfortschritts aktiviert wurde.</p>
<h3>7. Inhalte von Schülerinnen und Schülern</h3>
<p>Fotos, Texte und Sprachaufnahmen dürfen nur veröffentlicht werden, wenn die erforderlichen Rechte und Einwilligungen vorliegen. Besonders bei erkennbaren Kindern sind die Vorgaben der Schule und die Einwilligungen der Sorgeberechtigten zu beachten.</p>
<h3>8. Rechte betroffener Personen</h3>
<p>Betroffene Personen können je nach rechtlicher Voraussetzung Auskunft, Berichtigung, Löschung, Einschränkung der Verarbeitung, Datenübertragbarkeit und Widerspruch verlangen. Erteilte Einwilligungen können für die Zukunft widerrufen werden. Außerdem besteht das Recht, sich bei einer Datenschutzaufsichtsbehörde über die Verarbeitung personenbezogener Daten zu beschweren.</p>
<p>
Zuständige Datenschutzaufsicht:<br>
<strong>Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)</strong><br>
Postfach 90 04 55, 99107 Erfurt<br>
Häßlerstraße 8, 99096 Erfurt<br>
Telefon: 0361 / 57 311 29 00<br>
Telefax: 0361 / 57 311 29 04<br>
E-Mail: poststelle@datenschutz.thueringen.de<br>
Website: www.tlfdi.de
</p>
<h3 data-i18n="html.h3.5.deine.eigenen.strecken"></h3>
<p data-i18n="html.p.neue.strecken.bleiben.zuerst.auf.deinem.gerat"></p>
<p data-i18n="html.p.deine.entwurfe.bleiben.erhalten.wenn.alter.fortschritt"></p>
<h3 data-i18n="html.h3.6.werbung.und.gespeicherte.einstellungen"></h3>
<p data-i18n="html.p.wegwichtel.verwendet.keine.cookies.es.zahlt.keine"></p>
<h3 data-i18n="html.h3.7.inhalte.von.schulerinnen.und.schulern"></h3>
<p data-i18n="html.p.bilder.texte.und.tonaufnahmen.durfen.nur.geteilt"></p>
<h3 data-i18n="html.h3.8.deine.rechte"></h3>
<p data-i18n="html.p.du.kannst.fragen.welche.daten.uber.dich"></p>
<p><span data-i18n="html.p.zustandige.datenschutzaufsicht"></span><br>
<strong data-i18n="html.strong.thuringer.landesbeauftragter.fur.den.datenschutz.und.die"></strong><br><span data-i18n="html.p.postfach.90.04.55.99107.erfurt"></span><br><span data-i18n="html.p.halerstrae.8.99096.erfurt"></span><br><span data-i18n="html.p.telefon.0361.57.311.29.00"></span><br><span data-i18n="html.p.telefax.0361.57.311.29.04"></span><br><span data-i18n="html.p.e.mail.poststelle.datenschutz.thueringen.de"></span><br><span data-i18n="html.p.website.www.tlfdi.de"></span></p>
</article>
<article class="info-page" data-info-page="imprint" hidden>
<p class="legal-warning"><strong>Hinweis:</strong> Dieses Impressum ist mit den öffentlich angegebenen Angaben der Staatlichen Grundschule Herpf vorbefüllt. Vor Veröffentlichung der Wegwichtel-Instanz sollte geprüft werden, ob für dieses konkrete Angebot zusätzliche oder abweichende Pflichtangaben erforderlich sind.</p>
<h3>Anbieter und verantwortlich für dieses Angebot</h3>
<p class="legal-warning"><strong data-i18n="html.strong.hinweis"></strong><span data-i18n="html.p.diese.angaben.stammen.von.der.grundschule.herpf"></span></p>
<h3 data-i18n="html.h3.wer.fur.wegwichtel.verantwortlich.ist"></h3>
<p>
<strong>Staatliche Grundschule Herpf</strong><br>
Zum Eichig 3<br>
98617 Meiningen / OT Herpf
</p>
<h3>Kontakt</h3>
<strong data-i18n="html.strong.staatliche.grundschule.herpf"></strong><br><span data-i18n="html.p.zum.eichig.3"></span><br><span data-i18n="html.p.98617.meiningen.ot.herpf"></span></p>
<h3 data-i18n="html.h3.kontakt"></h3>
<p><span data-i18n="html.p.telefon.036943.63722"></span><br><span data-i18n="html.p.telefax.036943.63843"></span><br><span data-i18n="html.p.e.mail.info.grundschule.herpf.de"></span></p>
<h3 data-i18n="html.h3.leitung.der.schule"></h3>
<p data-i18n="html.p.frau.wichert.schulleiterin"></p>
<h3 data-i18n="html.h3.schultrager"></h3>
<p>
Telefon: (036943) 63722<br>
Telefax: (036943) 63843<br>
E-Mail: info@grundschule-herpf.de
</p>
<h3>Vertretungsberechtigte Person</h3>
<p>Frau Wichert, Schulleiterin</p>
<h3>Schulträger</h3>
<p>
<strong>Landkreis Schmalkalden-Meiningen</strong><br>
Landratsamt<br>
Obertshäuser Platz 1<br>
98617 Meiningen
</p>
<h3>Inhaltlich verantwortlich</h3>
<p>Frau Wichert</p>
<h3>Weitere Pflichtangaben</h3>
<p>Je nach konkretem Betrieb der Wegwichtel-Instanz können zusätzliche Angaben erforderlich sein, zum Beispiel zur technischen Betreiberin, zum Hosting oder zu ergänzenden Zuständigkeiten. Nicht benötigte Angaben sind zu entfernen.</p>
<h3>Haftung für Inhalte und Verweise</h3>
<p>Die Inhalte werden sorgfältig erstellt und regelmäßig geprüft. Für Hinweise auf fehlerhafte, veraltete oder rechtsverletzende Inhalte steht der oben genannte Kontakt zur Verfügung. Für Inhalte externer Seiten sind deren Betreiber verantwortlich.</p>
<h3>Urheber- und Nutzungsrechte</h3>
<p>Soweit nicht anders angegeben, liegen die Rechte bei der Grundschule Herpf. Texte, Bilder, Tonaufnahmen und Routendaten dürfen nur im Rahmen der jeweils eingeräumten Nutzungsrechte verwendet werden. Die Rechte der beteiligten Kinder, Familien, Lehrkräfte und weiteren Urheberinnen und Urheber bleiben unberührt.</p>
<strong data-i18n="html.strong.landkreis.schmalkalden.meiningen"></strong><br><span data-i18n="html.p.landratsamt"></span><br><span data-i18n="html.p.obertshauser.platz.1"></span><br><span data-i18n="html.p.98617.meiningen"></span></p>
<h3 data-i18n="html.h3.inhaltlich.verantwortlich"></h3>
<p data-i18n="html.p.frau.wichert"></p>
<h3 data-i18n="html.h3.weitere.pflichtangaben"></h3>
<p data-i18n="html.p.je.nachdem.wer.wegwichtel.betreibt.konnen.weitere"></p>
<h3 data-i18n="html.h3.haftung.fur.inhalte.und.verweise"></h3>
<p data-i18n="html.p.die.inhalte.werden.sorgfaltig.erstellt.und.regelmaig"></p>
<h3 data-i18n="html.h3.urheber.und.nutzungsrechte"></h3>
<p data-i18n="html.p.wenn.nichts.anderes.angegeben.ist.liegen.die"></p>
</article>
<article class="info-page" data-info-page="technology" hidden>
<h3>Cookies</h3>
<p>Wegwichtel setzt derzeit keine Cookies. Die Anwendung besitzt keine Werbe-, Analyse- oder Trackingfunktionen. Der Cookie-Schalter bleibt deshalb ausgeschaltet.</p>
<label class="switch-row switch-disabled compact-switch">
<span>
<strong>Optionale Cookies</strong>
<small>Derzeit nicht vorhanden.</small>
</span>
<input class="cookie-toggle" type="checkbox" role="switch" aria-checked="false" disabled>
<span class="switch-track" aria-hidden="true"></span>
</label>
<h3>Local Storage</h3>
<p>Der Browser kann auf Wunsch Streckenfortschritte lokal speichern. Verwendet werden die Schlüssel <code>wegwichtel.privacy.v1</code> für die gewählte Einstellung und <code>wegwichtel.route-progress.v1</code> für die freiwilligen Streckendaten. Die Streckendaten werden nach 24 Stunden bereinigt oder beim Ausschalten sofort entfernt.</p>
<p>Mit „App offline speichern“ werden ausschließlich App-Dateien auf diesem Gerät gespeichert, keine Strecken. „Strecke offline speichern“ lädt eine einzeln gewählte veröffentlichte Strecke mit GPX, Bildern und Audio in den geschützten Browser-Speicher (OPFS oder CacheStorage). Beide Speicherstände können unabhängig entfernt werden. „GPX herunterladen“ speichert eine GPX-Datei im Download-Ordner des Geräts. Lokale Entwürfe und der optionale Fortschritt werden separat verwaltet; ihre 24-Stunden-Bereinigung betrifft keine Offline-Dateien.</p>
<p>Lokale Streckenentwürfe werden getrennt davon unter <code>wegwichtel.local-routes.v1.*</code> gespeichert. Pro Entwurf werden GPX- und JSON-Daten im Local Storage vorgehalten; Bilder und Audiodateien liegen getrennt davon im lokalen Browser-Medienspeicher (IndexedDB). Diese Entwürfe werden nicht nach 24 Stunden bereinigt und sind nicht vom Schalter für den Streckenfortschritt abhängig. Durch Mediendateien kann das Speicherlimit des Browsers vergleichsweise schnell erreicht werden. Eine Serverübertragung erfolgt ausschließlich nach ausdrücklicher Betätigung des Upload-Buttons im Editor.</p>
<h3 data-i18n="html.h3.was.merkt.sich.wegwichtel"></h3>
<p data-i18n="html.p.du.kannst.deinen.fortschritt.auf.diesem.gerat"></p>
<p data-i18n="html.p.wenn.du.einen.tag.lang.nichts.mehr"></p>
<label class="switch-row compact-switch">
<span>
<strong>Streckenfortschritt lokal speichern</strong>
<small>Die Änderung gilt sofort auf diesem Gerät.</small>
<strong data-i18n="html.strong.meinen.fortschritt.merken"></strong>
<small data-i18n="html.small.die.anderung.gilt.sofort.auf.diesem.gerat"></small>
</span>
<input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span>
</label>
<h3>Lokal eingebundene JavaScripts</h3>
<h3 data-i18n="html.h3.ohne.internet.unterwegs"></h3>
<p data-i18n="html.p.mit.app.ohne.internet.offnen.speicherst.du"></p>
<p data-i18n="html.p.mit.weg.als.datei.herunterladen.kannst.du"></p>
<h3 data-i18n="html.h3.deine.eigenen.strecken"></h3>
<p data-i18n="html.p.neue.strecken.bleiben.zuerst.auf.diesem.gerat"></p>
<p data-i18n="html.p.deine.entwurfe.bleiben.gespeichert.bis.du.sie"></p>
<h3 data-i18n="html.h3.welche.erlaubnisse.braucht.die.app"></h3>
<ul>
<li><strong>jQuery 4.0.0:</strong> DOM-Zugriffe, Ereignisbehandlung und API-Anfragen.</li>
<li><strong>jQuery UI 1.14.2:</strong> Schaltflächen und Suchvorschläge.</li>
<li><strong>Wegwichtel-Module:</strong> Routenanzeige, Navigation, Standort, Geräteausrichtung, Bilder, Audio, Vibration, Datenschutz, lokaler Fortschritt und lokale Streckenentwürfe.</li>
<li data-i18n="html.li.deinen.standort.um.strecken.in.deiner.nahe"></li>
<li data-i18n="html.li.die.richtung.deines.gerats.damit.sich.der"></li>
<li data-i18n="html.li.zugriff.auf.ausgewahlte.bilder.und.tonaufnahmen.wenn"></li>
</ul>
<p>Alle Bibliotheken werden vom eigenen Server ausgeliefert. Die Anwendung lädt keine JavaScripts, Schriftarten oder Analysepixel von externen Anbietern.</p>
<h3>Browser-Schnittstellen</h3>
<ul>
<li>Geolocation API für freiwillige Standortdaten,</li>
<li>Device Orientation API für den Richtungspfeil,</li>
<li>Vibration API für Hinweise bei erreichten Streckenpunkten,</li>
<li>HTML-Audio und HTML-Bilder für lokal bereitgestellte Medien,</li>
<li>File System Access API und IndexedDB für lokale Medienreferenzen sowie FileReader und XML-Schnittstellen zum Verarbeiten von GPX-Dateien,</li>
<li>History API für die Navigation zwischen den Ansichten.</li>
</ul>
<h3>Technische Grenzen</h3>
<p>Browser und Betriebssystem können Standort, Kompass, Audio, Vibration oder lokale Speicherfunktionen sperren. Die Anwendung bleibt grundsätzlich nutzbar, einzelne Komfortfunktionen können dann jedoch fehlen. Lokale Streckenmetadaten liegen im Local Storage; Medienreferenzen beziehungsweise der kompatible Blob-Fallback liegen getrennt in IndexedDB.</p>
<p data-i18n="html.p.du.kannst.diese.erlaubnisse.ablehnen.dann.fehlen"></p>
<h3 data-i18n="html.h3.werbung.und.andere.anbieter"></h3>
<p data-i18n="html.p.wegwichtel.zeigt.keine.werbung.und.verfolgt.nicht"></p>
</article>
</div>
</div>
@@ -442,58 +391,58 @@
<dialog id="editor-dialog" class="editor-dialog" aria-labelledby="editor-dialog-title">
<div class="editor-dialog-shell">
<header class="editor-dialog-header">
<h2 id="editor-dialog-title">Streckenverwaltung</h2>
<button id="editor-close" type="button" aria-label="Streckenverwaltung schließen" title="Schließen">×</button>
<h2 id="editor-dialog-title" data-i18n="html.editor-button.strecken.bearbeiten"></h2>
<button id="editor-close" type="button" aria-label="" title="" data-i18n-title="legal-ui.schliessen" data-i18n-aria-label="html.editor-close.fenster.zum.bearbeiten.schlieen">×</button>
</header>
<output id="editor-status" class="editor-status" aria-live="polite" hidden></output>
<section id="editor-route-list-view" class="editor-view" aria-labelledby="editor-route-list-title">
<div class="editor-toolbar">
<h3 id="editor-route-list-title">Vorhandene Strecken</h3>
<button id="editor-new-route" class="editor-round-button" type="button" aria-label="Neue lokale Strecke anlegen" title="Neue lokale Strecke anlegen">
<h3 id="editor-route-list-title" data-i18n="html.editor-route-list-title.deine.strecken"></h3>
<button id="editor-new-route" class="editor-round-button" type="button" aria-label="" title="" data-i18n-title="html.editor-new-route.neue.strecke.anlegen" data-i18n-aria-label="html.editor-new-route.neue.strecke.anlegen">
<span aria-hidden="true">+</span>
</button>
</div>
<section class="storage-usage" data-storage-usage aria-label="Lokaler Speicher">
<h3>Lokaler Speicher</h3>
<meter data-storage-site-meter min="0" max="1" value="0" aria-label="Belegung des Website-Speichers" hidden></meter>
<p data-storage-site-text>Speicherbelegung wird ermittelt …</p>
<meter data-storage-local-meter min="0" max="5242880" value="0" aria-label="Local Storage im Verhältnis zum Richtwert von 5 MiB"></meter>
<section class="storage-usage" data-storage-usage aria-label="" data-i18n-aria-label="html.section.platz.fur.wegwichtel">
<h3 data-i18n="html.section.platz.fur.wegwichtel"></h3>
<meter data-storage-site-meter min="0" max="1" value="0" aria-label="" hidden data-i18n-aria-label="html.meter.belegter.platz.fur.app.bilder.und.ton"></meter>
<p data-storage-site-text data-i18n="html.p.wir.schauen.wie.viel.platz.belegt.ist"></p>
<meter data-storage-local-meter min="0" max="5242880" value="0" aria-label="" data-i18n-aria-label="html.meter.belegter.platz.fur.texte.und.einstellungen.der"></meter>
<p data-storage-local-text></p>
<p>Website-Speicher: Browser-Schätzung für App-Dateien, Offline-Strecken und Medien. Local Storage: separater Speicher für Entwürfe und Einstellungen. Beide Anzeigen beziehen sich nur auf diese Website, nicht auf die gesamte Festplatte.</p>
<p data-i18n="html.p.der.erste.balken.zeigt.den.platz.fur"></p>
<output data-storage-warning hidden></output>
<button data-storage-refresh type="button">Speicheranzeige aktualisieren</button>
<button data-storage-refresh type="button" data-i18n="html.button.platz.prufen"></button>
</section>
<p class="editor-muted">Veröffentlichte Serverstrecken und ausschließlich in diesem Browser gespeicherte Entwürfe werden getrennt dargestellt.</p>
<p class="editor-muted" data-i18n="html.p.hier.findest.du.strecken.fur.alle.und"></p>
<ul id="editor-route-list" class="editor-route-list" aria-live="polite"></ul>
</section>
<section id="editor-route-detail-view" class="editor-view" aria-labelledby="editor-route-detail-title" hidden>
<div class="editor-toolbar editor-toolbar-back">
<button id="editor-route-back" class="editor-secondary-button" type="button">Zurück</button>
<h3 id="editor-route-detail-title">Streckendetails</h3>
<button id="editor-route-back" class="editor-secondary-button" type="button" data-i18n="html.editor-route-back.zuruck"></button>
<h3 id="editor-route-detail-title" data-i18n="html.editor-route-detail-title.deine.strecke"></h3>
</div>
<form id="editor-route-form" class="editor-form">
<input id="editor-route-id" type="hidden">
<label for="editor-route-name">Name der Strecke</label>
<label for="editor-route-name" data-i18n="html.label.name.der.strecke"></label>
<input id="editor-route-name" type="text" autocomplete="off" required>
<label for="editor-route-description">Beschreibung</label>
<label for="editor-route-description" data-i18n="html.label.beschreibung"></label>
<textarea id="editor-route-description" rows="4"></textarea>
<div id="editor-route-gpx-group" class="editor-gpx-group">
<label for="editor-route-gpx">GPX-Datei importieren oder ersetzen</label>
<label for="editor-route-gpx" data-i18n="html.label.weg.aus.einer.datei.ubernehmen"></label>
<input id="editor-route-gpx" type="file" accept=".gpx,application/gpx+xml,text/xml,application/xml">
</div>
<p id="editor-route-activated" class="editor-muted">Neue Strecken werden zunächst lokal gespeichert. Der Upload-Button überträgt den Entwurf als unveröffentlichte Strecke zur Prüfung an den Server.</p>
<p id="editor-route-activated" class="editor-muted" data-i18n="html.editor-route-activated.deine.neue.strecke.bleibt.zuerst.auf.diesem"></p>
<div class="editor-form-actions">
<button id="editor-route-save" class="editor-primary-button" type="submit">Strecke speichern</button>
<button id="editor-route-upload" class="editor-secondary-button" type="button" hidden>An Server übertragen</button>
<button id="editor-route-save" class="editor-primary-button" type="submit" data-i18n="html.editor-route-save.strecke.speichern"></button>
<button id="editor-route-upload" class="editor-secondary-button" type="button" hidden data-i18n="editor-ui.zur.prufung.senden"></button>
</div>
</form>
<div class="editor-toolbar editor-poi-toolbar">
<h3>Streckenpunkte</h3>
<button id="editor-add-poi" class="editor-round-button" type="button" aria-label="Streckenpunkt hinzufügen" title="Streckenpunkt hinzufügen">
<h3 data-i18n="html.dt.stationen"></h3>
<button id="editor-add-poi" class="editor-round-button" type="button" aria-label="" title="" data-i18n-title="editor-ui.station.hinzufugen" data-i18n-aria-label="editor-ui.station.hinzufugen">
<span aria-hidden="true">+</span>
</button>
</div>
@@ -501,55 +450,58 @@
</section>
<section id="editor-poi-form-view" class="editor-view" aria-labelledby="editor-poi-form-title" hidden>
<div class="editor-toolbar editor-toolbar-back">
<button id="editor-poi-cancel" class="editor-secondary-button" type="button">Zurück zur Strecke</button>
<h3 id="editor-poi-form-title">Streckenpunkt bearbeiten</h3>
<button id="editor-poi-cancel" class="editor-secondary-button" type="button" data-i18n="html.button.zuruck.zur.strecke"></button>
<h3 id="editor-poi-form-title" data-i18n="editor-ui.station.bearbeiten"></h3>
</div>
<form id="editor-poi-form" class="editor-form">
<input id="editor-poi-id" type="hidden">
<label for="editor-poi-title">Titel</label>
<label for="editor-poi-title" data-i18n="html.label.name"></label>
<input id="editor-poi-title" type="text" autocomplete="off" required>
<label for="editor-poi-description">Text</label>
<label for="editor-poi-description" data-i18n="html.label.text"></label>
<textarea id="editor-poi-description" rows="6"></textarea>
<input id="editor-poi-lat" type="hidden">
<input id="editor-poi-lon" type="hidden">
<input id="editor-poi-radius" type="hidden" value="25">
<div class="editor-media-actions" aria-label="Medien zum Streckenpunkt hinzufügen">
<button id="editor-poi-audio-add" class="editor-media-button" type="button" title="Audio hinzufügen">
<svg class="editor-media-icon" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
<path d="M18 5v15.2a5 5 0 1 1-2-4V9l10-2v9.2a5 5 0 1 1-2-4V4.6z"/>
<path d="M6 4v6M3 7h6"/>
<div class="editor-media-actions" aria-label="" data-i18n-aria-label="html.div.bilder.und.ton.zur.station.hinzufugen">
<button id="editor-poi-audio-add" class="editor-media-button" type="button" title="" data-i18n-title="html.editor-poi-audio-add.ton.hinzufugen">
<svg class="editor-media-icon" viewbox="0 0 32 32" aria-hidden="true" focusable="false">
<path d="M18 5v15.2a5 5 0 1 1-2-4V9l10-2v9.2a5 5 0 1 1-2-4V4.6z"></path>
<path d="M6 4v6M3 7h6"></path>
</svg>
<span>Audio hinzufügen</span>
<span data-i18n="html.editor-poi-audio-add.ton.hinzufugen"></span>
</button>
<button id="editor-poi-images-add" class="editor-media-button" type="button" title="Bild hinzufügen">
<svg class="editor-media-icon" viewBox="0 0 32 32" aria-hidden="true" focusable="false">
<path d="M6 10h4l2-3h8l2 3h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V12a2 2 0 0 1 2-2z"/>
<circle cx="16" cy="18" r="5"/>
<path d="M6 3v6M3 6h6"/>
<button id="editor-poi-images-add" class="editor-media-button" type="button" title="" data-i18n-title="html.editor-poi-images-add.bild.hinzufugen">
<svg class="editor-media-icon" viewbox="0 0 32 32" aria-hidden="true" focusable="false">
<path d="M6 10h4l2-3h8l2 3h4a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V12a2 2 0 0 1 2-2z"></path>
<circle cx="16" cy="18" r="5"></circle>
<path d="M6 3v6M3 6h6"></path>
</svg>
<span>Bild hinzufügen</span>
<span data-i18n="html.editor-poi-images-add.bild.hinzufugen"></span>
</button>
</div>
<input id="editor-poi-audio" type="file" accept="audio/*" hidden>
<input id="editor-poi-images" type="file" accept="image/*" multiple hidden>
<div id="editor-poi-media-preview" class="editor-media-preview" aria-label="Medienvorschau"></div>
<div id="editor-poi-media-preview" class="editor-media-preview" aria-label="" data-i18n-aria-label="html.editor-poi-media-preview.bilder.und.ton.ansehen"></div>
<p id="editor-poi-media-status" class="editor-muted" aria-live="polite"></p>
<div class="editor-form-actions">
<button id="editor-poi-save" class="editor-primary-button" type="submit">Streckenpunkt speichern</button>
<button id="editor-poi-save" class="editor-primary-button" type="submit" data-i18n="html.editor-poi-save.station.speichern"></button>
<button id="editor-poi-delete" class="editor-danger-button" type="button" hidden>
<span class="editor-danger-icon" aria-hidden="true">−</span>
<span>Streckenpunkt entfernen</span>
<span data-i18n="html.span.station.entfernen"></span>
</button>
</div>
</form>
</section>
</div>
</dialog>
<script src="js/texts-de.js?v=0.12.14"></script>
<script src="js/i18n.js?v=0.12.14"></script>
<script src="js/user-messages.js?v=0.12.14"></script>
<script src="js/local-routes.js?v=0.12.14"></script>
<script src="js/editor-ui.js?v=0.12.14"></script>
<script src="js/privacy-storage.js?v=0.12.14"></script>
@@ -559,4 +511,4 @@
<script src="js/offline-cache.js?v=0.12.14"></script>
<script src="js/bootstrap-loader.js?v=0.12.14"></script>
</body>
</html>
</html>
+49 -49
View File
@@ -58,7 +58,7 @@
if (state.routeState === 'paused') {
$('#tracking-status')
.prop('hidden', false)
.text('Gespeicherter Streckenfortschritt gefunden. Die Route kann fortgesetzt werden.');
.text(ns.I18n.t("app.du.kannst.dort.weitermachen.wo.du.aufgehort"));
} else {
$('#tracking-status').prop('hidden', true).text('');
}
@@ -82,7 +82,7 @@
function navigate(pageSelector, options = {}) {
const target = $(pageSelector);
if (!target.length) throw new Error(`Unbekannte Ansicht: ${pageSelector}`);
if (!target.length) throw new Error(ns.I18n.t("app.page.missing"));
const previous = state.activePage;
$('.app-page').attr('hidden', true);
@@ -115,7 +115,7 @@
const proximity = route.proximityM == null
? ''
: `<small>${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</small>`;
const sourceLabel = source === 'local' ? '<small>Lokaler Entwurf</small>' : '';
const sourceLabel = source === 'local' ? ("<small>" + escapeHtml(ns.I18n.t("app.eigener.entwurf")) + "</small>") : '';
return `
<li>
<button type="button" data-route-select data-route-id="${escapeHtml(route.id)}" data-route-source="${source}">
@@ -157,21 +157,21 @@
function updateRouteControlSummary() {
if (!state.route) {
setRouteControlSummary('Route wählen', 'Aktive und lokale Strecken');
setRouteControlSummary(ns.I18n.t("app.strecke.wahlen"), ns.I18n.t("app.strecken.fur.alle.und.meine.entwurfe"));
return;
}
const titleText = state.route.name || 'Route';
const titleText = state.route.name || ns.I18n.t("app.strecke");
const metric = nextPoiMetric();
if (!metric) {
const detailText = (state.route.pois || []).length
? 'Alle Streckenpunkte erreicht'
: 'Keine Streckenpunkte vorhanden';
? ns.I18n.t("app.alle.stationen.erreicht")
: ns.I18n.t("app.keine.stationen.vorhanden");
setRouteControlSummary(titleText, detailText);
return;
}
const distance = metric.distance == null ? '' : ` · ${Math.round(metric.distance)} m`;
setRouteControlSummary(titleText, `Nächster Streckenpunkt: ${metric.poi.title}${distance}`);
setRouteControlSummary(titleText, ns.I18n.t("app.nachste.station", { value1: metric.poi.title, value2: distance }));
}
function renderRouteControlList(selector, routes, source, emptyText) {
@@ -187,7 +187,7 @@
<li>
<button type="button" data-route-select data-route-id="${escapeHtml(route.id)}" data-route-source="${source}" ${current ? 'aria-current="true"' : ''}>
<strong>${escapeHtml(route.name)}</strong>
<small>${source === 'local' ? 'Lokaler Entwurf' : 'Veröffentlicht'}</small>
<small>${source === 'local' ? ns.I18n.t("app.eigener.entwurf") : ns.I18n.t("app.veroffentlicht")}</small>
</button>
</li>
`);
@@ -198,22 +198,22 @@
const routeControlLists = $('.route-control-list');
routeControlLists.attr('aria-busy', 'true');
$('#route-control-server-title').text(`Aktive Strecken (${state.routes.length})`);
$('#route-control-local-title').text(`Lokale Strecken (${state.localRoutes.length})`);
$('#local-route-title').text(`Lokale Strecken (${state.localRoutes.length})`);
$('#route-control-server-title').text(ns.I18n.t("app.strecken.fur.alle", { value1: state.routes.length }));
$('#route-control-local-title').text(ns.I18n.t("app.meine.entwurfe", { value1: state.localRoutes.length }));
$('#local-route-title').text(ns.I18n.t("app.meine.entwurfe", { value1: state.localRoutes.length }));
updateRouteControlSummary();
renderRouteControlList(
'#route-control-server-list',
state.routes,
'server',
'Keine veröffentlichte Strecke vorhanden.'
ns.I18n.t("app.hier.gibt.es.noch.keine.strecken.fur")
);
renderRouteControlList(
'#route-control-local-list',
state.localRoutes,
'local',
state.localRoutesError || 'Noch keine lokale Strecke vorhanden.'
state.localRoutesError || ns.I18n.t("app.du.hast.noch.keine.eigene.strecke.angelegt")
);
routeControlLists.attr('aria-busy', 'false');
@@ -239,26 +239,26 @@
.sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
const nearbyEmptyText = state.position
? `Keine Route im Umkreis von ${ns.Config.routeRadiusKm} km gefunden. Wähle unten eine beliebige Route aus.`
: 'Für Empfehlungen in der Nähe ist ein Standort erforderlich. Alle Routen bleiben unten auswählbar.';
? ns.I18n.t("app.keine.strecke.im.umkreis.von.km.gefunden", { value1: ns.Config.routeRadiusKm })
: ns.I18n.t("app.erlaube.deinen.standort.um.strecken.in.deiner");
renderRouteItems('#nearby-route-list', nearbyRoutes, nearbyEmptyText);
renderRouteItems(
'#all-route-list',
allRoutes,
searchTerm ? 'Keine Route entspricht dem Suchbegriff.' : 'Es sind noch keine veröffentlichten Routen vorhanden.'
searchTerm ? ns.I18n.t("app.keine.passende.strecke.gefunden.versuche.ein.anderes") : ns.I18n.t("app.hier.gibt.es.noch.keine.strecken.fur")
);
renderRouteItems(
'#local-route-list',
localRoutes,
state.localRoutesError || 'Noch keine lokalen Strecken gespeichert.',
state.localRoutesError || ns.I18n.t("app.du.hast.noch.keine.eigene.strecke.angelegt"),
'local'
);
$('#all-routes-summary').text(
searchTerm
? `${allRoutes.length} von ${routes.length} veröffentlichten Routen gefunden.`
: `${routes.length} veröffentlichte Routen verfügbar.`
? ns.I18n.t("app.von.strecken.gefunden", { value1: allRoutes.length, value2: routes.length })
: ns.I18n.t("app.strecken.zur.auswahl", { value1: routes.length })
);
renderRouteControl();
}
@@ -288,7 +288,7 @@
$('#app-shell').removeClass('has-navigation');
}
function showNavigation(message = 'Position wird ermittelt', distance = '–') {
function showNavigation(message = ns.I18n.t("app.location.searching"), distance = '–') {
$('#navigation-mode').text(message);
$('#navigation-distance').text(distance);
$('#navigation-footer').prop('hidden', false);
@@ -305,7 +305,7 @@
function updateRouteControls() {
const running = state.routeState === 'running';
const paused = state.routeState === 'paused';
const startLabel = paused ? 'Route fortsetzen' : 'Route starten';
const startLabel = paused ? ns.I18n.t("app.strecke.fortsetzen") : ns.I18n.t("app.strecke.starten");
$('#start-route')
.prop('disabled', running)
.attr({ 'aria-label': startLabel, title: startLabel });
@@ -333,7 +333,7 @@
setActivePoi(null);
hideNavigation();
if (announce && wasActive && !returnToRoutes) {
$('#tracking-status').prop('hidden', false).text('Route beendet.');
$('#tracking-status').prop('hidden', false).text(ns.I18n.t("app.strecke.beendet"));
} else {
$('#tracking-status').prop('hidden', true).text('');
}
@@ -357,8 +357,8 @@
ns.AudioPlayer.pause();
$('#tracking-status')
.prop('hidden', false)
.text('Route pausiert. Die Standortverfolgung ist angehalten.');
showNavigation('Route pausiert', $('#navigation-distance').text() || '–');
.text(ns.I18n.t("app.du.machst.eine.pause.die.app.sucht"));
showNavigation(ns.I18n.t("app.strecke.pausiert"), $('#navigation-distance').text() || '–');
updateRouteControls();
updateRouteControlSummary();
}
@@ -384,7 +384,7 @@
updateRouteControls();
const list = $('#poi-list').empty();
if (!route.pois.length) {
list.append('<li><p>Diese Route enthält noch keine Streckenpunkte.</p></li>');
list.append(("<li><p>" + escapeHtml(ns.I18n.t("app.diese.strecke.hat.noch.keine.stationen")) + "</p></li>"));
updateRouteControlSummary();
return;
}
@@ -484,8 +484,8 @@
const indicator = $(`#poi-list [data-poi-id="${metric.poi.id}"] .poi-proximity`);
const distance = Math.round(metric.distance);
indicator
.attr('aria-label', `${distance} Meter entfernt`)
.attr('title', `${distance} m entfernt`);
.attr('aria-label', ns.I18n.t("app.distance.spoken", { value1: distance }))
.attr('title', ns.I18n.t("app.distance.short", { value1: distance }));
indicator.find('svg').css('transform', `rotate(${metric.rotation.toFixed(1)}deg)`);
indicator.find('small').text(`${distance} m`);
});
@@ -537,27 +537,27 @@
$('#navigation-arrow').css('transform', `rotate(${metric.rotation.toFixed(1)}deg)`);
showNavigation(
heading == null
? `Norden oben · nächster Streckenpunkt: ${metric.poi.title}`
: `Nächster Streckenpunkt: ${metric.poi.title}`,
? ns.I18n.t("app.norden.oben.nachste.station", { value1: metric.poi.title })
: ns.I18n.t("app.nachste.station.2", { value1: metric.poi.title }),
`${distance} m`
);
$('#navigation-footer').attr(
'aria-label',
`Nächster Streckenpunkt ${metric.poi.title} in ${distance} Metern`
ns.I18n.t("app.nachste.station.in.metern", { value1: metric.poi.title, value2: distance })
);
}
function showRoutePointNavigation(position, progress) {
if (!progress) {
$('#navigation-arrow').css('transform', 'rotate(0deg)');
showNavigation('Keine Navigationspunkte vorhanden', '–');
showNavigation(ns.I18n.t("app.path.missing"), '–');
return;
}
if (progress.reached) {
$('#navigation-arrow').css('transform', 'rotate(0deg)');
showNavigation('Alle Streckenpunkte und das Routenziel erreicht', '0 m');
$('#navigation-footer').attr('aria-label', 'Alle Streckenpunkte und das Routenziel erreicht');
showNavigation(ns.I18n.t("app.alle.stationen.und.das.ziel.erreicht"), '0 m');
$('#navigation-footer').attr('aria-label', ns.I18n.t("app.alle.stationen.und.das.ziel.erreicht"));
return;
}
const bearing = ns.Distance.bearing(position, progress.target);
@@ -568,12 +568,12 @@
const distance = Math.round(progress.distance);
$('#navigation-arrow').css('transform', `rotate(${rotation.toFixed(1)}deg)`);
showNavigation(
heading == null ? 'Norden oben · nächster Routenpunkt' : 'Nächster Routenpunkt',
heading == null ? ns.I18n.t("app.norden.oben.nachster.punkt.auf.dem.weg") : ns.I18n.t("app.nachster.punkt.auf.dem.weg"),
`${distance} m`
);
$('#navigation-footer').attr(
'aria-label',
`Nächster Routenpunkt in ${distance} Metern`
ns.I18n.t("app.nachster.punkt.auf.dem.weg.in.metern", { value1: distance })
);
}
@@ -608,8 +608,8 @@
const nearest = nearestPendingPoi(metrics);
updateNavigation(position);
$('#tracking-status').text(
`GPS aktiv · Genauigkeit ${Math.round(position.accuracy)} m`
+ (nearest ? ` · nächster Streckenpunkt ${Math.round(nearest.distance)} m` : ' · alle Streckenpunkte erreicht')
ns.I18n.t("app.dein.standort.wird.gesucht.auf.etwa.m", { value1: Math.round(position.accuracy) })
+ (nearest ? ns.I18n.t("app.nachste.station.m", { value1: Math.round(nearest.distance) }) : ns.I18n.t("app.alle.stationen.erreicht.2"))
);
if (nearest && nearest.distance <= nearest.poi.triggerRadiusM) {
state.triggered.add(nearest.poi.id);
@@ -636,11 +636,11 @@
updateRouteControlSummary();
$('#tracking-status')
.prop('hidden', false)
.text(resumed ? 'Route wird fortgesetzt.' : 'Route wird gestartet. GPS wird aktiviert.');
showNavigation(resumed ? 'Route wird fortgesetzt' : 'Position wird ermittelt');
.text(resumed ? ns.I18n.t("app.strecke.wird.fortgesetzt") : ns.I18n.t("app.location.start"));
showNavigation(resumed ? ns.I18n.t("app.strecke.wird.fortgesetzt.2") : ns.I18n.t("app.location.searching"));
ns.Geo.start(
evaluatePosition,
error => $('#tracking-status').text(`GPS-Fehler: ${error.message}`)
() => $('#tracking-status').text(ns.I18n.t("app.dein.standort.konnte.nicht.gefunden.werden.prufe"))
);
const orientationAvailable = await ns.Orientation.start(updateHeading);
@@ -648,12 +648,12 @@
ns.Orientation.stop();
return;
}
if (!orientationAvailable) $('#navigation-mode').text('Norden oben · Navigation');
if (!orientationAvailable) $('#navigation-mode').text(ns.I18n.t("app.norden.oben.navigation"));
}
function routesFromResponse(response) {
if (!Array.isArray(response?.routes)) {
throw new TypeError('Die Routen-API hat keine gültige Routenliste geliefert.');
throw new TypeError(ns.I18n.t("app.die.strecken.konnten.nicht.geladen.werden.bitte"));
}
return response.routes
.filter(route => route?.id != null && route?.name != null)
@@ -666,7 +666,7 @@
state.localRoutesError = null;
} catch (error) {
state.localRoutes = [];
state.localRoutesError = error.message || 'Lokale Strecken konnten nicht gelesen werden.';
state.localRoutesError = ns.UserMessages?.error(error, ns.I18n.t("app.deine.entwurfe.konnten.nicht.geoffnet.werden.bitte")) || ns.I18n.t("app.deine.entwurfe.konnten.nicht.geoffnet.werden.bitte");
}
}
@@ -685,12 +685,12 @@
state.position = position;
updateRouteProximity(position);
$('#location-hint').text(
`Standort ermittelt (Genauigkeit ${Math.round(position.accuracy)} m). `
+ `Nahe Routen werden bis ${ns.Config.routeRadiusKm} km empfohlen.`
ns.I18n.t("app.dein.standort.wurde.auf.etwa.m.genau", { value1: Math.round(position.accuracy) })
+ ns.I18n.t("app.wir.zeigen.dir.strecken.bis.km.entfernung", { value1: ns.Config.routeRadiusKm })
);
} catch (error) {
} catch {
$('#location-hint').text(
`Standort nicht verfügbar: ${error.message}. Alle Routen können weiterhin ausgewählt werden.`
ns.I18n.t("app.dein.standort.konnte.nicht.gefunden.werden.du")
);
} finally {
context.markStep('location', 'done');
@@ -746,7 +746,7 @@
try {
await selectRoute(String(button.data('route-source') || 'server'), String(button.data('route-id')));
} catch (error) {
alert(error.responseJSON?.message || error.statusText || error.message);
alert(ns.UserMessages?.error(error, ns.I18n.t("app.die.strecke.konnte.nicht.geoffnet.werden.prufe")) || ns.I18n.t("app.die.strecke.konnte.nicht.geoffnet.werden.bitte"));
} finally {
button.prop('disabled', false);
}
+15 -15
View File
@@ -4,13 +4,13 @@
const versions = Object.freeze({ app: '0.12.14', jquery: '4.0.0', jqueryUi: '1.14.2' });
const steps = ['storage', 'jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location'];
const stepLabels = Object.freeze({
storage: 'Lokalen Speicher prüfen …',
jquery: 'jQuery laden …',
'jquery-ui': 'jQuery UI laden …',
modules: 'Anwendungsmodule laden …',
server: 'Server prüfen …',
routes: 'Routen laden …',
location: 'Standort ermitteln …'
get storage() { return globalThis.Wegwichtel.I18n.t("bootstrap-loader.gespeicherte.inhalte.prufen"); },
get jquery() { return globalThis.Wegwichtel.I18n.t("bootstrap-loader.wegwichtel.vorbereiten"); },
get 'jquery-ui'() { return globalThis.Wegwichtel.I18n.t("bootstrap-loader.die.bedienung.vorbereiten"); },
get modules() { return globalThis.Wegwichtel.I18n.t("bootstrap-loader.die.app.vorbereiten"); },
get server() { return globalThis.Wegwichtel.I18n.t("bootstrap-loader.verbindung.prufen"); },
get routes() { return globalThis.Wegwichtel.I18n.t("bootstrap-loader.strecken.laden"); },
get location() { return globalThis.Wegwichtel.I18n.t("bootstrap.location"); }
});
const screen = document.getElementById('bootstrap-screen');
const progress = document.getElementById('bootstrap-progress');
@@ -43,8 +43,8 @@
androidBrowserHint.hidden = false;
androidBrowserHint.dataset.browser = isChrome ? 'chrome' : 'other';
androidBrowserHint.textContent = isChrome
? 'Android mit Chrome erkannt. Lokale Bilder und Audiodateien können als Dateireferenzen verwendet werden.'
: 'Android erkannt. Für lokale Bilder und Audiodateien bitte Chrome verwenden; dort können Dateireferenzen ohne Kopie in den Local Storage genutzt werden.';
? globalThis.Wegwichtel.I18n.t("bootstrap-loader.auf.diesem.gerat.kannst.du.bilder.und")
: globalThis.Wegwichtel.I18n.t("bootstrap-loader.tipp.fur.android.mit.chrome.lassen.sich");
}
function schedule(callback, delay = 0) {
@@ -93,7 +93,7 @@
if (progress) progress.value = completedSteps.size;
if (state === 'failed') {
setStatusText(`Fehler bei: ${stepLabels[step] || step}`);
setStatusText(globalThis.Wegwichtel.I18n.t("bootstrap.step.failed", { value1: stepLabels[step] || step }));
return;
}
if (state === 'done') {
@@ -108,10 +108,10 @@
script.src = versioned(src);
script.async = false;
script.onload = () => {
if (test && !test()) reject(new Error(`Selbsttest fehlgeschlagen: ${src}`));
if (test && !test()) reject(new Error(globalThis.Wegwichtel.I18n.t("bootstrap-loader.ein.teil.der.app.konnte.nicht.starten")));
else resolve();
};
script.onerror = () => reject(new Error(`Datei konnte nicht geladen werden: ${src}`));
script.onerror = () => reject(new Error(globalThis.Wegwichtel.I18n.t("bootstrap-loader.ein.teil.der.app.konnte.nicht.geladen")));
document.head.appendChild(script);
});
}
@@ -127,7 +127,7 @@
}
function updateCleanupProgress(removed, total) {
cleanupStatus.textContent = `Entferne alte Streckendaten aus dem Speicher (${removed} / ${total})`;
cleanupStatus.textContent = globalThis.Wegwichtel.I18n.t("bootstrap-loader.alten.fortschritt.loschen", { value1: removed, value2: total });
cleanupStatus.hidden = false;
}
@@ -161,7 +161,7 @@
screen.classList.add('ready');
readyText.hidden = false;
setStatusText('Vorbereitung abgeschlossen.');
setStatusText(globalThis.Wegwichtel.I18n.t("bootstrap.ready"));
actionStatus?.classList.add('is-leaving');
enter.hidden = false;
const showEnter = () => enter.classList.add('is-visible');
@@ -222,7 +222,7 @@
console.error(error);
const current = nextPendingStep();
if (current) mark(current, 'failed');
errorBox.textContent = error.message;
errorBox.textContent = globalThis.Wegwichtel.UserMessages?.error(error, globalThis.Wegwichtel.I18n.t("bootstrap-loader.wegwichtel.konnte.nicht.starten.prufe.deine.verbindung")) || globalThis.Wegwichtel.I18n.t("bootstrap-loader.wegwichtel.konnte.nicht.starten.bitte.versuche.es");
errorBox.hidden = false;
retry.hidden = false;
}
+77 -77
View File
@@ -141,17 +141,17 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
const next = highest + 1;
if (!Number.isSafeInteger(next)) {
throw new TypeError('Für diese lokale Strecke kann keine weitere Streckenpunkt-ID vergeben werden.');
throw new TypeError(ns.I18n.t("editor-ui.zu.dieser.strecke.kann.gerade.keine.weitere"));
}
return next;
}
function routeName(route) {
return text(route?.name || route?.title || 'Unbenannte Strecke');
return text(route?.name || route?.title || ns.I18n.t("editor-ui.unbenannte.strecke"));
}
function poiTitle(poi) {
return text(poi?.title || poi?.name || 'Unbenannter Streckenpunkt');
return text(poi?.title || poi?.name || ns.I18n.t("editor-ui.station.ohne.namen"));
}
function apiBase() {
@@ -184,7 +184,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function setStatus(message, isError = false) {
const status = byId(ids.status);
if (!status) return;
status.textContent = message;
status.textContent = isError ? (ns.UserMessages?.error(message) || message) : message;
status.hidden = !message;
status.dataset.kind = isError ? 'error' : 'notice';
}
@@ -196,9 +196,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function routeDialogTitle() {
if (state.currentSource === 'local') {
return routeId(state.currentRoute) ? 'Lokale Strecke bearbeiten' : 'Neue lokale Strecke';
return routeId(state.currentRoute) ? ns.I18n.t("editor-ui.eigene.strecke.bearbeiten") : ns.I18n.t("editor-ui.neue.strecke");
}
return 'Veröffentlichte Strecke bearbeiten';
return ns.I18n.t("editor-ui.strecke.ansehen");
}
function showListView() {
@@ -216,7 +216,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function showPoiView() {
setDialogTitle(state.currentPoi ? 'Streckenpunkt bearbeiten' : 'Streckenpunkt hinzufügen');
setDialogTitle(state.currentPoi ? ns.I18n.t("editor-ui.station.bearbeiten") : ns.I18n.t("editor-ui.station.hinzufugen"));
setHidden(byId(ids.routeListView), true);
setHidden(byId(ids.routeDetailView), true);
setHidden(byId(ids.poiFormView), false);
@@ -264,8 +264,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function handleResponse(response) {
if (response.ok) return parseJson(response);
const message = await response.text();
const error = new Error(message || `HTTP ${response.status}`);
const payload = await parseJson(response);
const error = new Error(payload.message || ns.I18n.t("editor-ui.die.aktion.konnte.nicht.ausgefuhrt.werden.bitte"));
error.status = response.status;
throw error;
}
@@ -354,13 +354,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const label = document.createElement('div');
appendText(label, 'strong', routeName(route));
appendText(label, 'small', routeSource(route) === 'local' ? 'Lokaler Entwurf' : 'Veröffentlichte Strecke');
appendText(label, 'small', routeSource(route) === 'local' ? ns.I18n.t("app.eigener.entwurf") : ns.I18n.t("editor-ui.veroffentlichte.strecke"));
const actions = document.createElement('div');
actions.className = classes.iconRow;
actions.appendChild(iconButton('Strecke bearbeiten', '✎', 'edit-route'));
actions.appendChild(iconButton(ns.I18n.t("editor-ui.strecke.bearbeiten"), '✎', 'edit-route'));
if (routeSource(route) === 'local') {
actions.appendChild(iconButton('Strecke löschen', '×', 'delete-route', false, true));
actions.appendChild(iconButton(ns.I18n.t("editor-ui.strecke.loschen"), '×', 'delete-route', false, true));
}
item.append(label, actions);
return item;
@@ -380,13 +380,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const title = poiTitle(poi);
const label = document.createElement('div');
appendText(label, 'strong', title);
appendText(label, 'small', `Streckenpunkt ${index + 1}`);
appendText(label, 'small', ns.I18n.t("editor-ui.station", { value1: index + 1 }));
const actions = document.createElement('div');
actions.className = classes.iconRow;
actions.appendChild(iconButton(`${title} nach oben schieben`, '↑', 'move-poi-up', index === 0));
actions.appendChild(iconButton(`${title} nach unten schieben`, '↓', 'move-poi-down', index === state.pois.length - 1));
actions.appendChild(iconButton(ns.I18n.t("editor.station.up", { value1: title }), '↑', 'move-poi-up', index === 0));
actions.appendChild(iconButton(ns.I18n.t("editor.station.down", { value1: title }), '↓', 'move-poi-down', index === state.pois.length - 1));
actions.appendChild(iconButton(`${title} bearbeiten`, '✎', 'edit-poi'));
actions.appendChild(iconButton(`${title} löschen`, '×', 'delete-poi', false, true));
actions.appendChild(iconButton(ns.I18n.t("editor-ui.loschen", { value1: title }), '×', 'delete-poi', false, true));
item.append(label, actions);
return item;
}
@@ -409,8 +409,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
list.textContent = '';
const serverRoutes = state.routes.filter(route => routeSource(route) === 'server');
const localRoutes = state.routes.filter(route => routeSource(route) === 'local');
renderRouteGroup(list, 'Veröffentlichte Strecken', serverRoutes, 'Keine veröffentlichte Strecke vorhanden.');
renderRouteGroup(list, 'Lokale Strecken', localRoutes, 'Noch kein lokaler Entwurf vorhanden.');
renderRouteGroup(list, ns.I18n.t("editor-ui.strecken.fur.alle"), serverRoutes, ns.I18n.t("app.hier.gibt.es.noch.keine.strecken.fur"));
renderRouteGroup(list, ns.I18n.t("editor-ui.meine.entwurfe"), localRoutes, ns.I18n.t("editor-ui.noch.kein.eigener.entwurf.vorhanden"));
}
function renderPoiList() {
@@ -420,7 +420,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (!state.pois.length) {
const item = document.createElement('li');
item.className = classes.poiItem;
appendText(item, 'span', 'Noch keine Streckenpunkte angelegt.');
appendText(item, 'span', ns.I18n.t("editor-ui.noch.keine.stationen.angelegt"));
list.appendChild(item);
return;
}
@@ -437,7 +437,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
async function loadRoutes() {
setStatus('Strecken werden geladen …');
setStatus(ns.I18n.t("editor-ui.strecken.werden.geladen"));
const payload = await readJson('/routes?activatedOnly=true');
const serverRoutes = routeArray(payload).map(route => ({ ...route, _source: 'server' }));
let localRoutes = [];
@@ -496,7 +496,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function fillRouteForm(route) {
byId(ids.routeId).value = routeId(route);
byId(ids.routeName).value = routeName(route) === 'Unbenannte Strecke' ? '' : routeName(route);
byId(ids.routeName).value = routeName(route) === ns.I18n.t("editor-ui.unbenannte.strecke") ? '' : routeName(route);
byId(ids.routeDescription).value = text(route?.description);
byId(ids.routeGpx).value = '';
setHidden(byId(ids.routeGpxGroup), state.currentSource !== 'server');
@@ -506,16 +506,16 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const uploaded = Boolean(route?.serverRouteId);
setHidden(uploadButton, false);
setDisabled(uploadButton, uploaded);
if (uploadButton) uploadButton.textContent = uploaded ? 'Bereits übertragen' : 'An Server übertragen';
if (uploadButton) uploadButton.textContent = uploaded ? ns.I18n.t("editor-ui.schon.zur.prufung.gesendet") : ns.I18n.t("editor-ui.zur.prufung.senden");
byId(ids.routeActivated).textContent = uploaded
? `Lokaler Entwurf. Bereits als unveröffentlichte Server-Strecke ${route.serverRouteId} zur Prüfung übertragen.`
: 'Lokaler Entwurf: GPX, Texte und Medien bleiben im Browser, bis „An Server übertragen“ gewählt wird.';
? ns.I18n.t("editor-ui.diese.strecke.wurde.zur.prufung.gesendet.andere")
: ns.I18n.t("editor-ui.dieser.entwurf.bleibt.auf.deinem.gerat.mit");
} else {
setHidden(uploadButton, true);
setDisabled(uploadButton, true);
byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at
? `Veröffentlicht am ${route.activatedAt || route.activated_at}. Der Editor verändert die Freigabe nicht.`
: 'Server-Strecke ohne Freigabedatum. Der Editor verändert activated_at nicht.';
? ns.I18n.t("editor-ui.diese.strecke.ist.fur.alle.freigegeben.und")
: ns.I18n.t("editor-ui.diese.strecke.wurde.noch.nicht.fur.alle");
}
}
@@ -559,7 +559,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
select.dataset.mediaKey = key;
select.type = 'button';
select.setAttribute('aria-expanded', String(state.selectedMediaKey === key));
select.setAttribute('aria-label', `${label}: Entfernen-Schaltfläche ${state.selectedMediaKey === key ? 'ausblenden' : 'anzeigen'}`);
select.setAttribute('aria-label', ns.I18n.t("editor-ui.knopf.zum.entfernen", { value1: label, value2: state.selectedMediaKey === key ? 'ausblenden' : 'anzeigen' }));
if (kind === 'image' && previewUrl) {
const image = document.createElement('img');
@@ -603,7 +603,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
preview.appendChild(mediaPreviewCard({
key: 'existing-audio',
kind: 'audio',
label: poi?.audio?.name || 'Audioaufnahme',
label: poi?.audio?.name || 'Tonaufnahme',
source: 'existing'
}));
}
@@ -612,7 +612,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
preview.appendChild(mediaPreviewCard({
key: 'pending-audio',
kind: 'audio',
label: state.pendingAudioFile.name || 'Neue Audioaufnahme',
label: state.pendingAudioFile.name || ns.I18n.t("editor-ui.neue.tonaufnahme"),
source: 'pending'
}));
}
@@ -622,7 +622,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
preview.appendChild(mediaPreviewCard({
key: `existing-image-${image.id ?? index}`,
kind: 'image',
label: image.name || image.caption || `Bild ${index + 1}`,
label: image.name || image.caption || ns.I18n.t("editor-ui.bild", { value1: index + 1 }),
mediaId: image.id ?? '',
mediaIndex: index,
previewUrl: imagePreviewUrl(image),
@@ -634,7 +634,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
preview.appendChild(mediaPreviewCard({
key: pending.key,
kind: 'image',
label: pending.file?.name || `Neues Bild ${index + 1}`,
label: pending.file?.name || ns.I18n.t("editor-ui.neues.bild", { value1: index + 1 }),
mediaIndex: index,
previewUrl: pending.previewUrl || null,
source: 'pending'
@@ -651,12 +651,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const imageCount = existingImages + state.pendingImages.length;
const parts = [];
if (audioCount) parts.push(`${audioCount} Audio${audioCount === 1 ? 'aufnahme' : 'aufnahmen'}`);
if (imageCount) parts.push(`${imageCount} Bild${imageCount === 1 ? '' : 'er'}`);
if (audioCount) parts.push(ns.I18n.t('media.audio.count', { count: audioCount }));
if (imageCount) parts.push(ns.I18n.t('media.image.count', { count: imageCount }));
status.textContent = parts.length
? `${parts.join(' · ')}. Vorschau antippen, um die Entfernen-Schaltfläche einzublenden.`
: 'Noch keine Medien vorhanden.';
? ns.I18n.t("editor-ui.tippe.auf.ein.bild.oder.eine.tonaufnahme", { value1: parts.join(' · ') })
: ns.I18n.t("editor-ui.noch.keine.bilder.oder.tonaufnahmen.vorhanden");
}
function openFilePicker(inputId) {
@@ -668,12 +668,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (kind === 'audio') {
return {
multiple: false,
types: [{ description: 'Audio', accept: { 'audio/*': ['.mp3', '.m4a', '.wav', '.ogg', '.webm'] } }]
types: [{ description: ns.I18n.t("editor-ui.tonaufnahmen"), accept: { 'audio/*': ['.mp3', '.m4a', '.wav', '.ogg', '.webm'] } }]
};
}
return {
multiple: true,
types: [{ description: 'Bilder', accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
types: [{ description: ns.I18n.t("editor-ui.bilder"), accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
};
}
@@ -721,7 +721,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function fillPoiForm(poi) {
resetPendingMedia();
byId(ids.poiId).value = poiId(poi);
byId(ids.poiTitle).value = poiTitle(poi) === 'Unbenannter Streckenpunkt' ? '' : poiTitle(poi);
byId(ids.poiTitle).value = poiTitle(poi) === ns.I18n.t("editor-ui.station.ohne.namen") ? '' : poiTitle(poi);
byId(ids.poiDescription).value = text(poi?.description || poi?.text);
byId(ids.poiLat).value = text(poi?.lat ?? poi?.latitude);
byId(ids.poiLon).value = text(poi?.lon ?? poi?.longitude);
@@ -759,12 +759,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function loadRoute(route) {
const id = routeId(route);
if (!id) return;
setStatus('Streckendetails werden geladen …');
setStatus(ns.I18n.t("editor.route.loading"));
state.currentSource = routeSource(route);
if (state.currentSource === 'local') {
const routePackage = await ns.LocalRoutes.getWithMedia(id, { requestPermission: true });
if (!routePackage) throw new Error('Der lokale Entwurf wurde nicht gefunden.');
if (!routePackage) throw new Error(ns.I18n.t("editor-ui.dein.entwurf.wurde.nicht.gefunden"));
state.currentRoute = {
...routePackage.route,
id: routePackage.id,
@@ -792,7 +792,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
id: '',
name: '',
description: '',
gpx: ns.LocalRoutes.emptyGpx('Neue Strecke'),
gpx: ns.LocalRoutes.emptyGpx(ns.I18n.t("editor-ui.neue.strecke")),
_source: 'local'
};
state.currentPoi = null;
@@ -800,7 +800,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
fillRouteForm(state.currentRoute);
renderPoiList();
showRouteView();
setStatus('Neue Strecke wird als lokaler GPX-/JSON-Entwurf angelegt.');
setStatus(ns.I18n.t("editor-ui.deine.neue.strecke.bleibt.zuerst.auf.diesem"));
}
function formDataFromRoute(values) {
@@ -838,12 +838,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.pois = saved.pois;
await loadRoutes();
await loadRoute({ id: saved.id, _source: 'local' });
setStatus('Speichern erfolgreich.');
setStatus(ns.I18n.t("editor-ui.gespeichert"));
}
async function saveServerRoute(values) {
const id = routeId(state.currentRoute);
if (!id) throw new Error('Neue Strecken werden ausschließlich lokal angelegt.');
if (!id) throw new Error(ns.I18n.t("editor-ui.lege.eine.neue.strecke.zuerst.auf.diesem"));
const file = byId(ids.routeGpx)?.files?.[0];
const path = `/routes/${encodeURIComponent(id)}`;
const payload = file
@@ -853,12 +853,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.currentRoute = { ...saved, _source: 'server' };
await loadRoutes();
await loadRoute({ id, _source: 'server' });
setStatus('Speichern erfolgreich.');
setStatus(ns.I18n.t("editor-ui.gespeichert"));
}
async function saveRoute() {
const values = routeFromForm();
if (!values.name) throw new Error('Der Name der Strecke ist erforderlich.');
if (!values.name) throw new Error(ns.I18n.t("editor-ui.gib.deiner.strecke.einen.namen"));
if (state.currentSource === 'server') await saveServerRoute(values);
else await saveLocalRoute(values);
}
@@ -877,7 +877,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function localRouteUploadForm(routePackage) {
const data = new FormData();
data.append('name', routePackage.route?.name || 'Unbenannte Strecke');
data.append('name', routePackage.route?.name || ns.I18n.t("editor-ui.unbenannte.strecke"));
data.append('description', routePackage.route?.description || '');
const uploadGpx = ns.LocalRoutes.gpxForUpload(routePackage);
const gpx = new Blob([uploadGpx], { type: 'application/gpx+xml' });
@@ -888,7 +888,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function storedMediaBlob(media) {
const blob = await ns.LocalRoutes.mediaBlob(media, { requestPermission: true });
if (!blob) {
throw new Error(`Die Mediendatei ${media?.name || ''} konnte nicht für den Upload vorbereitet werden.`.trim());
throw new Error(ns.I18n.t("editor-ui.die.datei.konnte.nicht.gesendet.werden.bitte", { value1: media?.name || '' }).trim());
}
return blob;
}
@@ -918,12 +918,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
const serverRoute = payload.route || payload;
createdRouteId = routeId(serverRoute);
if (!createdRouteId) throw new Error('Der Server hat keine Strecken-ID für den Upload zurückgegeben.');
if (!createdRouteId) throw new Error(ns.I18n.t("editor-ui.die.strecke.konnte.nicht.gesendet.werden.bitte"));
const orderedPois = [...(routePackage.pois || [])]
.sort((left, right) => Number(left.sequence) - Number(right.sequence));
for (const [index, poi] of orderedPois.entries()) {
const poiTitle = poi.title || `Streckenpunkt ${index + 1}`;
const poiTitle = poi.title || ns.I18n.t("editor-ui.station", { value1: index + 1 });
const poiPayload = await sendJson(`/routes/${encodeURIComponent(createdRouteId)}/pois`, 'POST', {
description: poi.description || '',
lat: poi.lat,
@@ -934,7 +934,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
});
const serverPoi = poiPayload.poi || poiPayload;
const serverPoiId = poiId(serverPoi);
if (!serverPoiId) throw new Error(`Für ${poiTitle} wurde keine Server-ID zurückgegeben.`);
if (!serverPoiId) throw new Error(ns.I18n.t("editor-ui.die.station.konnte.nicht.gesendet.werden.bitte", { value1: poiTitle }));
await uploadLocalPoiMedia(createdRouteId, serverPoiId, poi);
}
return createdRouteId;
@@ -951,11 +951,11 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
async function uploadLocalRoute() {
if (state.currentSource !== 'local') throw new Error('Nur lokale Strecken können hochgeladen werden.');
if (state.currentSource !== 'local') throw new Error(ns.I18n.t("editor-ui.du.kannst.nur.deine.eigenen.entwurfe.zur"));
const values = routeFromForm();
if (!values.name) throw new Error('Der Name der Strecke ist erforderlich.');
if (!values.name) throw new Error(ns.I18n.t("editor-ui.gib.deiner.strecke.einen.namen"));
if (state.currentRoute?.serverRouteId) {
setStatus('Diese Strecke wurde bereits an den Server übertragen.');
setStatus(ns.I18n.t("editor-ui.diese.strecke.wurde.schon.zur.prufung.gesendet"));
return;
}
@@ -963,10 +963,10 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
setDisabled(uploadButton, true);
try {
await saveLocalRoute(values);
setStatus('Übertragung läuft …');
setStatus(ns.I18n.t("editor-ui.deine.strecke.wird.gesendet"));
const localId = routeId(state.currentRoute);
const routePackage = ns.LocalRoutes.get(localId);
if (!routePackage) throw new Error('Der lokale Streckenentwurf wurde nicht gefunden.');
if (!routePackage) throw new Error(ns.I18n.t("editor-ui.dein.entwurf.wurde.nicht.gefunden"));
const serverRouteId = await createServerRouteFromLocal(routePackage);
const uploadedAt = new Date().toISOString();
@@ -982,7 +982,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.pois = saved.pois;
fillRouteForm(state.currentRoute);
await loadRoutes();
setStatus('Upload erfolgreich.');
setStatus(ns.I18n.t("editor-ui.zur.prufung.gesendet"));
} finally {
if (!state.currentRoute?.serverRouteId) setDisabled(uploadButton, false);
}
@@ -997,7 +997,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const lat = Number(position?.lat);
const lon = Number(position?.lon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
setStatus('Für einen neuen Streckenpunkt muss zuerst eine aktuelle GPS-Position verfügbar sein.', true);
setStatus(ns.I18n.t("editor-ui.fur.eine.neue.station.brauchen.wir.deinen"), true);
return;
}
@@ -1010,20 +1010,20 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
lon,
triggerRadiusM: Number.isFinite(accuracy) && accuracy > 0 ? accuracy : 25
});
byId(ids.poiFormTitle).textContent = 'Streckenpunkt hinzufügen';
byId(ids.poiFormTitle).textContent = ns.I18n.t("editor-ui.station.hinzufugen");
showPoiView();
}
function editPoi(poi) {
state.currentPoi = poi;
fillPoiForm(poi);
byId(ids.poiFormTitle).textContent = 'Streckenpunkt bearbeiten';
byId(ids.poiFormTitle).textContent = ns.I18n.t("editor-ui.station.bearbeiten");
showPoiView();
}
async function mediaFromLocalForm(existingPoi, poiIdentifier) {
const routeIdentifier = routeId(state.currentRoute);
if (!routeIdentifier) throw new Error('Speichere zuerst die Strecke.');
if (!routeIdentifier) throw new Error(ns.I18n.t("editor-ui.speichere.zuerst.die.strecke"));
const storedNow = [];
const replacedAfterSave = [];
@@ -1063,7 +1063,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function persistLocalCurrentRoute() {
const id = routeId(state.currentRoute);
if (!id) throw new Error('Speichere zuerst die Strecke.');
if (!id) throw new Error(ns.I18n.t("editor-ui.speichere.zuerst.die.strecke"));
const currentPoiId = poiId(state.currentPoi);
const saved = ns.LocalRoutes.save({
id,
@@ -1082,7 +1082,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function saveLocalPoi(values) {
if (!Number.isFinite(values.lat) || !Number.isFinite(values.lon)) {
throw new TypeError('Für einen Streckenpunkt sind gültige Breiten- und Längengrade erforderlich.');
throw new TypeError(ns.I18n.t("editor-ui.der.ort.dieser.station.fehlt.erlaube.deinen"));
}
const currentId = numericPoiId(byId(ids.poiId).value);
const identifier = currentId ?? nextLocalPoiId();
@@ -1109,7 +1109,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
await Promise.allSettled(media.replacedAfterSave.map(ns.LocalRoutes.removeMedia));
resetPendingMedia();
setStatus('Streckenpunkt und zugehörige Mediendaten lokal gespeichert.');
setStatus(ns.I18n.t("editor-ui.station.mit.bildern.und.ton.auf.diesem"));
await loadRoute({ id: routeId(state.currentRoute), _source: 'local' });
}
@@ -1132,7 +1132,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function saveServerPoi(values) {
const currentRouteId = routeId(state.currentRoute);
if (!currentRouteId) throw new Error('Speichere zuerst die Strecke.');
if (!currentRouteId) throw new Error(ns.I18n.t("editor-ui.speichere.zuerst.die.strecke"));
const currentPoiId = byId(ids.poiId).value;
const path = currentPoiId
? `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`
@@ -1142,13 +1142,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const saved = payload.poi || payload;
const savedPoiId = poiId(saved) || currentPoiId;
if (savedPoiId) await uploadPoiMedia(currentRouteId, savedPoiId);
setStatus('Streckenpunkt gespeichert.');
setStatus(ns.I18n.t("editor-ui.station.gespeichert"));
await loadRoute({ id: currentRouteId, _source: 'server' });
}
async function savePoi() {
const values = poiFromForm();
if (!values.title) throw new Error('Der Titel des Streckenpunkts ist erforderlich.');
if (!values.title) throw new Error(ns.I18n.t("editor-ui.gib.deiner.station.einen.namen"));
if (state.currentSource === 'local') await saveLocalPoi(values);
else await saveServerPoi(values);
}
@@ -1196,13 +1196,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.selectedMediaKey = null;
renderPoiMediaPreview();
updatePoiMediaStatus();
setStatus(kind === 'audio' ? 'Audioaufnahme entfernt.' : 'Bild entfernt.');
setStatus(kind === 'audio' ? ns.I18n.t("editor.audio.removed") : ns.I18n.t("editor-ui.bild.entfernt"));
}
async function removeExistingServerMedia(kind, mediaId) {
const currentRouteId = routeId(state.currentRoute);
const currentPoiId = poiId(state.currentPoi);
if (!currentRouteId || !currentPoiId) throw new Error('Der Streckenpunkt ist noch nicht gespeichert.');
if (!currentRouteId || !currentPoiId) throw new Error(ns.I18n.t("editor-ui.die.station.ist.noch.nicht.gespeichert"));
const basePath = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`;
const updated = { ...state.currentPoi };
@@ -1212,7 +1212,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
updated.audioUrl = null;
updated.audio_path = null;
} else {
if (!mediaId) throw new Error('Das Bild besitzt keine Server-ID und kann nicht entfernt werden.');
if (!mediaId) throw new Error(ns.I18n.t("editor-ui.das.bild.konnte.nicht.entfernt.werden.lade"));
await sendDelete(`${basePath}/pictures/${encodeURIComponent(mediaId)}`);
updated.images = (state.currentPoi.images || []).filter(image => text(image.id) !== text(mediaId));
}
@@ -1221,7 +1221,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.selectedMediaKey = null;
renderPoiMediaPreview();
updatePoiMediaStatus();
setStatus(kind === 'audio' ? 'Audioaufnahme entfernt.' : 'Bild entfernt.');
setStatus(kind === 'audio' ? ns.I18n.t("editor.audio.removed") : ns.I18n.t("editor-ui.bild.entfernt"));
}
async function removeMedia(control) {
@@ -1266,13 +1266,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await Promise.allSettled(removedMedia.map(ns.LocalRoutes.removeMedia));
renderPoiList();
showRouteView();
setStatus('Streckenpunkt entfernt und lokale Strecke aktualisiert.');
setStatus(ns.I18n.t("editor-ui.station.entfernt.deine.strecke.wurde.gespeichert"));
return;
}
await sendDelete(`/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`);
await loadRoute({ id: currentRouteId, _source: 'server' });
setStatus('Streckenpunkt entfernt.');
setStatus(ns.I18n.t("editor-ui.station.entfernt"));
}
function reorderPois(index, offset) {
@@ -1295,7 +1295,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (!currentRouteId) return;
if (state.currentSource === 'local') {
await persistLocalCurrentRoute();
setStatus('Lokale Streckenpunkt-Reihenfolge gespeichert.');
setStatus(ns.I18n.t("editor-ui.reihenfolge.der.stationen.gespeichert"));
return;
}
const jobs = state.pois.map((poi, index) => {
@@ -1305,7 +1305,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
return sendJson(path, 'PUT', { ...poi, sequence: index, sortOrder: index });
}).filter(Boolean);
await Promise.all(jobs);
setStatus('Reihenfolge gespeichert.');
setStatus(ns.I18n.t("editor.order.saved"));
}
function routeForItem(target) {
@@ -1340,7 +1340,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await loadRoutes();
showListView();
setStatus('Strecke gelöscht.');
setStatus(ns.I18n.t("editor-ui.strecke.geloscht"));
}
async function routeListAction(event) {
@@ -1387,7 +1387,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await ns.StorageUsage?.refresh();
} catch (error) {
ns.StorageUsage?.reportError(error);
setStatus(error.message || 'Die Aktion konnte nicht ausgeführt werden.', true);
setStatus(ns.UserMessages?.error(error) || ns.I18n.t("editor-ui.das.hat.leider.nicht.geklappt.bitte.versuche"), true);
}
}
+1 -1
View File
@@ -25,7 +25,7 @@
}
function unsupportedError() {
return new Error('Dieses Gerät unterstützt keine Geolokalisierung.');
return new Error(ns.I18n.t("geolocation.dieses.gerat.kann.deinen.standort.nicht.finden"));
}
ns.Geo = {
+11 -11
View File
@@ -4,18 +4,18 @@ globalThis.Wegwichtel ??= {};
'use strict';
const entries = Object.freeze([
['🪵', 'Holz'],
['🪨', 'Felsen'],
['🏮', 'Laterne'],
['🌼', 'Blume'],
['🚬', 'Pfeife'],
['🐦', 'Vogel'],
['🍄', 'Pilz'],
['🍓', 'Erdbeere'],
['🎃', 'Kürbis']
].map(([unicode, label]) => Object.freeze({
['🪵', "group-symbols.holz"],
['🪨', "group-symbols.felsen"],
['🏮', "group-symbols.laterne"],
['🌼', "group-symbols.blume"],
['🚬', "group-symbols.pfeife"],
['🐦', "group-symbols.vogel"],
['🍄', "group-symbols.pilz"],
['🍓', "group-symbols.erdbeere"],
['🎃', "group-symbols.kurbis"]
].map(([unicode, labelKey]) => Object.freeze({
unicode,
label,
get label() { return ns.I18n.t(labelKey); },
imageUrl: `/images/group-symbols/${unicode}.png`
})));
+54
View File
@@ -0,0 +1,54 @@
globalThis.Wegwichtel ??= {};
(function (ns) {
'use strict';
class WegwichtelI18n {
static #languages = new Map([['de', ns.TextsDe.messages]]);
static #language = 'de';
static get language() { return this.#language; }
static register(language, messages) {
this.#languages.set(language.toLowerCase(), Object.freeze({ ...messages }));
}
static setLanguage(language, root = globalThis.document) {
const requested = String(language).toLowerCase();
const base = requested.split('-')[0];
this.#language = 'de';
if (this.#languages.has(base)) this.#language = base;
if (this.#languages.has(requested)) this.#language = requested;
this.apply(root);
return this.#language;
}
static t(key, variables = {}) {
let value = this.#languages.get(this.#language)?.[key] ?? ns.TextsDe.messages[key];
if (value == null) throw new RangeError(`Missing translation: ${key}`);
if (typeof value === 'object') {
const plural = new Intl.PluralRules(this.#language).select(Number(variables.count));
value = value[plural] ?? value.other;
}
return value.replace(/\{([a-zA-Z0-9_]+)\}/g, (placeholder, name) => {
if (!Object.hasOwn(variables, name)) return placeholder;
return String(variables[name] ?? '');
});
}
static apply(root = globalThis.document) {
if (!root?.querySelectorAll) return;
if (root.documentElement) root.documentElement.lang = this.#language;
for (const element of root.querySelectorAll('[data-i18n]')) {
if (typeof element.getAttribute === 'function') element.textContent = this.t(element.getAttribute('data-i18n'));
}
for (const attribute of ['title', 'aria-label', 'placeholder', 'alt']) {
for (const element of root.querySelectorAll(`[data-i18n-${attribute}]`)) {
if (typeof element.getAttribute === 'function' && typeof element.setAttribute === 'function') element.setAttribute(attribute, this.t(element.getAttribute(`data-i18n-${attribute}`)));
}
}
}
}
ns.I18n = WegwichtelI18n;
WegwichtelI18n.apply();
}(globalThis.Wegwichtel));
+20 -20
View File
@@ -5,15 +5,15 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const DROPDOWN_ANIMATION_MS = 180;
const pageTitles = Object.freeze({
privacy: 'Datenschutzerklärung',
imprint: 'Impressum',
technology: 'Cookies und Technik'
get privacy() { return ns.I18n.t("legal-ui.datenschutzerklarung"); },
get imprint() { return ns.I18n.t("legal-ui.impressum"); },
get technology() { return ns.I18n.t("legal-ui.speichern.und.berechtigungen"); }
});
const menuEntries = Object.freeze([
['privacy', 'Datenschutzerklärung'],
['imprint', 'Impressum'],
['technology', 'Cookies und verwendete JavaScripts']
]);
function menuEntries() { return [
['privacy', ns.I18n.t("legal-ui.datenschutzerklarung")],
['imprint', ns.I18n.t("legal-ui.impressum")],
['technology', ns.I18n.t("legal-ui.speichern.und.berechtigungen")]
]; }
const selectors = Object.freeze({
content: '.info-dialog-content',
cookieToggle: '.cookie-toggle',
@@ -159,8 +159,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
id: 'info-button',
className: 'info-button',
type: 'button',
'aria-label': 'Informationsmenü öffnen',
title: 'Informationen'
'aria-label': ns.I18n.t("legal-ui.hilfe.und.informationen.offnen"),
title: ns.I18n.t("legal-ui.informationen")
});
append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i'));
append(control, infoButton);
@@ -173,7 +173,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const dropdown = createElement('nav', {
id: 'info-dropdown',
className: 'info-dropdown',
'aria-label': 'Rechtliche und technische Informationen'
'aria-label': ns.I18n.t("legal-ui.rechtliche.und.technische.informationen")
});
append(control, dropdown);
return dropdown;
@@ -191,7 +191,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function setInfoButtonLabel(infoButton) {
if (typeof infoButton.getAttribute === 'function' && infoButton.getAttribute('aria-label')) return;
setAttribute(infoButton, 'aria-label', 'Informationsmenü öffnen');
setAttribute(infoButton, 'aria-label', ns.I18n.t("legal-ui.hilfe.und.informationen.offnen"));
}
function ensureInfoButtonIcon(infoButton) {
@@ -201,9 +201,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function configureDropdown(dropdown) {
if (!dropdown) return;
setAttribute(dropdown, 'aria-label', 'Rechtliche und technische Informationen');
setAttribute(dropdown, 'aria-label', ns.I18n.t("legal-ui.rechtliche.und.technische.informationen"));
setHidden(dropdown, true);
menuEntries.forEach(([target, label]) => ensureMenuButton(dropdown, target, label));
menuEntries().forEach(([target, label]) => ensureMenuButton(dropdown, target, label));
}
function ensureMenuButton(dropdown, target, label) {
@@ -277,7 +277,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function configureBackButton(backButton) {
setAttribute(backButton, 'type', 'button');
setAttribute(backButton, 'aria-label', 'Zurück zum Informationsmenü');
setAttribute(backButton, 'aria-label', ns.I18n.t("legal-ui.zuruck.zum.informationsmenu"));
setHidden(backButton, true);
return backButton;
}
@@ -293,8 +293,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const closeButton = createElement('button', {
id: 'info-close',
type: 'button',
'aria-label': 'Informationsfenster schließen',
title: 'Schließen'
'aria-label': ns.I18n.t("legal-ui.informationsfenster.schliessen"),
title: ns.I18n.t("legal-ui.schliessen")
}, '×');
append(header, closeButton);
return configureCloseButton(closeButton);
@@ -302,7 +302,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function configureCloseButton(closeButton) {
setAttribute(closeButton, 'type', 'button');
setAttribute(closeButton, 'aria-label', 'Informationsfenster schließen');
setAttribute(closeButton, 'aria-label', ns.I18n.t("legal-ui.informationsfenster.schliessen"));
if (closeButton && !String(closeButton.textContent ?? '').trim()) closeButton.textContent = '×';
return closeButton;
}
@@ -335,7 +335,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function createFallbackPage(content, name) {
const page = createElement('article', { className: 'info-page', dataset: { infoPage: name } });
append(page, createElement('p', {}, `${pageTitles[name]} ist noch nicht hinterlegt.`));
append(page, createElement('p', {}, ns.I18n.t("info.missing", { value1: pageTitles[name] })));
append(content, page);
return page;
}
@@ -436,7 +436,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function updateDialogTitle(pageName) {
if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? 'Informationen';
if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? ns.I18n.t("legal-ui.informationen");
}
function focusSafely(element) {
+32 -32
View File
@@ -15,7 +15,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function storage() {
const target = globalThis.localStorage;
if (!target) throw new Error('Lokaler Browser-Speicher ist auf diesem Gerät nicht verfügbar.');
if (!target) throw new Error(ns.I18n.t("local-routes.deine.strecke.kann.auf.diesem.gerat.gerade"));
return target;
}
@@ -27,7 +27,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (mediaDatabasePromise) return mediaDatabasePromise;
const indexedDb = globalThis.indexedDB;
if (!indexedDb || typeof indexedDb.open !== 'function') {
return Promise.reject(new Error('Der lokale Medienspeicher (IndexedDB) ist in diesem Browser nicht verfügbar.'));
return Promise.reject(new Error(ns.I18n.t("local-routes.bilder.und.ton.konnen.auf.diesem.gerat")));
}
mediaDatabasePromise = new Promise((resolve, reject) => {
@@ -41,11 +41,11 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
request.onsuccess = () => resolve(request.result);
request.onerror = () => {
mediaDatabasePromise = null;
reject(request.error || new Error('Der lokale Medienspeicher konnte nicht geöffnet werden.'));
reject(request.error || new Error(ns.I18n.t("local-routes.gespeicherte.bilder.und.ton.konnten.nicht.geoffnet")));
};
request.onblocked = () => {
mediaDatabasePromise = null;
reject(new Error('Der lokale Medienspeicher ist durch eine andere Browser-Sitzung blockiert.'));
reject(new Error(ns.I18n.t("local-routes.schliesse.andere.fenster.mit.wegwichtel.und.versuche")));
};
});
return mediaDatabasePromise;
@@ -59,8 +59,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const transaction = database.transaction(MEDIA_STORE_NAME, mode);
request = operation(transaction.objectStore(MEDIA_STORE_NAME));
request.onsuccess = () => resolve(request.result ?? null);
request.onerror = () => reject(request.error || new Error('Der lokale Medienspeicher konnte nicht gelesen oder geschrieben werden.'));
transaction.onabort = () => reject(transaction.error || new Error('Die Medien-Speicheroperation wurde abgebrochen.'));
request.onerror = () => reject(request.error || new Error(ns.I18n.t("local-routes.bilder.und.ton.konnten.nicht.geoffnet.oder")));
transaction.onabort = () => reject(transaction.error || new Error(ns.I18n.t("local-routes.das.speichern.von.bildern.und.ton.wurde")));
} catch (error) {
reject(error);
}
@@ -100,7 +100,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
};
} catch (error) {
if (error?.name === 'QuotaExceededError') {
throw new Error('Der lokale Medienspeicher ist voll. Entferne nicht mehr benötigte lokale Medien oder Strecken.', { cause: error });
throw new Error(ns.I18n.t("local-routes.der.platz.fur.bilder.und.ton.ist"), { cause: error });
}
// Manche Browser können FileSystemFileHandle noch nicht in IndexedDB serialisieren.
// In diesem Fall wird die Datei als Blob in IndexedDB gespeichert.
@@ -111,9 +111,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await putMediaRecord({ ...metadata, storage: 'indexeddb', blob: file });
} catch (error) {
const prefix = error?.name === 'QuotaExceededError'
? 'Der lokale Medienspeicher ist voll.'
: 'Die Mediendatei konnte nicht lokal gespeichert werden.';
throw new Error(`${prefix} Entferne nicht mehr benötigte lokale Medien oder Strecken.`, { cause: error });
? ns.I18n.t("local-routes.der.platz.fur.bilder.und.ton.ist.2")
: ns.I18n.t("local-routes.das.bild.oder.die.tonaufnahme.konnte.nicht");
throw new Error(ns.I18n.t("local-routes.entferne.bilder.tonaufnahmen.oder.eigene.strecken.die", { value1: prefix }), { cause: error });
}
return {
id: String(mediaId),
@@ -135,12 +135,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
permission = await handle.requestPermission({ mode: 'read' });
}
if (permission !== 'granted') {
throw new Error('Der Zugriff auf eine lokale Mediendatei muss erneut erlaubt werden.');
throw new Error(ns.I18n.t("local-routes.erlaube.noch.einmal.den.zugriff.auf.das"));
}
try {
return await handle.getFile();
} catch (error) {
throw new Error('Eine referenzierte Mediendatei ist auf dem Gerät nicht mehr verfügbar.', { cause: error });
throw new Error(ns.I18n.t("local-routes.ein.ausgewahltes.bild.oder.eine.tonaufnahme.wurde"), { cause: error });
}
}
@@ -238,7 +238,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (typeof cryptoApi?.getRandomValues === 'function') {
return `local-${uuidFromRandomValues(cryptoApi)}`;
}
throw new Error('Keine sichere Zufallsquelle für eine lokale Strecken-ID verfügbar.');
throw new Error(ns.I18n.t("local-routes.auf.diesem.gerat.kann.gerade.keine.neue"));
}
function createPoiId() {
@@ -255,7 +255,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
.replaceAll("'", '&apos;');
}
function emptyGpx(name = 'Neue Strecke') {
function emptyGpx(name = ns.I18n.t("editor-ui.neue.strecke")) {
const safeName = escapeXml(name);
return `<?xml version="1.0" encoding="UTF-8"?>\n`
+ `<gpx version="1.1" creator="Wegwichtel" xmlns="${GPX_NAMESPACE}" xmlns:wegwichtel="${POI_NAMESPACE}">\n`
@@ -268,7 +268,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const parser = new DOMParser();
const document = parser.parseFromString(String(gpx || ''), 'application/xml');
if (document.querySelector('parsererror') || document.documentElement?.localName !== 'gpx') {
throw new Error('Die GPX-Datei ist nicht gültig.');
throw new Error(ns.I18n.t("local-routes.diese.datei.enthalt.keinen.lesbaren.weg.wahle"));
}
return document;
}
@@ -300,7 +300,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const document = parseXml(gpx);
return Array.from(document.getElementsByTagNameNS('*', 'wpt')).map((node, index) => ({
id: waypointId(node),
title: textOfChild(node, 'name') || `Streckenpunkt ${index + 1}`,
title: textOfChild(node, 'name') || ns.I18n.t("editor-ui.station", { value1: index + 1 }),
description: textOfChild(node, 'desc'),
lat: numberAttribute(node, 'lat'),
lon: numberAttribute(node, 'lon'),
@@ -360,11 +360,11 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function syncGpx(gpx, route, pois) {
const routeName = route?.name || 'Neue Strecke';
const routeName = route?.name || ns.I18n.t("editor-ui.neue.strecke");
const document = parseXml(gpx || emptyGpx(routeName));
updateMetadataName(document, routeName);
// Wegwichtel verwaltet neue Streckenpunkte direkt als Punkte im GPX-Track.
// Wegwichtel verwaltet neue Stationen direkt als Punkte im GPX-Track.
// Fremde/importierte GPX-Punkte ohne Wegwichtel-ID bleiben erhalten.
Array.from(document.getElementsByTagNameNS('*', 'wpt')).forEach(node => node.remove());
Array.from(document.getElementsByTagNameNS('*', 'trkpt'))
@@ -374,9 +374,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const segment = ensureTrackSegment(document, routeName);
const orderedPois = [...(pois || [])].sort((left, right) => Number(left.sequence) - Number(right.sequence));
orderedPois.forEach((poi, index) => {
const title = poi.title || `Streckenpunkt ${index + 1}`;
const title = poi.title || ns.I18n.t("editor-ui.station", { value1: index + 1 });
if (!Number.isFinite(Number(poi.lat)) || !Number.isFinite(Number(poi.lon))) {
throw new TypeError(`Für ${title} fehlen gültige Koordinaten.`);
throw new TypeError(ns.I18n.t("local-routes.der.ort.von.fehlt", { value1: title }));
}
const gpxPoint = createElement(document, 'trkpt');
@@ -469,9 +469,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
ns.StorageUsage?.reportError(error);
const name = error?.name === 'QuotaExceededError'
? 'Der lokale Speicher für Streckendaten ist voll.'
: 'Die lokale Strecke konnte nicht gespeichert werden.';
throw new Error(`${name} Entferne nicht mehr benötigte lokale Strecken.`, { cause: error });
? ns.I18n.t("local-routes.der.platz.fur.deine.strecken.ist.voll")
: ns.I18n.t("local-routes.deine.strecke.konnte.nicht.gespeichert.werden");
throw new Error(ns.I18n.t("local-routes.entferne.eigene.strecken.die.du.nicht.mehr", { value1: name }), { cause: error });
}
notify(id, 'saved');
@@ -514,9 +514,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}
function gpxForUpload(routePackage) {
if (!routePackage?.gpx) throw new Error('Für die lokale Strecke ist keine GPX-Datei vorhanden.');
if (!routePackage?.gpx) throw new Error(ns.I18n.t("local-routes.der.weg.deiner.strecke.fehlt"));
if (!routePoints(routePackage.gpx).length) {
throw new Error('Die lokale Strecke enthält noch keine Streckenpunkte im GPX-Track.');
throw new Error(ns.I18n.t("local-routes.deine.strecke.hat.noch.keinen.aufgezeichneten.weg"));
}
return routePackage.gpx;
}
@@ -577,7 +577,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function appPoi(poi) {
return {
id: Number(poi.id),
title: poi.title || 'Unbenannter Streckenpunkt',
title: poi.title || ns.I18n.t("editor-ui.station.ohne.namen"),
description: poi.description || '',
lat: Number(poi.lat),
lon: Number(poi.lon),
@@ -604,9 +604,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
id: String(routePackage.id),
source: 'local',
local: true,
name: routePackage.route?.name || 'Unbenannte Strecke',
name: routePackage.route?.name || ns.I18n.t("editor-ui.unbenannte.strecke"),
description: routePackage.route?.description || '',
schoolName: 'Lokal gespeichert',
schoolName: ns.I18n.t("local.device"),
status: 'local',
activatedAt: null,
points,
@@ -625,7 +625,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function getAppRoute(id, options = {}) {
const routePackage = await withMedia(readPackage(id), options);
if (!routePackage) throw new Error('Die lokale Strecke wurde nicht gefunden.');
if (!routePackage) throw new Error(ns.I18n.t("local-routes.deine.strecke.wurde.nicht.gefunden"));
return toAppRoute(routePackage);
}
@@ -636,7 +636,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function readFileAsText(file) {
if (!file) return Promise.resolve('');
if (typeof file.text !== 'function') {
return Promise.reject(new TypeError('Die ausgewählte Datei unterstützt Blob.text() nicht.'));
return Promise.reject(new TypeError(ns.I18n.t("local-routes.diese.datei.kann.auf.deinem.gerat.nicht")));
}
return file.text();
}
@@ -647,7 +647,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const reader = new FileReader();
reader.addEventListener('load', () => {
if (typeof reader.result !== 'string') {
reject(new TypeError('Die Datei konnte nicht als Data-URL gelesen werden.'));
reject(new TypeError(ns.I18n.t("local-routes.diese.datei.konnte.nicht.geoffnet.werden.wahle")));
return;
}
resolve({
@@ -657,7 +657,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
type: file.type || 'application/octet-stream'
});
}, { once: true });
reader.addEventListener('error', () => reject(reader.error || new Error('Datei konnte nicht gelesen werden.')), { once: true });
reader.addEventListener('error', () => reject(reader.error || new Error(ns.I18n.t("file.read.failed"))), { once: true });
reader.readAsDataURL(file);
});
}
+25 -25
View File
@@ -44,7 +44,7 @@ globalThis.Wegwichtel ??= {};
await new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
navigator.serviceWorker.removeEventListener('controllerchange', changed);
reject(new Error('Offline-Speicher konnte nicht aktiviert werden. Bitte erneut versuchen.'));
reject(new Error(ns.I18n.t("offline-cache.das.speichern.fur.unterwegs.hat.nicht.geklappt")));
}, 10000);
function changed() {
clearTimeout(timeout);
@@ -64,32 +64,32 @@ globalThis.Wegwichtel ??= {};
let stage = null;
try {
await worker();
announce('Offline-Dateien werden vorbereitet …');
announce(ns.I18n.t("offline-cache.wegwichtel.wird.fur.unterwegs.vorbereitet"));
// Persistent storage is best-effort; a denial must not block downloads.
await navigator.storage?.persist?.().catch(() => false);
const options = { cache: 'no-store', headers: { 'X-Wegwichtel-Offline-Download': 'true' }, signal };
const manifestResponse = await fetch('/api/offline-manifest', options);
if (!manifestResponse.ok) throw new Error('Die Liste der Offline-Dateien konnte nicht geladen werden.');
if (!manifestResponse.ok) throw new Error(ns.I18n.t("offline-cache.wegwichtel.konnte.nicht.fur.unterwegs.geladen.werden"));
const manifest = await manifestResponse.json();
if (!Array.isArray(manifest.resources) || !manifest.resources.length) throw new TypeError('Ungültige Offline-Dateiliste.');
if (!Array.isArray(manifest.resources) || !manifest.resources.length) throw new TypeError(ns.I18n.t("offline-cache.wegwichtel.konnte.nicht.fur.unterwegs.geladen.werden.2"));
stage = await ns.OfflineStore.create();
const urls = [...new Set(manifest.resources.map(url => ns.OfflineStore.key(url)))];
for (const [index, url] of urls.entries()) {
signal.throwIfAborted();
announce(`Offline-Dateien speichern: ${index + 1} von ${urls.length}`);
announce(ns.I18n.t("offline-cache.fur.unterwegs.speichern.von", { value1: index + 1, value2: urls.length }));
const response = await fetch(url, options);
await ns.OfflineStore.put(stage, url, response);
}
signal.throwIfAborted();
const saved = await ns.OfflineStore.commit(stage);
announce(`${saved.count} Dateien sind offline verfügbar.`);
announce(ns.I18n.t("offline-cache.wegwichtel.ist.fur.unterwegs.bereit"));
return saved;
} catch (error) {
if (stage) await ns.OfflineStore.remove(stage.id);
ns.StorageUsage?.reportError(error);
const saved = await ns.OfflineStore.state();
if (error.name === 'AbortError') announce('Download abgebrochen.');
else announce(`${error.message} ${saved ? 'Der bisherige Offline-Stand bleibt erhalten.' : 'Die App ist noch nicht vollständig offline verfügbar.'}`);
if (error.name === 'AbortError') announce(ns.I18n.t("offline-cache.speichern.abgebrochen"));
else announce(`${ns.UserMessages?.error(error) || ns.I18n.t("offline-cache.das.speichern.hat.nicht.geklappt")} ${saved ? ns.I18n.t("offline.previous.kept") : ns.I18n.t("offline-cache.wegwichtel.ist.noch.nicht.ohne.internet.bereit")}`);
throw error;
} finally {
task = null;
@@ -109,7 +109,7 @@ globalThis.Wegwichtel ??= {};
if (task) await task.catch(() => {});
await ns.OfflineStore.clear();
await updateControls();
announce('App-Dateien wurden entfernt. Gespeicherte Strecken bleiben erhalten.');
announce(ns.I18n.t("offline-cache.die.gespeicherte.app.wurde.entfernt.deine.strecken"));
await ns.StorageUsage?.refresh();
}
@@ -126,12 +126,12 @@ globalThis.Wegwichtel ??= {};
const message = document.getElementById('route-offline-status');
if (save) {
save.disabled = !supported() || Boolean(routeTask);
save.textContent = saved ? 'Offline-Strecke aktualisieren' : 'Strecke offline speichern';
save.textContent = saved ? ns.I18n.t("offline-cache.gespeicherte.strecke.erneuern") : ns.I18n.t("offline-cache.fur.unterwegs.speichern");
}
if (remove) { remove.hidden = !saved; remove.disabled = Boolean(routeTask); }
if (message && !routeTask) message.textContent = saved
? `${saved.count} Dateien dieser Strecke sind auf dem Gerät gespeichert.`
: 'Speichert nur diese Strecke mit GPX, Bildern und Audio für die App.';
? ns.I18n.t("offline-cache.diese.strecke.ist.fur.unterwegs.bereit")
: ns.I18n.t("offline-cache.nimm.diese.strecke.mit.bildern.und.tonaufnahmen");
updateGpxDownload(route);
}
@@ -147,9 +147,9 @@ globalThis.Wegwichtel ??= {};
}
async function saveRoute(route) {
if (routeTask) throw new Error('Eine Strecke wird bereits gespeichert.');
if (!supported()) throw new Error('Offline-Speicherung benötigt HTTPS und Browser-Unterstützung.');
if (!route || route.source === 'local') throw new Error('Bitte eine veröffentlichte Strecke auswählen.');
if (routeTask) throw new Error(ns.I18n.t("offline-cache.bitte.warte.bis.die.andere.strecke.gespeichert"));
if (!supported()) throw new Error(ns.I18n.t("offline-cache.auf.diesem.gerat.kannst.du.strecken.gerade"));
if (!route || route.source === 'local') throw new Error(ns.I18n.t("offline-cache.wahle.zuerst.eine.strecke.die.fur.alle"));
routeTask = (async () => {
let stage = null;
const message = document.getElementById('route-offline-status');
@@ -159,10 +159,10 @@ globalThis.Wegwichtel ??= {};
const url = `/api/routes/${encodeURIComponent(route.id)}`;
const options = { cache: 'no-store', headers: { 'X-Wegwichtel-Offline-Download': 'true' } };
const response = await fetch(url, options);
if (!response.ok) throw new Error('Die Strecke konnte nicht geladen werden.');
if (!response.ok) throw new Error(ns.I18n.t("offline-cache.die.strecke.konnte.nicht.geladen.werden"));
const fresh = await response.clone().json();
if (String(fresh.id) !== String(route.id) || !fresh.activatedAt) throw new Error('Nur veröffentlichte Strecken können offline gespeichert werden.');
if (!fresh.gpxUrl) throw new Error('Für diese Strecke ist keine GPX-Datei vorhanden.');
if (String(fresh.id) !== String(route.id) || !fresh.activatedAt) throw new Error(ns.I18n.t("offline-cache.du.kannst.nur.freigegebene.strecken.fur.unterwegs"));
if (!fresh.gpxUrl) throw new Error(ns.I18n.t("offline-cache.der.weg.dieser.strecke.fehlt.sie.kann"));
const resources = [fresh.gpxUrl];
for (const poi of fresh.pois || []) {
if (poi.audioUrl) resources.push(poi.audioUrl);
@@ -172,7 +172,7 @@ globalThis.Wegwichtel ??= {};
stage = await ns.OfflineStore.create();
await ns.OfflineStore.put(stage, url, response);
for (const [index, resource] of urls.entries()) {
if (message && selectedRoute === route) message.textContent = `Strecke speichern: ${index + 1} von ${urls.length}`;
if (message && selectedRoute === route) message.textContent = ns.I18n.t("offline-cache.strecke.speichern.von", { value1: index + 1, value2: urls.length });
await ns.OfflineStore.put(stage, resource, await fetch(resource, options));
await ns.StorageUsage?.refresh();
}
@@ -193,7 +193,7 @@ globalThis.Wegwichtel ??= {};
}
async function removeRoute(route) {
if (routeTask) throw new Error('Bitte den laufenden Download abwarten.');
if (routeTask) throw new Error(ns.I18n.t("offline-cache.bitte.warte.bis.das.speichern.fertig.ist"));
await ns.OfflineStore.clearRoute(route.id);
await renderRoute(selectedRoute);
await ns.StorageUsage?.refresh();
@@ -206,7 +206,7 @@ globalThis.Wegwichtel ??= {};
return action(route).catch(error => {
if (selectedRoute !== route) return;
const message = document.getElementById('route-offline-status');
if (message) message.textContent = `${error.message} Ein bisheriger Offline-Stand bleibt erhalten.`;
if (message) message.textContent = ns.I18n.t("offline.error.previous.kept", { value1: ns.UserMessages?.error(error) || ns.I18n.t("offline-cache.das.speichern.hat.nicht.geklappt") });
});
};
document.getElementById('route-offline-save')?.addEventListener('click', () => run(saveRoute));
@@ -226,7 +226,7 @@ globalThis.Wegwichtel ??= {};
}
if (!supported()) {
if (toggle) toggle.disabled = true;
announce('Offline-Speicherung ist in diesem Browser oder ohne HTTPS nicht verfügbar.');
announce(ns.I18n.t("offline-cache.auf.diesem.gerat.lasst.sich.wegwichtel.gerade"));
return;
}
// Remove old v1 snapshots that also included all routes automatically.
@@ -236,9 +236,9 @@ globalThis.Wegwichtel ??= {};
if ((await ns.OfflineStore.routes()).length) await worker();
if (saved) {
await worker();
announce(`${saved.count} Dateien sind offline verfügbar.`);
announce(ns.I18n.t("offline-cache.wegwichtel.ist.fur.unterwegs.bereit"));
} else {
announce('Speichert nur die App-Dateien auf diesem Gerät. Strecken kannst du einzeln speichern.');
announce(ns.I18n.t("offline-cache.speichere.zuerst.die.app.danach.kannst.du"));
}
toggle?.addEventListener('change', () => {
toggle.setAttribute('aria-checked', String(toggle.checked));
@@ -249,7 +249,7 @@ globalThis.Wegwichtel ??= {};
ns.OfflineCache = Object.freeze({
initialize() {
initialized ??= setup().catch(error => { announce(error.message); });
initialized ??= setup().catch(error => { announce(ns.UserMessages?.error(error) || ns.I18n.t("offline-cache.das.speichern.fur.unterwegs.hat.nicht.geklappt")); });
return initialized;
},
download,
+2 -2
View File
@@ -10,7 +10,7 @@ globalThis.Wegwichtel ??= {};
function key(value) {
const url = new URL(value, globalThis.location.origin);
if (url.origin !== globalThis.location.origin) throw new TypeError('Nur Ressourcen dieser App sind zulässig.');
if (url.origin !== globalThis.location.origin) throw new TypeError(ns.I18n.t("offline-storage.inhalte.von.anderen.seiten.konnen.hier.nicht"));
url.searchParams.delete('v');
if (url.pathname.startsWith('/api/')) url.searchParams.delete('_');
url.hash = '';
@@ -60,7 +60,7 @@ globalThis.Wegwichtel ??= {};
}
async function put(stage, url, response) {
if (!response.ok || response.type === 'opaque') throw new Error(`Download fehlgeschlagen: ${url}`);
if (!response.ok || response.type === 'opaque') throw new Error(ns.I18n.t("offline-storage.ein.teil.konnte.nicht.gespeichert.werden.prufe"));
const normalized = key(url);
if (stage.mode === 'cache') {
const cache = await caches.open(stage.id);
+2 -2
View File
@@ -41,10 +41,10 @@
const image = images[index];
$('#slide-image')
.attr('src', mediaUrl(image.url))
.attr('alt', image.caption || 'Bild zum Streckenpunkt')
.attr('alt', image.caption || ns.I18n.t("slideshow.bild.zur.station"))
.off('error.wegwichtel')
.on('error.wegwichtel', function () {
$(this).removeAttr('src').attr('alt', 'Bild konnte nicht geladen werden');
$(this).removeAttr('src').attr('alt', ns.I18n.t("slideshow.bild.konnte.nicht.geladen.werden"));
});
$('#slide-caption').text(image.caption || '');
$('#slide-position').text(`${index + 1} / ${images.length}`);
+8 -8
View File
@@ -7,7 +7,7 @@ globalThis.Wegwichtel ??= {};
let refreshTask = null;
function format(bytes) {
return `${(bytes / 1024 / 1024).toLocaleString('de-DE', { maximumFractionDigits: 1 })} MiB`;
return `${(bytes / 1000000).toLocaleString(ns.I18n.language, { maximumFractionDigits: 1 })} MB`;
}
function localBytes() {
@@ -34,15 +34,15 @@ globalThis.Wegwichtel ??= {};
}
function siteDescription(usage, quota, known) {
if (!known) return 'Der Browser meldet keine Speicherkapazität.';
const warning = usage / quota >= .9 ? ' Speicher fast voll.' : '';
return `Website-Speicher: ${format(usage)} von ${format(quota)} belegt (${Math.round(usage / quota * 100)} %). Etwa ${format(Math.max(0, quota - usage))} verfügbar.${warning}`;
if (!known) return ns.I18n.t("storage-usage.der.freie.platz.konnte.nicht.gemessen.werden");
const warning = usage / quota >= .9 ? ns.I18n.t("storage-usage.fast.voll.entferne.inhalte.die.du.nicht") : '';
return ns.I18n.t("storage-usage.app.bilder.und.ton.des.verfugbaren.platzes", { value1: Math.round(usage / quota * 100), value2: format(Math.max(0, quota - usage)), value3: warning });
}
function localDescription(local) {
if (local == null) return 'Local Storage ist nicht verfügbar.';
const warning = local >= LOCAL_REFERENCE * .9 ? ' Richtwert fast erreicht: Große Bilder können das Speichern verhindern.' : '';
return `Local Storage: ungefähr ${format(local)} belegt. Balken bezogen auf einen Richtwert von 5 MiB; die tatsächliche Grenze hängt vom Browser ab.${warning}`;
if (local == null) return ns.I18n.t("storage-usage.texte.und.einstellungen.konnen.hier.gerade.nicht");
const warning = local >= LOCAL_REFERENCE * .9 ? ns.I18n.t("storage-usage.der.balken.ist.fast.voll.grosse.bilder") : '';
return ns.I18n.t("storage-usage.texte.und.einstellungen.ungefahr.belegt.der.balken", { value1: format(local), value2: warning });
}
async function refresh() {
@@ -69,7 +69,7 @@ globalThis.Wegwichtel ??= {};
let current = error;
while (current) {
if (current.name === 'QuotaExceededError') {
lastError = 'Speicher voll: Entferne nicht mehr benötigte Offline-Strecken oder lokale Entwürfe. Eine Browser-Quote oder die separate Local-Storage-Grenze kann erreicht sein.';
lastError = ns.I18n.t("storage-usage.der.platz.ist.voll.entferne.gespeicherte.strecken");
break;
}
current = current.cause;
+419
View File
@@ -0,0 +1,419 @@
globalThis.Wegwichtel ??= {};
(function (ns) {
'use strict';
class WegwichtelTextsDe {
static language = 'de';
static messages = Object.freeze({
"app.du.kannst.dort.weitermachen.wo.du.aufgehort": "Du kannst dort weitermachen, wo du aufgehört hast.",
"app.eigener.entwurf": "Eigener Entwurf",
"app.strecke.wahlen": "Strecke wählen",
"app.strecken.fur.alle.und.meine.entwurfe": "Strecken für alle und meine Entwürfe",
"app.strecke": "Strecke",
"app.alle.stationen.erreicht": "Alle Stationen erreicht",
"app.keine.stationen.vorhanden": "Keine Stationen vorhanden",
"app.nachste.station": "Nächste Station: {value1}{value2}",
"app.veroffentlicht": "Veröffentlicht",
"app.strecken.fur.alle": "Strecken für alle ({value1})",
"app.meine.entwurfe": "Meine Entwürfe ({value1})",
"app.hier.gibt.es.noch.keine.strecken.fur": "Hier gibt es noch keine Strecken für alle.",
"app.du.hast.noch.keine.eigene.strecke.angelegt": "Du hast noch keine eigene Strecke angelegt.",
"app.keine.strecke.im.umkreis.von.km.gefunden": "Keine Strecke im Umkreis von {value1} km gefunden. Wähle unten eine beliebige Strecke aus.",
"app.erlaube.deinen.standort.um.strecken.in.deiner": "Erlaube deinen Standort, um Strecken in deiner Nähe zu finden. Unten kannst du alle Strecken auswählen.",
"app.keine.passende.strecke.gefunden.versuche.ein.anderes": "Keine passende Strecke gefunden. Versuche ein anderes Wort.",
"app.von.strecken.gefunden": "{value1} von {value2} Strecken gefunden.",
"app.strecken.zur.auswahl": "{value1} Strecken zur Auswahl.",
"app.strecke.fortsetzen": "Strecke fortsetzen",
"app.strecke.starten": "Strecke starten",
"app.strecke.beendet": "Strecke beendet.",
"app.du.machst.eine.pause.die.app.sucht": "Du machst eine Pause. Die App sucht gerade nicht nach deinem Standort.",
"app.strecke.pausiert": "Strecke pausiert",
"app.diese.strecke.hat.noch.keine.stationen": "Diese Strecke hat noch keine Stationen.",
"app.norden.oben.nachste.station": "Norden oben · nächste Station: {value1}",
"app.nachste.station.2": "Nächste Station: {value1}",
"app.nachste.station.in.metern": "Nächste Station {value1} in {value2} Metern",
"app.alle.stationen.und.das.ziel.erreicht": "Alle Stationen und das Ziel erreicht",
"app.norden.oben.nachster.punkt.auf.dem.weg": "Norden oben · nächster Punkt auf dem Weg",
"app.nachster.punkt.auf.dem.weg": "Nächster Punkt auf dem Weg",
"app.nachster.punkt.auf.dem.weg.in.metern": "Nächster Punkt auf dem Weg in {value1} Metern",
"app.dein.standort.wird.gesucht.auf.etwa.m": "Dein Standort wird gesucht · auf etwa {value1} m genau",
"app.nachste.station.m": " · nächste Station {value1} m",
"app.alle.stationen.erreicht.2": " · alle Stationen erreicht",
"app.strecke.wird.fortgesetzt": "Strecke wird fortgesetzt.",
"app.strecke.wird.fortgesetzt.2": "Strecke wird fortgesetzt",
"app.dein.standort.konnte.nicht.gefunden.werden.prufe": "Dein Standort konnte nicht gefunden werden. Prüfe die Standort-Erlaubnis auf deinem Gerät.",
"app.norden.oben.navigation": "Norden oben · Navigation",
"app.die.strecken.konnten.nicht.geladen.werden.bitte": "Die Strecken konnten nicht geladen werden. Bitte versuche es noch einmal.",
"app.deine.entwurfe.konnten.nicht.geoffnet.werden.bitte": "Deine Entwürfe konnten nicht geöffnet werden. Bitte versuche es noch einmal.",
"app.dein.standort.wurde.auf.etwa.m.genau": "Dein Standort wurde auf etwa {value1} m genau gefunden. ",
"app.wir.zeigen.dir.strecken.bis.km.entfernung": "Wir zeigen dir Strecken bis {value1} km Entfernung.",
"app.dein.standort.konnte.nicht.gefunden.werden.du": "Dein Standort konnte nicht gefunden werden. Du kannst trotzdem alle Strecken auswählen.",
"app.die.strecke.konnte.nicht.geoffnet.werden.prufe": "Die Strecke konnte nicht geöffnet werden. Prüfe deine Verbindung und versuche es noch einmal.",
"app.die.strecke.konnte.nicht.geoffnet.werden.bitte": "Die Strecke konnte nicht geöffnet werden. Bitte versuche es noch einmal.",
"bootstrap-loader.gespeicherte.inhalte.prufen": "Gespeicherte Inhalte prüfen …",
"bootstrap-loader.wegwichtel.vorbereiten": "Wegwichtel vorbereiten …",
"bootstrap-loader.die.bedienung.vorbereiten": "Die Bedienung vorbereiten …",
"bootstrap-loader.die.app.vorbereiten": "Die App vorbereiten …",
"bootstrap-loader.verbindung.prufen": "Verbindung prüfen …",
"bootstrap-loader.strecken.laden": "Strecken laden …",
"bootstrap-loader.auf.diesem.gerat.kannst.du.bilder.und": "Auf diesem Gerät kannst du Bilder und Tonaufnahmen direkt auswählen.",
"bootstrap-loader.tipp.fur.android.mit.chrome.lassen.sich": "Tipp für Android: Mit Chrome lassen sich Bilder und Tonaufnahmen auswählen, ohne sie noch einmal zu speichern.",
"bootstrap-loader.ein.teil.der.app.konnte.nicht.starten": "Ein Teil der App konnte nicht starten. Bitte versuche es noch einmal.",
"bootstrap-loader.ein.teil.der.app.konnte.nicht.geladen": "Ein Teil der App konnte nicht geladen werden. Prüfe deine Internetverbindung und versuche es noch einmal.",
"bootstrap-loader.alten.fortschritt.loschen": "Alten Fortschritt löschen ({value1} / {value2})",
"bootstrap-loader.wegwichtel.konnte.nicht.starten.prufe.deine.verbindung": "Wegwichtel konnte nicht starten. Prüfe deine Verbindung und versuche es noch einmal.",
"bootstrap-loader.wegwichtel.konnte.nicht.starten.bitte.versuche.es": "Wegwichtel konnte nicht starten. Bitte versuche es noch einmal.",
"editor-ui.zu.dieser.strecke.kann.gerade.keine.weitere": "Zu dieser Strecke kann gerade keine weitere Station hinzugefügt werden.",
"editor-ui.unbenannte.strecke": "Unbenannte Strecke",
"editor-ui.station.ohne.namen": "Station ohne Namen",
"editor-ui.eigene.strecke.bearbeiten": "Eigene Strecke bearbeiten",
"editor-ui.neue.strecke": "Neue Strecke",
"editor-ui.strecke.ansehen": "Strecke ansehen",
"editor-ui.station.bearbeiten": "Station bearbeiten",
"editor-ui.station.hinzufugen": "Station hinzufügen",
"editor-ui.die.aktion.konnte.nicht.ausgefuhrt.werden.bitte": "Die Aktion konnte nicht ausgeführt werden. Bitte versuche es noch einmal.",
"editor-ui.veroffentlichte.strecke": "Veröffentlichte Strecke",
"editor-ui.strecke.bearbeiten": "Strecke bearbeiten",
"editor-ui.strecke.loschen": "Strecke löschen",
"editor-ui.station": "Station {value1}",
"editor-ui.loschen": "{value1} löschen",
"editor-ui.strecken.fur.alle": "Strecken für alle",
"editor-ui.meine.entwurfe": "Meine Entwürfe",
"editor-ui.noch.kein.eigener.entwurf.vorhanden": "Noch kein eigener Entwurf vorhanden.",
"editor-ui.noch.keine.stationen.angelegt": "Noch keine Stationen angelegt.",
"editor-ui.strecken.werden.geladen": "Strecken werden geladen …",
"editor-ui.schon.zur.prufung.gesendet": "Schon zur Prüfung gesendet",
"editor-ui.zur.prufung.senden": "Zur Prüfung senden",
"editor-ui.diese.strecke.wurde.zur.prufung.gesendet.andere": "Diese Strecke wurde zur Prüfung gesendet. Andere können sie erst nach der Freigabe sehen.",
"editor-ui.dieser.entwurf.bleibt.auf.deinem.gerat.mit": "Dieser Entwurf bleibt auf deinem Gerät. Mit „Zur Prüfung senden“ reichst du ihn zur Freigabe ein.",
"editor-ui.diese.strecke.ist.fur.alle.freigegeben.und": "Diese Strecke ist für alle freigegeben und kann hier nicht mehr verändert werden.",
"editor-ui.diese.strecke.wurde.noch.nicht.fur.alle": "Diese Strecke wurde noch nicht für alle freigegeben.",
"editor-ui.knopf.zum.entfernen": "{value1}: Knopf zum Entfernen {value2}",
"editor-ui.neue.tonaufnahme": "Neue Tonaufnahme",
"editor-ui.bild": "Bild {value1}",
"editor-ui.neues.bild": "Neues Bild {value1}",
"editor-ui.bild.2": "{value1} Bild{value2}",
"editor-ui.tippe.auf.ein.bild.oder.eine.tonaufnahme": "{value1}. Tippe auf ein Bild oder eine Tonaufnahme, um sie zu entfernen.",
"editor-ui.noch.keine.bilder.oder.tonaufnahmen.vorhanden": "Noch keine Bilder oder Tonaufnahmen vorhanden.",
"editor-ui.tonaufnahmen": "Tonaufnahmen",
"editor-ui.bilder": "Bilder",
"editor-ui.dein.entwurf.wurde.nicht.gefunden": "Dein Entwurf wurde nicht gefunden.",
"editor-ui.deine.neue.strecke.bleibt.zuerst.auf.diesem": "Deine neue Strecke bleibt zuerst auf diesem Gerät.",
"editor-ui.gespeichert": "Gespeichert.",
"editor-ui.lege.eine.neue.strecke.zuerst.auf.diesem": "Lege eine neue Strecke zuerst auf diesem Gerät an.",
"editor-ui.gib.deiner.strecke.einen.namen": "Gib deiner Strecke einen Namen.",
"editor-ui.die.datei.konnte.nicht.gesendet.werden.bitte": "Die Datei {value1} konnte nicht gesendet werden. Bitte wähle sie noch einmal aus.",
"editor-ui.die.strecke.konnte.nicht.gesendet.werden.bitte": "Die Strecke konnte nicht gesendet werden. Bitte versuche es noch einmal.",
"editor-ui.die.station.konnte.nicht.gesendet.werden.bitte": "Die Station {value1} konnte nicht gesendet werden. Bitte versuche es noch einmal.",
"editor-ui.du.kannst.nur.deine.eigenen.entwurfe.zur": "Du kannst nur deine eigenen Entwürfe zur Prüfung senden.",
"editor-ui.diese.strecke.wurde.schon.zur.prufung.gesendet": "Diese Strecke wurde schon zur Prüfung gesendet.",
"editor-ui.deine.strecke.wird.gesendet": "Deine Strecke wird gesendet …",
"editor-ui.zur.prufung.gesendet": "Zur Prüfung gesendet.",
"editor-ui.fur.eine.neue.station.brauchen.wir.deinen": "Für eine neue Station brauchen wir deinen Standort. Erlaube den Standort und versuche es noch einmal.",
"editor-ui.speichere.zuerst.die.strecke": "Speichere zuerst die Strecke.",
"editor-ui.der.ort.dieser.station.fehlt.erlaube.deinen": "Der Ort dieser Station fehlt. Erlaube deinen Standort und versuche es noch einmal.",
"editor-ui.station.mit.bildern.und.ton.auf.diesem": "Station mit Bildern und Ton auf diesem Gerät gespeichert.",
"editor-ui.station.gespeichert": "Station gespeichert.",
"editor-ui.gib.deiner.station.einen.namen": "Gib deiner Station einen Namen.",
"editor-ui.bild.entfernt": "Bild entfernt.",
"editor-ui.die.station.ist.noch.nicht.gespeichert": "Die Station ist noch nicht gespeichert.",
"editor-ui.das.bild.konnte.nicht.entfernt.werden.lade": "Das Bild konnte nicht entfernt werden. Lade die Strecke noch einmal.",
"editor-ui.station.entfernt.deine.strecke.wurde.gespeichert": "Station entfernt. Deine Strecke wurde gespeichert.",
"editor-ui.station.entfernt": "Station entfernt.",
"editor-ui.reihenfolge.der.stationen.gespeichert": "Reihenfolge der Stationen gespeichert.",
"editor-ui.strecke.geloscht": "Strecke gelöscht.",
"editor-ui.das.hat.leider.nicht.geklappt.bitte.versuche": "Das hat leider nicht geklappt. Bitte versuche es noch einmal.",
"geolocation.dieses.gerat.kann.deinen.standort.nicht.finden": "Dieses Gerät kann deinen Standort nicht finden.",
"group-symbols.holz": "Holz",
"group-symbols.felsen": "Felsen",
"group-symbols.laterne": "Laterne",
"group-symbols.blume": "Blume",
"group-symbols.pfeife": "Pfeife",
"group-symbols.vogel": "Vogel",
"group-symbols.pilz": "Pilz",
"group-symbols.erdbeere": "Erdbeere",
"group-symbols.kurbis": "Kürbis",
"legal-ui.datenschutzerklarung": "Datenschutzerklärung",
"legal-ui.impressum": "Impressum",
"legal-ui.speichern.und.berechtigungen": "Speichern und Berechtigungen",
"legal-ui.hilfe.und.informationen.offnen": "Hilfe und Informationen öffnen",
"legal-ui.informationen": "Informationen",
"legal-ui.rechtliche.und.technische.informationen": "Rechtliche und technische Informationen",
"legal-ui.zuruck.zum.informationsmenu": "Zurück zum Informationsmenü",
"legal-ui.informationsfenster.schliessen": "Informationsfenster schließen",
"legal-ui.schliessen": "Schließen",
"local-routes.deine.strecke.kann.auf.diesem.gerat.gerade": "Deine Strecke kann auf diesem Gerät gerade nicht gespeichert werden.",
"local-routes.bilder.und.ton.konnen.auf.diesem.gerat": "Bilder und Ton können auf diesem Gerät gerade nicht gespeichert werden.",
"local-routes.gespeicherte.bilder.und.ton.konnten.nicht.geoffnet": "Gespeicherte Bilder und Ton konnten nicht geöffnet werden.",
"local-routes.schliesse.andere.fenster.mit.wegwichtel.und.versuche": "Schließe andere Fenster mit Wegwichtel und versuche es noch einmal.",
"local-routes.bilder.und.ton.konnten.nicht.geoffnet.oder": "Bilder und Ton konnten nicht geöffnet oder gespeichert werden.",
"local-routes.das.speichern.von.bildern.und.ton.wurde": "Das Speichern von Bildern und Ton wurde abgebrochen.",
"local-routes.der.platz.fur.bilder.und.ton.ist": "Der Platz für Bilder und Ton ist voll. Entferne Bilder, Tonaufnahmen oder eigene Strecken, die du nicht mehr brauchst.",
"local-routes.der.platz.fur.bilder.und.ton.ist.2": "Der Platz für Bilder und Ton ist voll.",
"local-routes.das.bild.oder.die.tonaufnahme.konnte.nicht": "Das Bild oder die Tonaufnahme konnte nicht gespeichert werden.",
"local-routes.entferne.bilder.tonaufnahmen.oder.eigene.strecken.die": "{value1} Entferne Bilder, Tonaufnahmen oder eigene Strecken, die du nicht mehr brauchst.",
"local-routes.erlaube.noch.einmal.den.zugriff.auf.das": "Erlaube noch einmal den Zugriff auf das ausgewählte Bild oder die Tonaufnahme.",
"local-routes.ein.ausgewahltes.bild.oder.eine.tonaufnahme.wurde": "Ein ausgewähltes Bild oder eine Tonaufnahme wurde verschoben oder gelöscht. Wähle die Datei noch einmal aus.",
"local-routes.auf.diesem.gerat.kann.gerade.keine.neue": "Auf diesem Gerät kann gerade keine neue Strecke angelegt werden.",
"local-routes.diese.datei.enthalt.keinen.lesbaren.weg.wahle": "Diese Datei enthält keinen lesbaren Weg. Wähle eine andere Datei.",
"local-routes.der.ort.von.fehlt": "Der Ort von {value1} fehlt.",
"local-routes.der.platz.fur.deine.strecken.ist.voll": "Der Platz für deine Strecken ist voll.",
"local-routes.deine.strecke.konnte.nicht.gespeichert.werden": "Deine Strecke konnte nicht gespeichert werden.",
"local-routes.entferne.eigene.strecken.die.du.nicht.mehr": "{value1} Entferne eigene Strecken, die du nicht mehr brauchst.",
"local-routes.der.weg.deiner.strecke.fehlt": "Der Weg deiner Strecke fehlt.",
"local-routes.deine.strecke.hat.noch.keinen.aufgezeichneten.weg": "Deine Strecke hat noch keinen aufgezeichneten Weg.",
"local-routes.deine.strecke.wurde.nicht.gefunden": "Deine Strecke wurde nicht gefunden.",
"local-routes.diese.datei.kann.auf.deinem.gerat.nicht": "Diese Datei kann auf deinem Gerät nicht gelesen werden.",
"local-routes.diese.datei.konnte.nicht.geoffnet.werden.wahle": "Diese Datei konnte nicht geöffnet werden. Wähle sie noch einmal aus.",
"offline-cache.das.speichern.fur.unterwegs.hat.nicht.geklappt": "Das Speichern für unterwegs hat nicht geklappt. Bitte versuche es noch einmal.",
"offline-cache.wegwichtel.wird.fur.unterwegs.vorbereitet": "Wegwichtel wird für unterwegs vorbereitet …",
"offline-cache.wegwichtel.konnte.nicht.fur.unterwegs.geladen.werden": "Wegwichtel konnte nicht für unterwegs geladen werden. Prüfe deine Internetverbindung.",
"offline-cache.wegwichtel.konnte.nicht.fur.unterwegs.geladen.werden.2": "Wegwichtel konnte nicht für unterwegs geladen werden. Bitte versuche es noch einmal.",
"offline-cache.fur.unterwegs.speichern.von": "Für unterwegs speichern: {value1} von {value2}",
"offline-cache.wegwichtel.ist.fur.unterwegs.bereit": "Wegwichtel ist für unterwegs bereit.",
"offline-cache.speichern.abgebrochen": "Speichern abgebrochen.",
"offline-cache.das.speichern.hat.nicht.geklappt": "Das Speichern hat nicht geklappt.",
"offline-cache.wegwichtel.ist.noch.nicht.ohne.internet.bereit": "Wegwichtel ist noch nicht ohne Internet bereit.",
"offline-cache.die.gespeicherte.app.wurde.entfernt.deine.strecken": "Die gespeicherte App wurde entfernt. Deine Strecken bleiben erhalten.",
"offline-cache.gespeicherte.strecke.erneuern": "Gespeicherte Strecke erneuern",
"offline-cache.fur.unterwegs.speichern": "Für unterwegs speichern",
"offline-cache.diese.strecke.ist.fur.unterwegs.bereit": "Diese Strecke ist für unterwegs bereit.",
"offline-cache.nimm.diese.strecke.mit.bildern.und.tonaufnahmen": "Nimm diese Strecke mit Bildern und Tonaufnahmen mit.",
"offline-cache.bitte.warte.bis.die.andere.strecke.gespeichert": "Bitte warte, bis die andere Strecke gespeichert ist.",
"offline-cache.auf.diesem.gerat.kannst.du.strecken.gerade": "Auf diesem Gerät kannst du Strecken gerade nicht für unterwegs speichern.",
"offline-cache.wahle.zuerst.eine.strecke.die.fur.alle": "Wähle zuerst eine Strecke, die für alle freigegeben ist.",
"offline-cache.die.strecke.konnte.nicht.geladen.werden": "Die Strecke konnte nicht geladen werden.",
"offline-cache.du.kannst.nur.freigegebene.strecken.fur.unterwegs": "Du kannst nur freigegebene Strecken für unterwegs speichern.",
"offline-cache.der.weg.dieser.strecke.fehlt.sie.kann": "Der Weg dieser Strecke fehlt. Sie kann noch nicht für unterwegs gespeichert werden.",
"offline-cache.strecke.speichern.von": "Strecke speichern: {value1} von {value2}",
"offline-cache.bitte.warte.bis.das.speichern.fertig.ist": "Bitte warte, bis das Speichern fertig ist.",
"offline-cache.auf.diesem.gerat.lasst.sich.wegwichtel.gerade": "Auf diesem Gerät lässt sich Wegwichtel gerade nicht für unterwegs speichern. Du kannst es mit Internet weiter nutzen.",
"offline-cache.speichere.zuerst.die.app.danach.kannst.du": "Speichere zuerst die App. Danach kannst du die Strecken auswählen, die du mitnehmen möchtest.",
"offline-storage.inhalte.von.anderen.seiten.konnen.hier.nicht": "Inhalte von anderen Seiten können hier nicht gespeichert werden.",
"offline-storage.ein.teil.konnte.nicht.gespeichert.werden.prufe": "Ein Teil konnte nicht gespeichert werden. Prüfe deine Internetverbindung und versuche es noch einmal.",
"slideshow.bild.zur.station": "Bild zur Station",
"slideshow.bild.konnte.nicht.geladen.werden": "Bild konnte nicht geladen werden",
"storage-usage.der.freie.platz.konnte.nicht.gemessen.werden": "Der freie Platz konnte nicht gemessen werden.",
"storage-usage.fast.voll.entferne.inhalte.die.du.nicht": " Fast voll: Entferne Inhalte, die du nicht mehr brauchst.",
"storage-usage.app.bilder.und.ton.des.verfugbaren.platzes": "App, Bilder und Ton: {value1} % des verfügbaren Platzes belegt. Ungefähr {value2} frei.{value3}",
"storage-usage.texte.und.einstellungen.konnen.hier.gerade.nicht": "Texte und Einstellungen können hier gerade nicht gespeichert werden.",
"storage-usage.der.balken.ist.fast.voll.grosse.bilder": " Der Balken ist fast voll. Große Bilder können das Speichern verhindern.",
"storage-usage.texte.und.einstellungen.ungefahr.belegt.der.balken": "Texte und Einstellungen: ungefähr {value1} belegt. Der Balken zeigt einen Richtwert von etwa 5 MB. Wie viel wirklich hineinpasst, hängt von deinem Internetprogramm ab.{value2}",
"storage-usage.der.platz.ist.voll.entferne.gespeicherte.strecken": "Der Platz ist voll. Entferne gespeicherte Strecken, Entwürfe, Bilder oder Tonaufnahmen, die du nicht mehr brauchst. Auch bei einem fast leeren ersten Balken kann der Platz für Texte und Einstellungen voll sein.",
"user-messages.diese.strecke.kann.nur.die.person.loschen": "Diese Strecke kann nur die Person löschen, die Wegwichtel betreut.",
"user-messages.diese.strecke.ist.fur.alle.freigegeben.du": "Diese Strecke ist für alle freigegeben. Du kannst sie hier nicht mehr verändern.",
"user-messages.die.datei.enthalt.keinen.lesbaren.weg.wahle": "Die Datei enthält keinen lesbaren Weg. Wähle eine andere Datei.",
"user-messages.diese.station.wurde.nicht.gefunden.offne.die": "Diese Station wurde nicht gefunden. Öffne die Strecke noch einmal.",
"user-messages.der.ort.dieser.station.fehlt.oder.konnte": "Der Ort dieser Station fehlt oder konnte nicht gelesen werden.",
"user-messages.dieses.bild.kann.nicht.geoffnet.werden.wahle": "Dieses Bild kann nicht geöffnet werden. Wähle ein anderes Bild.",
"user-messages.diese.tonaufnahme.kann.nicht.geoffnet.werden.wahle": "Diese Tonaufnahme kann nicht geöffnet werden. Wähle eine andere Aufnahme.",
"user-messages.diese.datei.ist.zu.gross.wahle.eine": "Diese Datei ist zu groß. Wähle eine kleinere Datei.",
"user-messages.du.darfst.diese.aktion.hier.nicht.ausfuhren": "Du darfst diese Aktion hier nicht ausführen.",
"user-messages.der.platz.ist.voll.entferne.gespeicherte.strecken": "Der Platz ist voll. Entferne gespeicherte Strecken, Bilder oder Tonaufnahmen, die du nicht mehr brauchst.",
"user-messages.wegwichtel.ist.gerade.nicht.erreichbar.bitte.versuche": "Wegwichtel ist gerade nicht erreichbar. Bitte versuche es später noch einmal.",
"vibration.das.vibrationsmuster.muss.aus.nichtnegativen.millisekunden.bestehen": "Das Vibrationsmuster muss aus nichtnegativen Millisekunden bestehen.",
"vibration.die.vibrationsdauer.muss.eine.nichtnegative.millisekundenangabe.sein": "Die Vibrationsdauer muss eine nichtnegative Millisekundenangabe sein.",
"html.title.wegwichtel": "Wegwichtel",
"html.span.i": "i",
"html.info-dropdown.hilfe.und.informationen": "Hilfe und Informationen",
"html.p.gleich.kannst.du.mit.dem.wegwichtel.losziehen": "Gleich kannst du mit dem Wegwichtel losziehen.",
"html.bootstrap-status-text.wegwichtel.wird.vorbereitet": "Wegwichtel wird vorbereitet …",
"html.span.los.gehts": "Los geht’s",
"html.bootstrap-retry.erneut.versuchen": "Erneut versuchen",
"html.bootstrap-ready.alles.ist.bereit": "Alles ist bereit.",
"html.div.wegwichtel.funktionen": "Wegwichtel Funktionen",
"html.span.strecken": "Strecken",
"html.span.entdecken": "entdecken",
"html.span.spannende": "Spannende",
"html.span.geschichten": "Geschichten",
"html.span.kindgerechte": "Kindgerechte",
"html.span.erlebnisse": "Erlebnisse",
"html.p.hier.gibt.es.keine.werbung.wir.verfolgen": "Hier gibt es keine Werbung. Wir verfolgen nicht, welche anderen Seiten du besuchst.",
"html.strong.optionale.cookies": "Optionale Cookies",
"html.small.diese.funktion.wird.nicht.verwendet": "Diese Funktion wird nicht verwendet.",
"html.strong.meinen.fortschritt.merken": "Meinen Fortschritt merken",
"html.small.merkt.sich.auf.diesem.gerat.welche.stationen": "Merkt sich auf diesem Gerät, welche Stationen du schon besucht hast.",
"html.strong.app.ohne.internet.offnen": "App ohne Internet öffnen",
"html.small.damit.kannst.du.wegwichtel.auch.ohne.internet": "Damit kannst du Wegwichtel auch ohne Internet öffnen. Wähle danach die Strecken aus, die du mitnehmen möchtest.",
"html.offline-cache-help-button.was.wird.fur.unterwegs.gespeichert": "Was wird für unterwegs gespeichert?",
"html.offline-cache-help-button.hilfe.zum.speichern.fur.unterwegs": "Hilfe zum Speichern für unterwegs",
"html.p.mit.diesem.schalter.kannst.du.wegwichtel.auch": "Mit diesem Schalter kannst du Wegwichtel auch ohne Internet öffnen. Strecken werden dabei noch nicht gespeichert.",
"html.p.offne.eine.strecke.und.tippe.auf.fur": "Öffne eine Strecke und tippe auf „Für unterwegs speichern“. So nimmst du den Weg, die Stationen, Bilder und Tonaufnahmen mit.",
"html.p.warte.vor.dem.losgehen.bis.alles.gespeichert": "Warte vor dem Losgehen, bis alles gespeichert ist. Zum Speichern und Erneuern brauchst du Internet.",
"html.p.alles.bleibt.auf.diesem.gerat.der.schalter": "Alles bleibt auf diesem Gerät. Der Schalter zeichnet deinen Weg nicht auf. Beim Ausschalten bleiben deine gespeicherten Strecken und Entwürfe erhalten.",
"html.p.wenn.du.die.gespeicherten.daten.von.wegwichtel": "Wenn du die gespeicherten Daten von Wegwichtel in deinem Internetprogramm löschst, gehen auch deine Strecken verloren. Bei wenig Platz kann das Gerät gespeicherte Inhalte entfernen. Karten von anderen Anbietern werden nicht mitgenommen.",
"html.offline-cache-refresh.gespeicherte.app.erneuern": "Gespeicherte App erneuern",
"html.section.platz.fur.wegwichtel": "Platz für Wegwichtel",
"html.meter.belegter.platz.fur.app.bilder.und.ton": "Belegter Platz für App, Bilder und Ton",
"html.p.wir.schauen.wie.viel.platz.belegt.ist": "Wir schauen, wie viel Platz belegt ist …",
"html.meter.belegter.platz.fur.texte.und.einstellungen.der": "Belegter Platz für Texte und Einstellungen; der Balken zeigt einen ungefähren Richtwert",
"html.p.der.erste.balken.zeigt.den.platz.fur": "Der erste Balken zeigt den Platz für die App, Bilder und Ton. Der zweite zeigt den Platz für Texte und Einstellungen. Die Balken zeigen nur den Platz für Wegwichtel.",
"html.button.platz.prufen": "Platz prüfen",
"html.li.gespeicherte.inhalte.prufen": "Gespeicherte Inhalte prüfen",
"html.li.wegwichtel.vorbereiten": "Wegwichtel vorbereiten",
"html.li.bedienung.vorbereiten": "Bedienung vorbereiten",
"html.li.app.vorbereiten": "App vorbereiten",
"html.li.verbindung.prufen": "Verbindung prüfen",
"html.li.strecken.laden": "Strecken laden",
"html.li.deinen.standort.suchen": "Deinen Standort suchen",
"html.route-control-button.strecke.auswahlen.oder.wechseln": "Strecke auswählen oder wechseln",
"html.route-control-dropdown.strecken.auswahlen.und.bearbeiten": "Strecken auswählen und bearbeiten",
"html.editor-button.strecken.bearbeiten": "Strecken bearbeiten",
"html.location-hint.wir.suchen.strecken.in.deiner.nahe.dein": "Wir suchen Strecken in deiner Nähe. Dein Standort bleibt auf diesem Gerät.",
"html.h2.strecken.in.deiner.nahe": "Strecken in deiner Nähe",
"html.h2.alle.strecken": "Alle Strecken",
"html.label.strecken.durchsuchen": "Strecken durchsuchen",
"html.route-search.name.schule.oder.beschreibung": "Name, Schule oder Beschreibung",
"html.clear-route-search.suche.leeren": "Suche leeren",
"html.p.diese.strecken.hast.du.auf.diesem.gerat": "Diese Strecken hast du auf diesem Gerät angelegt. Andere können sie noch nicht sehen.",
"html.p.meine.entwurfe.werden.geladen": "Meine Entwürfe werden geladen …",
"html.button.zuruck.zur.streckenauswahl": "Zurück zur Streckenauswahl",
"html.route-offline-controls.strecke.auf.dem.gerat.speichern": "Strecke auf dem Gerät speichern",
"html.route-offline-remove.gespeicherte.strecke.entfernen": "Gespeicherte Strecke entfernen",
"html.route-gpx-download.weg.als.datei.herunterladen": "Weg als Datei herunterladen",
"html.p.fur.unterwegs.speichern.nimmt.diese.strecke.mit": "„Für unterwegs speichern“ nimmt diese Strecke mit Bildern und Ton mit. „Weg als Datei herunterladen“ speichert nur den Weg für eine andere Wander-App.",
"html.dt.lange": "Länge",
"html.dt.hohenmeter": "Höhenmeter",
"html.dt.stationen": "Stationen",
"html.legend.unterwegs": "Unterwegs",
"html.pause-route.strecke.pausieren": "Strecke pausieren",
"html.stop-route.strecke.beenden": "Strecke beenden",
"html.button.zuruck.zur.strecke": "Zurück zur Strecke",
"html.poi-title.station": "Station",
"html.legend.bildnavigation": "Bildnavigation",
"html.slide-prev.vorheriges.bild": "Vorheriges Bild",
"html.slide-next.nachstes.bild": "Nächstes Bild",
"html.poi-audio.ton.zur.station": "Ton zur Station",
"html.poi-audio.dieses.gerat.kann.den.ton.leider.nicht": "Dieses Gerät kann den Ton leider nicht abspielen.",
"html.navigation-mode.richtung.zum.nachsten.station": "Richtung zum nächsten Station",
"html.p.diese.angaben.sind.noch.nicht.vollstandig.die": "Diese Angaben sind noch nicht vollständig. Die Kontakte stammen von der Grundschule Herpf. Wer die Website betreibt, welche Verbindungsdaten gespeichert werden und wie lange sie bleiben, muss noch ergänzt werden.",
"html.strong.hinweis": "Hinweis:",
"html.h3.1.verantwortliche.stelle.und.datenschutzkontakt": "1. Verantwortliche Stelle und Datenschutzkontakt",
"html.p.zum.eichig.3": "Zum Eichig 3",
"html.p.98617.meiningen.ot.herpf": "98617 Meiningen / OT Herpf",
"html.p.telefon.036943.63722": "Telefon: (036943) 63722",
"html.p.e.mail.info.grundschule.herpf.de": "E-Mail: info@grundschule-herpf.de",
"html.strong.staatliche.grundschule.herpf": "Staatliche Grundschule Herpf",
"html.p.datenschutzangaben.der.schule": "Datenschutzangaben der Schule:",
"html.p.datenschutzbeauftragter": "Datenschutzbeauftragter",
"html.p.holderlinstrae.1": "Hölderlinstraße 1",
"html.p.98527.suhl": "98527 Suhl",
"html.p.telefon.03681.7341.25": "Telefon: 03681 / 7341 25",
"html.p.fax.03681.7341.09": "Fax: 03681 / 7341 09",
"html.p.e.mail.datenschutz.suedthueriungen.schulamt.thueringen.de": "E-Mail: datenschutz.suedthueriungen@schulamt.thueringen.de",
"html.strong.staatliches.schulamt.sudthuringen": "Staatliches Schulamt Südthüringen",
"html.p.der.datenschutzbeauftragte.kann.auerdem.uber.die.postadresse": "Der Datenschutzbeauftragte kann außerdem über die Postadresse der Schule mit dem Zusatz „der Datenschutzbeauftragte“ erreicht werden.",
"html.h3.2.wenn.du.wegwichtel.offnest": "2. Wenn du Wegwichtel öffnest",
"html.p.wenn.du.wegwichtel.offnest.konnen.verbindungsdaten.verarbeitet": "Wenn du Wegwichtel öffnest, können Verbindungsdaten verarbeitet werden. Dazu gehören zum Beispiel die Internetadresse deines Geräts, Datum, Uhrzeit und Zeitzone, die aufgerufene Seite, übertragene Daten und ob der Aufruf geklappt hat. Auch die vorherige Seite und Angaben zu deinem Gerät, Internetprogramm und dessen Sprache können dazugehören. Diese Daten helfen dabei, die Website bereitzustellen, Fehler zu finden und sie zu schützen.",
"html.p.wer.die.website.bereitstellt.und.welche.verbindungsdaten": "Wer die Website bereitstellt und welche Verbindungsdaten wie lange gespeichert werden:",
"html.strong.anbieter.anschrift.gespeicherte.daten.und.speicherdauer.erganzen": "[Anbieter, Anschrift, gespeicherte Daten und Speicherdauer ergänzen]",
"html.h3.3.standortdaten": "3. Standortdaten",
"html.p.du.entscheidest.ob.wegwichtel.deinen.standort.nutzen": "Du entscheidest, ob Wegwichtel deinen Standort nutzen darf. Damit findest du Strecken in deiner Nähe und siehst den Weg zur nächsten Station. Dein laufender Standort wird auf deinem Gerät verarbeitet. Wegwichtel speichert daraus keinen Verlauf auf der Website.",
"html.h3.4.deinen.fortschritt.merken": "4. Deinen Fortschritt merken",
"html.p.wenn.du.meinen.fortschritt.merken.einschaltest.speichert": "Wenn du „Meinen Fortschritt merken“ einschaltest, speichert Wegwichtel deine gewählte Strecke, besuchte und angesehene Stationen sowie angehörte Tonaufnahmen. Auch ob du unterwegs bist, eine Pause machst oder fertig bist und der Zeitpunkt werden gespeichert. Diese Angaben bleiben auf deinem Gerät und werden nicht an die Website gesendet.",
"html.p.wenn.du.deinen.fortschritt.mindestens.einen.tag": "Wenn du deinen Fortschritt mindestens einen Tag lang nicht verändert hast, wird er beim nächsten Öffnen gelöscht. Beim Ausschalten wird er sofort gelöscht. Deine eigenen und für unterwegs gespeicherten Strecken bleiben erhalten.",
"html.small.du.entscheidest.ob.sich.wegwichtel.deinen.fortschritt": "Du entscheidest, ob sich Wegwichtel deinen Fortschritt merkt.",
"html.h3.5.deine.eigenen.strecken": "5. Deine eigenen Strecken",
"html.p.neue.strecken.bleiben.zuerst.auf.deinem.gerat": "Neue Strecken bleiben zuerst auf deinem Gerät. Gespeichert werden der Weg, die Orte der Stationen, Texte sowie ausgewählte Bilder und Tonaufnahmen.",
"html.p.deine.entwurfe.bleiben.erhalten.wenn.alter.fortschritt": "Deine Entwürfe bleiben erhalten, wenn alter Fortschritt gelöscht wird oder du „Meinen Fortschritt merken“ ausschaltest. Sie bleiben auf diesem Gerät, bis du sie löschst oder die gespeicherten Daten von Wegwichtel in deinem Internetprogramm entfernst. Nur wenn du „Zur Prüfung senden“ wählst, wird deine Strecke an die Website gesendet.",
"html.h3.6.werbung.und.gespeicherte.einstellungen": "6. Werbung und gespeicherte Einstellungen",
"html.p.wegwichtel.verwendet.keine.cookies.es.zahlt.keine": "Wegwichtel verwendet keine Cookies. Es zählt keine Besuche für Werbung und verfolgt nicht, welche anderen Seiten du besuchst. Auf deinem Gerät wird gespeichert, ob du „Meinen Fortschritt merken“ eingeschaltet hast.",
"html.h3.7.inhalte.von.schulerinnen.und.schulern": "7. Inhalte von Schülerinnen und Schülern",
"html.p.bilder.texte.und.tonaufnahmen.durfen.nur.geteilt": "Bilder, Texte und Tonaufnahmen dürfen nur geteilt werden, wenn das erlaubt ist. Wenn Kinder auf Bildern oder in Aufnahmen erkannt werden können, müssen die Regeln der Schule beachtet werden. Auch die Eltern oder anderen Sorgeberechtigten müssen die nötige Erlaubnis geben.",
"html.h3.8.deine.rechte": "8. Deine Rechte",
"html.p.du.kannst.fragen.welche.daten.uber.dich": "Du kannst fragen, welche Daten über dich gespeichert sind. Unter den gesetzlichen Voraussetzungen kannst du verlangen, dass sie berichtigt, gelöscht, nur noch eingeschränkt genutzt oder dir zur Weitergabe bereitgestellt werden. Du kannst der Nutzung widersprechen und eine erteilte Erlaubnis für die Zukunft zurücknehmen. Wenn du dich über den Umgang mit deinen Daten beschweren möchtest, kannst du dich an die folgende Datenschutzbehörde wenden.",
"html.p.zustandige.datenschutzaufsicht": "Zuständige Datenschutzaufsicht:",
"html.p.postfach.90.04.55.99107.erfurt": "Postfach 90 04 55, 99107 Erfurt",
"html.p.halerstrae.8.99096.erfurt": "Häßlerstraße 8, 99096 Erfurt",
"html.p.telefon.0361.57.311.29.00": "Telefon: 0361 / 57 311 29 00",
"html.p.telefax.0361.57.311.29.04": "Telefax: 0361 / 57 311 29 04",
"html.p.e.mail.poststelle.datenschutz.thueringen.de": "E-Mail: poststelle@datenschutz.thueringen.de",
"html.p.website.www.tlfdi.de": "Website: www.tlfdi.de",
"html.strong.thuringer.landesbeauftragter.fur.den.datenschutz.und.die": "Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)",
"html.p.diese.angaben.stammen.von.der.grundschule.herpf": "Diese Angaben stammen von der Grundschule Herpf. Vor der Veröffentlichung muss geprüft werden, ob für Wegwichtel noch weitere oder andere Angaben nötig sind.",
"html.h3.wer.fur.wegwichtel.verantwortlich.ist": "Wer für Wegwichtel verantwortlich ist",
"html.h3.kontakt": "Kontakt",
"html.p.telefax.036943.63843": "Telefax: (036943) 63843",
"html.h3.leitung.der.schule": "Leitung der Schule",
"html.p.frau.wichert.schulleiterin": "Frau Wichert, Schulleiterin",
"html.h3.schultrager": "Schulträger",
"html.p.landratsamt": "Landratsamt",
"html.p.obertshauser.platz.1": "Obertshäuser Platz 1",
"html.p.98617.meiningen": "98617 Meiningen",
"html.strong.landkreis.schmalkalden.meiningen": "Landkreis Schmalkalden-Meiningen",
"html.h3.inhaltlich.verantwortlich": "Inhaltlich verantwortlich",
"html.p.frau.wichert": "Frau Wichert",
"html.h3.weitere.pflichtangaben": "Weitere Pflichtangaben",
"html.p.je.nachdem.wer.wegwichtel.betreibt.konnen.weitere": "Je nachdem, wer Wegwichtel betreibt, können weitere Angaben nötig sein. Dazu gehören zum Beispiel die Verantwortlichen für den Betrieb der Website. Diese Angaben müssen noch geprüft werden.",
"html.h3.haftung.fur.inhalte.und.verweise": "Haftung für Inhalte und Verweise",
"html.p.die.inhalte.werden.sorgfaltig.erstellt.und.regelmaig": "Die Inhalte werden sorgfältig erstellt und regelmäßig geprüft. Falls etwas falsch, veraltet oder nicht erlaubt ist, melde dich beim Kontakt oben. Für andere verlinkte Seiten sind deren Betreiber verantwortlich.",
"html.h3.urheber.und.nutzungsrechte": "Urheber- und Nutzungsrechte",
"html.p.wenn.nichts.anderes.angegeben.ist.liegen.die": "Wenn nichts anderes angegeben ist, liegen die Rechte bei der Grundschule Herpf. Du darfst Texte, Bilder, Tonaufnahmen und Wege nur so nutzen, wie es jeweils erlaubt wurde. Auch die Rechte der beteiligten Kinder, Familien, Lehrkräfte und anderen Personen bleiben bestehen.",
"html.h3.was.merkt.sich.wegwichtel": "Was merkt sich Wegwichtel?",
"html.p.du.kannst.deinen.fortschritt.auf.diesem.gerat": "Du kannst deinen Fortschritt auf diesem Gerät speichern. Dann weiß Wegwichtel beim nächsten Öffnen, welche Stationen du schon besucht oder angesehen hast und welche Tonaufnahmen du angehört hast.",
"html.p.wenn.du.einen.tag.lang.nichts.mehr": "Wenn du einen Tag lang nichts mehr daran geändert hast, wird dein Fortschritt beim nächsten Öffnen gelöscht. Beim Ausschalten wird er sofort gelöscht. Deine selbst angelegten und für unterwegs gespeicherten Strecken bleiben erhalten.",
"html.small.die.anderung.gilt.sofort.auf.diesem.gerat": "Die Änderung gilt sofort auf diesem Gerät.",
"html.h3.ohne.internet.unterwegs": "Ohne Internet unterwegs",
"html.p.mit.app.ohne.internet.offnen.speicherst.du": "Mit „App ohne Internet öffnen“ speicherst du Wegwichtel auf diesem Gerät. Danach kannst du einzelne Strecken mit „Für unterwegs speichern“ mitnehmen. Warte vor dem Losgehen, bis alles gespeichert ist.",
"html.p.mit.weg.als.datei.herunterladen.kannst.du": "Mit „Weg als Datei herunterladen“ kannst du den Weg in einer anderen Wander-App öffnen. Bilder und Tonaufnahmen sind in dieser Datei nicht enthalten.",
"html.h3.deine.eigenen.strecken": "Deine eigenen Strecken",
"html.p.neue.strecken.bleiben.zuerst.auf.diesem.gerat": "Neue Strecken bleiben zuerst auf diesem Gerät. Mit „Zur Prüfung senden“ reichst du eine Strecke zur Freigabe ein. Erst nach der Freigabe können andere sie sehen.",
"html.p.deine.entwurfe.bleiben.gespeichert.bis.du.sie": "Deine Entwürfe bleiben gespeichert, bis du sie löschst oder die gespeicherten Daten von Wegwichtel in deinem Internetprogramm entfernst. Viele große Bilder brauchen viel Platz. Die Anzeige „Platz für Wegwichtel“ hilft dir, den belegten Platz im Blick zu behalten.",
"html.h3.welche.erlaubnisse.braucht.die.app": "Welche Erlaubnisse braucht die App?",
"html.li.deinen.standort.um.strecken.in.deiner.nahe": "Deinen Standort, um Strecken in deiner Nähe und den Weg zur nächsten Station zu zeigen.",
"html.li.die.richtung.deines.gerats.damit.sich.der": "Die Richtung deines Geräts, damit sich der Pfeil mitdrehen kann.",
"html.li.zugriff.auf.ausgewahlte.bilder.und.tonaufnahmen.wenn": "Zugriff auf ausgewählte Bilder und Tonaufnahmen, wenn du sie zu einer Strecke hinzufügen möchtest.",
"html.p.du.kannst.diese.erlaubnisse.ablehnen.dann.fehlen": "Du kannst diese Erlaubnisse ablehnen. Dann fehlen die jeweiligen Funktionen. Strecken und Stationen kannst du weiterhin selbst auswählen.",
"html.h3.werbung.und.andere.anbieter": "Werbung und andere Anbieter",
"html.p.wegwichtel.zeigt.keine.werbung.und.verfolgt.nicht": "Wegwichtel zeigt keine Werbung und verfolgt nicht, welche anderen Seiten du besuchst. Die App lädt ihre eigenen Programme und Bilder von Wegwichtel. Es werden keine Cookies verwendet.",
"html.editor-close.fenster.zum.bearbeiten.schlieen": "Fenster zum Bearbeiten schließen",
"html.editor-route-list-title.deine.strecken": "Deine Strecken",
"html.editor-new-route.neue.strecke.anlegen": "Neue Strecke anlegen",
"html.p.hier.findest.du.strecken.fur.alle.und": "Hier findest du Strecken für alle und deine eigenen Entwürfe.",
"html.editor-route-back.zuruck": "Zurück",
"html.editor-route-detail-title.deine.strecke": "Deine Strecke",
"html.label.name.der.strecke": "Name der Strecke",
"html.label.beschreibung": "Beschreibung",
"html.label.weg.aus.einer.datei.ubernehmen": "Weg aus einer Datei übernehmen",
"html.editor-route-activated.deine.neue.strecke.bleibt.zuerst.auf.diesem": "Deine neue Strecke bleibt zuerst auf diesem Gerät. Mit „Zur Prüfung senden“ kannst du sie zur Freigabe einreichen.",
"html.editor-route-save.strecke.speichern": "Strecke speichern",
"html.label.name": "Name",
"html.label.text": "Text",
"html.div.bilder.und.ton.zur.station.hinzufugen": "Bilder und Ton zur Station hinzufügen",
"html.editor-poi-audio-add.ton.hinzufugen": "Ton hinzufügen",
"html.editor-poi-images-add.bild.hinzufugen": "Bild hinzufügen",
"html.editor-poi-media-preview.bilder.und.ton.ansehen": "Bilder und Ton ansehen",
"html.editor-poi-save.station.speichern": "Station speichern",
"html.span.station.entfernen": "Station entfernen",
"media.audio.count": {
"one": "{count} Tonaufnahme",
"other": "{count} Tonaufnahmen"
},
"media.image.count": {
"one": "{count} Bild",
"other": "{count} Bilder"
},
"app.page.missing": "Diese Seite konnte nicht geöffnet werden.",
"app.location.searching": "Deinen Standort suchen",
"app.distance.spoken": "{value1} Meter entfernt",
"app.distance.short": "{value1} m entfernt",
"app.path.missing": "Der Weg dieser Strecke fehlt.",
"app.location.start": "Los geht’s! Wir suchen deinen Standort.",
"bootstrap.location": "Deinen Standort suchen …",
"bootstrap.step.failed": "Das hat nicht geklappt: {value1}",
"bootstrap.ready": "Alles ist bereit.",
"editor.station.up": "{value1} nach oben schieben",
"editor.station.down": "{value1} nach unten schieben",
"editor.route.loading": "Deine Strecke wird geöffnet …",
"editor.audio.removed": "Tonaufnahme entfernt.",
"editor.order.saved": "Reihenfolge gespeichert.",
"info.missing": "{value1}: Diese Information fehlt noch.",
"local.device": "Auf diesem Gerät",
"file.read.failed": "Die Datei konnte nicht geöffnet werden. Wähle sie noch einmal aus.",
"offline.previous.kept": "Was du vorher gespeichert hast, bleibt erhalten.",
"offline.error.previous.kept": "{value1} Was du vorher gespeichert hast, bleibt erhalten."
});
}
ns.TextsDe = WegwichtelTextsDe;
}(globalThis.Wegwichtel));
+50
View File
@@ -0,0 +1,50 @@
globalThis.Wegwichtel ??= {};
(function (ns) {
'use strict';
function messageText(error) {
const value = error?.responseJSON?.message || error?.message || error;
if (typeof value !== 'string') return '';
try {
const parsed = JSON.parse(value);
return typeof parsed.message === 'string' ? parsed.message : '';
} catch {
return value;
}
}
const explanations = [
[/Freigegebene Strecken dürfen nur/, "user-messages.diese.strecke.kann.nur.die.person.loschen"],
[/Freigegebene Strecken können/, "user-messages.diese.strecke.ist.fur.alle.freigegeben.du"],
[/GPX|XML|Trackpunkte/, "user-messages.die.datei.enthalt.keinen.lesbaren.weg.wahle"],
[/POI.*nicht gefunden/, "user-messages.diese.station.wurde.nicht.gefunden.offne.die"],
[/POI-Koordinaten/, "user-messages.der.ort.dieser.station.fehlt.oder.konnte"],
[/Bildformat|Bilddatei/, "user-messages.dieses.bild.kann.nicht.geoffnet.werden.wahle"],
[/Audioformat|Audiodatei/, "user-messages.diese.tonaufnahme.kann.nicht.geoffnet.werden.wahle"]
];
const statusExplanations = new Map([
[413, "user-messages.diese.datei.ist.zu.gross.wahle.eine"],
[401, "user-messages.du.darfst.diese.aktion.hier.nicht.ausfuhren"],
[403, "user-messages.du.darfst.diese.aktion.hier.nicht.ausfuhren"]
]);
function readableMessage(message, fallback) {
if (/\b(?:HTTP|API|HTTPS|JSON|IndexedDB|OPFS|CacheStorage|Base64|Blob|Server|sequence|dataUrl|PUT|POST)\b|https?:\/\/|[{}]/i.test(message)) return fallback;
if (/\b(?:Failed|NetworkError|TypeError|AbortError|SecurityError|NotAllowedError|undefined)\b/i.test(message)) return fallback;
if (message && /[äöüÄÖÜß]|\b(?:Die|Der|Das|Dein|Deine|Diese|Dieser|Dieses|Bitte|Wähle|Erlaube|Ein|Eine|Kein|Keine|Speicher|Strecke|Station|Wegwichtel|Gespeichert|Entferne)\b/.test(message)) return message;
return fallback;
}
function errorMessage(error, fallback = ns.I18n.t("editor-ui.das.hat.leider.nicht.geklappt.bitte.versuche")) {
const message = messageText(error);
if (error?.name === 'QuotaExceededError' || error?.cause?.name === 'QuotaExceededError') return ns.I18n.t("user-messages.der.platz.ist.voll.entferne.gespeicherte.strecken");
const explanation = explanations.find(([pattern]) => pattern.test(message));
if (explanation) return ns.I18n.t(explanation[1]);
const statusExplanation = statusExplanations.get(error?.status);
if (statusExplanation) return ns.I18n.t(statusExplanation);
if (error?.status >= 500) return ns.I18n.t("user-messages.wegwichtel.ist.gerade.nicht.erreichbar.bitte.versuche");
return readableMessage(message, fallback);
}
ns.UserMessages = Object.freeze({ error: errorMessage });
}(globalThis.Wegwichtel));
+2 -2
View File
@@ -21,14 +21,14 @@
if (Array.isArray(selected)) {
const values = selected.map(Number);
if (!values.length || values.some(value => !Number.isFinite(value) || value < 0)) {
throw new TypeError('Das Vibrationsmuster muss aus nichtnegativen Millisekunden bestehen.');
throw new TypeError(ns.I18n.t("vibration.das.vibrationsmuster.muss.aus.nichtnegativen.millisekunden.bestehen"));
}
return values;
}
const duration = Number(selected);
if (!Number.isFinite(duration) || duration < 0) {
throw new TypeError('Die Vibrationsdauer muss eine nichtnegative Millisekundenangabe sein.');
throw new TypeError(ns.I18n.t("vibration.die.vibrationsdauer.muss.eine.nichtnegative.millisekundenangabe.sein"));
}
return [duration];
}
+1 -1
View File
@@ -1,6 +1,6 @@
'use strict';
// Serve app files and individually selected route snapshots independently (v2).
importScripts('/js/offline-storage.js');
importScripts('/js/texts-de.js', '/js/i18n.js', '/js/offline-storage.js');
globalThis.addEventListener('install', event => event.waitUntil(globalThis.skipWaiting()));
globalThis.addEventListener('activate', event => event.waitUntil(globalThis.clients.claim()));