fix sonar findings and translation structure
Sonarqube Scanner / Prepare environment (push) Successful in 19s
Sonarqube Scanner / Build and test (push) Successful in 43s
Sonarqube Scanner / SonarQube analysis (push) Failing after 2m28s
Sonarqube Scanner / Deploy to Server (push) Successful in 16s

This commit is contained in:
Florian Zumpe
2026-10-01 16:31:46 +02:00
parent 2b42507f13
commit 5c4d20bd1d
30 changed files with 1753 additions and 894 deletions
+15 -3
View File
@@ -26,9 +26,13 @@ Die App richtet sich an Grundschulkinder und Menschen ohne technische Vorkenntni
## Texte und Übersetzungen ## Texte und Übersetzungen
- Alle eigenen Bedienungstexte liegen zentral in der Klasse `WegwichtelTextsDe` - Alle eigenen Bedienungstexte liegen zentral in der Klasse `WegwichtelTexts`
in `public/js/texts-de.js`. Keine neuen sichtbaren Texte direkt in HTML oder in `public/js/texts-de.js`. Keine neuen sichtbaren Texte direkt in HTML oder
Funktionscode schreiben. Funktionscode schreiben.
- Das HTML enthält daraus erzeugte deutsche Standardtexte, damit Beschriftungen
vor dem Laden der Skripte vorhanden sind. Nach Änderungen am Textkatalog
`node scripts/localize-html.js` ausführen und `public/index.html` mit ablegen.
Diese erzeugten Texte nicht von Hand ändern; die i18n-Attribute bleiben erhalten.
- JavaScript bezieht Texte über `Wegwichtel.I18n.t(textKey, variables)`. - JavaScript bezieht Texte über `Wegwichtel.I18n.t(textKey, variables)`.
HTML verwendet `data-i18n` bzw. `data-i18n-aria-label`, `data-i18n-title`, HTML verwendet `data-i18n` bzw. `data-i18n-aria-label`, `data-i18n-title`,
`data-i18n-placeholder` und `data-i18n-alt`. `data-i18n-placeholder` und `data-i18n-alt`.
@@ -37,6 +41,14 @@ Die App richtet sich an Grundschulkinder und Menschen ohne technische Vorkenntni
- Nutzerinhalte wie Streckennamen, Beschreibungen und Aufnahmen nicht automatisch - Nutzerinhalte wie Streckennamen, Beschreibungen und Aufnahmen nicht automatisch
übersetzen oder verändern. Interne Fehlerdiagnosen dürfen technisch bleiben; übersetzen oder verändern. Interne Fehlerdiagnosen dürfen technisch bleiben;
die sichtbare Meldung muss verständlich sein. die sichtbare Meldung muss verständlich sein.
- Zusätzliche Sprachen über `I18n.register` ergänzen. Fehlende Übersetzungen - Sprachdateien heißen `public/js/texts-<Sprache>.js` und verwenden dieselbe
fallen auf Deutsch zurück. Sprachwechsel aktualisieren feste Beschriftungen; generische Klasse `WegwichtelTexts`. Sie registrieren sich in `TextCatalogs`.
Die App lädt Deutsch und Englisch und wählt anhand von `navigator.languages`
bzw. `navigator.language`. Fehlende Übersetzungen fallen auf den ersten
geladenen Katalog zurück (in der App Deutsch).
- Verwende kurze, stabile englische Funktionsschlüssel wie `app.no-route` oder
`bootstrap-loader.error.start-error`. Schlüssel nicht aus dem deutschen Text
ableiten. Startmeldungen in `bootstrap-loader.steps` nach Schritt gruppieren.
- Deutsch und Englisch enthalten dieselben Schlüssel, Platzhalter und Pluralformen.
Texte in beiden Sprachen einfach und sinngemäß schreiben. Sprachwechsel aktualisieren feste Beschriftungen;
bereits erzeugte dynamische Ansichten bei Bedarf neu rendern. bereits erzeugte dynamische Ansichten bei Bedarf neu rendern.
+49
View File
@@ -0,0 +1,49 @@
# Texte und Sprachen
Die Sprachdateien `public/js/texts-de.js` und `public/js/texts-en.js` enthalten
jeweils die generische Klasse `WegwichtelTexts`. Beide Dateien verwenden dieselben
Schlüssel und registrieren ihren Katalog mit der eigenen Sprachkennung in
`Wegwichtel.TextCatalogs`. Es gibt keine sprachabhängigen Klassennamen.
Die App lädt beide Kataloge vor `i18n.js`. Beim Start wird die erste unterstützte
Sprache aus `navigator.languages` gewählt; ohne diese Liste wird
`navigator.language` verwendet. Regionale Varianten wie `en-GB` oder `de-DE`
verwenden den passenden Grundkatalog. Passt keine Sprache, wird der erste
geladene Katalog verwendet: in der App Deutsch. Einzelne fehlende Übersetzungen
fallen ebenfalls auf diesen Katalog zurück. Wird nur eine Sprachdatei eingebunden,
funktioniert sie allein und bildet den Fallback.
## Verwendung
Schlüssel beschreiben die Funktion, nicht den Wortlaut. Zusammengehörige Texte
liegen in verschachtelten Abschnitten; der Aufruf verwendet Punkte als Trennung.
```js
Wegwichtel.I18n.t('app.no-route');
Wegwichtel.I18n.t('bootstrap-loader.error.start-error');
Wegwichtel.I18n.t(`bootstrap-loader.steps.${step}`);
Wegwichtel.I18n.t('media.image.count', { count: 2 });
```
`bootstrap-loader.steps` ist nach den stabilen Schrittkennungen aufgebaut:
`storage`, `jquery`, `jquery-ui`, `modules`, `server`, `routes` und `location`.
Die Zuordnung benötigt keine zusätzliche Liste von Nachrichtenschlüsseln.
HTML verwendet `data-i18n`, `data-i18n-title`, `data-i18n-aria-label`,
`data-i18n-placeholder` und `data-i18n-alt`. Deutsche Standardtexte werden mit
`node scripts/localize-html.js` aus dem Katalog ins HTML übernommen. Dadurch
stehen Beschriftungen bereits vor dem Laden der Skripte bereit. Nach Änderungen
die erzeugte `public/index.html` mit ins Repository übernehmen.
Zum gezielten Prüfen kann `Wegwichtel.I18n.setLanguage('en')` aufgerufen werden.
Das aktualisiert feste Beschriftungen. Bereits aufgebaute dynamische Ansichten
müssen neu aufgebaut werden; die automatische Startauswahl erfolgt vor ihrem
ersten Aufbau. Ein sichtbarer Sprachschalter ist nicht enthalten.
Die Tests prüfen beide Kataloge, Platzhalter und Pluralformen, Browserpräferenzen,
den Betrieb mit nur einer Sprachdatei, deutsche HTML-Standardtexte und englische
App-Meldungen. Nutzerinhalte wie Streckennamen werden nicht übersetzt.
SonarQube prüft auch die Sprachdateien auf Probleme und Testabdeckung. Nur die
Duplikaterkennung nimmt `public/js/texts-*.js` aus: Übersetzungskataloge müssen
dieselbe Struktur haben und sind keine mehrfach implementierte Programmlogik.
+202 -201
View File
@@ -4,7 +4,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<meta name="theme-color" content="#315d3a"> <meta name="theme-color" content="#315d3a">
<title data-i18n="html.title.wegwichtel"></title> <title data-i18n="common.app-name">Wegwichtel</title>
<link rel="stylesheet" href="css/bootstrap.css?v=0.12.14"> <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="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"> <link rel="stylesheet" href="css/app.css?v=0.12.14">
@@ -12,13 +12,13 @@
</head> </head>
<body> <body>
<div class="info-control"> <div class="info-control">
<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"> <button id="info-button" class="info-button" type="button" aria-label="Hilfe und Informationen öffnen" title="Informationen" aria-haspopup="true" aria-expanded="false" aria-controls="info-dropdown" data-i18n-title="info.title" data-i18n-aria-label="info.open">
<span aria-hidden="true" data-i18n="html.span.i"></span> <span aria-hidden="true" data-i18n="info.icon">i</span>
</button> </button>
<nav id="info-dropdown" class="info-dropdown" aria-label="" hidden data-i18n-aria-label="html.info-dropdown.hilfe.und.informationen"> <nav id="info-dropdown" class="info-dropdown" aria-label="Hilfe und Informationen" hidden data-i18n-aria-label="info.dropdown-label">
<button type="button" data-info-target="privacy" data-i18n="legal-ui.datenschutzerklarung"></button> <button type="button" data-info-target="privacy" data-i18n="info.privacy">Datenschutzerklärung</button>
<button type="button" data-info-target="imprint" data-i18n="legal-ui.impressum"></button> <button type="button" data-info-target="imprint" data-i18n="info.imprint">Impressum</button>
<button type="button" data-info-target="technology" data-i18n="legal-ui.speichern.und.berechtigungen"></button> <button type="button" data-info-target="technology" data-i18n="info.storage">Speichern und Berechtigungen</button>
</nav> </nav>
</div> </div>
<div id="bootstrap-screen" aria-live="polite" aria-busy="true"> <div id="bootstrap-screen" aria-live="polite" aria-busy="true">
@@ -33,8 +33,8 @@
<circle cx="80" cy="16" r="8" fill="#fff" stroke="#f4b400" stroke-width="5"></circle> <circle cx="80" cy="16" r="8" fill="#fff" stroke="#f4b400" stroke-width="5"></circle>
</svg> </svg>
<div> <div>
<h1 data-i18n="html.title.wegwichtel"></h1> <h1 data-i18n="common.app-name">Wegwichtel</h1>
<p data-i18n="html.p.gleich.kannst.du.mit.dem.wegwichtel.losziehen"></p> <p data-i18n="start.intro">Gleich kannst du mit dem Wegwichtel losziehen.</p>
</div> </div>
</header> </header>
@@ -64,201 +64,201 @@
<div class="bootstrap-action-slot"> <div class="bootstrap-action-slot">
<output id="bootstrap-action-status" class="bootstrap-action-status" aria-live="polite"> <output id="bootstrap-action-status" class="bootstrap-action-status" aria-live="polite">
<span class="bootstrap-loading-dot" aria-hidden="true"></span> <span class="bootstrap-loading-dot" aria-hidden="true"></span>
<span id="bootstrap-status-text" class="bootstrap-status-text" data-i18n="html.bootstrap-status-text.wegwichtel.wird.vorbereitet"></span> <span id="bootstrap-status-text" class="bootstrap-status-text" data-i18n="start.preparing">Wegwichtel wird vorbereitet …</span>
</output> </output>
<button id="bootstrap-enter" class="bootstrap-enter" type="button" hidden> <button id="bootstrap-enter" class="bootstrap-enter" type="button" hidden>
<span data-i18n="html.span.los.gehts"></span> <span data-i18n="common.start">Los geht’s</span>
<svg viewbox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 5 7 7-7 7"></path></svg> <svg viewbox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m9 5 7 7-7 7"></path></svg>
</button> </button>
</div> </div>
<p id="bootstrap-error" class="bootstrap-error" hidden></p> <p id="bootstrap-error" class="bootstrap-error" hidden></p>
<button id="bootstrap-retry" class="bootstrap-retry" type="button" hidden data-i18n="html.bootstrap-retry.erneut.versuchen"></button> <button id="bootstrap-retry" class="bootstrap-retry" type="button" hidden data-i18n="common.retry">Erneut versuchen</button>
<p id="bootstrap-ready" class="bootstrap-ready" hidden data-i18n="html.bootstrap-ready.alles.ist.bereit"></p> <p id="bootstrap-ready" class="bootstrap-ready" hidden data-i18n="bootstrap-loader.ready">Alles ist bereit.</p>
<div class="bootstrap-features" aria-label="" data-i18n-aria-label="html.div.wegwichtel.funktionen"> <div class="bootstrap-features" aria-label="Wegwichtel Funktionen" data-i18n-aria-label="start.features-label">
<div> <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"></path></svg> <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> <span><span data-i18n="start.features.routes">Strecken</span><br><span data-i18n="start.features.explore">entdecken</span></span>
</div> </div>
<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"></path></svg> <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> <span><span data-i18n="start.features.exciting">Spannende</span><br><span data-i18n="start.features.stories">Geschichten</span></span>
</div> </div>
<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"></path></svg> <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> <span><span data-i18n="start.features.child-friendly">Kindgerechte</span><br><span data-i18n="start.features.experiences">Erlebnisse</span></span>
</div> </div>
</div> </div>
<p id="android-browser-hint" class="android-browser-hint" hidden></p> <p id="android-browser-hint" class="android-browser-hint" hidden></p>
<div class="privacy-summary"> <div class="privacy-summary">
<p data-i18n="html.p.hier.gibt.es.keine.werbung.wir.verfolgen"></p> <p data-i18n="privacy.no-tracking">Hier gibt es keine Werbung. Wir verfolgen nicht, welche anderen Seiten du besuchst.</p>
<label class="switch-row switch-disabled"> <label for="cookie-toggle-1" class="switch-row switch-disabled">
<span> <span>
<strong data-i18n="html.strong.optionale.cookies"></strong> <strong data-i18n="progress.cookies-label">Optionale Cookies</strong>
<small data-i18n="html.small.diese.funktion.wird.nicht.verwendet"></small> <small data-i18n="progress.cookies-unused">Diese Funktion wird nicht verwendet.</small>
</span> </span>
<input class="cookie-toggle" type="checkbox" role="switch" aria-checked="false" disabled> <input id="cookie-toggle-1" class="cookie-toggle" type="checkbox" role="switch" aria-checked="false" disabled>
<span class="switch-track" aria-hidden="true"></span> <span class="switch-track" aria-hidden="true"></span>
</label> </label>
<label class="switch-row"> <label for="progress-storage-toggle-1" class="switch-row">
<span> <span>
<strong data-i18n="html.strong.meinen.fortschritt.merken"></strong> <strong data-i18n="progress.remember">Meinen Fortschritt merken</strong>
<small data-i18n="html.small.merkt.sich.auf.diesem.gerat.welche.stationen"></small> <small data-i18n="progress.remember-hint">Merkt sich auf diesem Gerät, welche Stationen du schon besucht hast.</small>
</span> </span>
<input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false"> <input id="progress-storage-toggle-1" class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span> <span class="switch-track" aria-hidden="true"></span>
</label> </label>
<div class="offline-option-row"> <div class="offline-option-row">
<label class="switch-row"> <label for="offline-cache-toggle" class="switch-row">
<span> <span>
<strong data-i18n="html.strong.app.ohne.internet.offnen"></strong> <strong data-i18n="offline.open-app">App ohne Internet öffnen</strong>
<small data-i18n="html.small.damit.kannst.du.wegwichtel.auch.ohne.internet"></small> <small data-i18n="offline.open-app-hint">Damit kannst du Wegwichtel auch ohne Internet öffnen. Wähle danach die Strecken aus, die du mitnehmen möchtest.</small>
</span> </span>
<input id="offline-cache-toggle" type="checkbox" role="switch" aria-checked="false" aria-describedby="offline-cache-status"> <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> <span class="switch-track" aria-hidden="true"></span>
</label> </label>
<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> <button id="offline-cache-help-button" class="offline-help-button" type="button" aria-label="Hilfe zum Speichern für unterwegs" title="Was wird für unterwegs gespeichert?" aria-expanded="false" aria-controls="offline-cache-help" data-i18n-title="offline.help-title" data-i18n-aria-label="offline.help-label">?</button>
</div> </div>
<section id="offline-cache-help" class="offline-help" aria-labelledby="offline-cache-help-title" hidden> <section id="offline-cache-help" class="offline-help" aria-labelledby="offline-cache-help-title" hidden>
<h2 id="offline-cache-help-title" data-i18n="html.offline-cache-help-button.was.wird.fur.unterwegs.gespeichert"></h2> <h2 id="offline-cache-help-title" data-i18n="offline.help-title">Was wird für unterwegs gespeichert?</h2>
<p data-i18n="html.p.mit.diesem.schalter.kannst.du.wegwichtel.auch"></p> <p data-i18n="offline.help.app-only">Mit diesem Schalter kannst du Wegwichtel auch ohne Internet öffnen. Strecken werden dabei noch nicht gespeichert.</p>
<p data-i18n="html.p.offne.eine.strecke.und.tippe.auf.fur"></p> <p data-i18n="offline.help.save-route">Öffne eine Strecke und tippe auf „Für unterwegs speichern“. So nimmst du den Weg, die Stationen, Bilder und Tonaufnahmen mit.</p>
<p data-i18n="html.p.warte.vor.dem.losgehen.bis.alles.gespeichert"></p> <p data-i18n="offline.help.connection">Warte vor dem Losgehen, bis alles gespeichert ist. Zum Speichern und Erneuern brauchst du Internet.</p>
<p data-i18n="html.p.alles.bleibt.auf.diesem.gerat.der.schalter"></p> <p data-i18n="offline.help.keep-routes">Alles bleibt auf diesem Gerät. Der Schalter zeichnet deinen Weg nicht auf. Beim Ausschalten bleiben deine gespeicherten Strecken und Entwürfe erhalten.</p>
<p data-i18n="html.p.wenn.du.die.gespeicherten.daten.von.wegwichtel"></p> <p data-i18n="offline.help.storage-limits">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.</p>
</section> </section>
<output id="offline-cache-status" aria-live="polite"></output> <output id="offline-cache-status" aria-live="polite"></output>
<button id="offline-cache-refresh" type="button" hidden data-i18n="html.offline-cache-refresh.gespeicherte.app.erneuern"></button> <button id="offline-cache-refresh" type="button" hidden data-i18n="offline.renew-app">Gespeicherte App erneuern</button>
</div> </div>
<section class="storage-usage" data-storage-usage aria-label="" data-i18n-aria-label="html.section.platz.fur.wegwichtel"> <section class="storage-usage" data-storage-usage aria-label="Platz für Wegwichtel" data-i18n-aria-label="storage.title">
<h3 data-i18n="html.section.platz.fur.wegwichtel"></h3> <h3 data-i18n="storage.title">Platz für 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> <meter data-storage-site-meter min="0" max="1" value="0" aria-label="Belegter Platz für App, Bilder und Ton" hidden data-i18n-aria-label="storage.site-meter-label"></meter>
<p data-storage-site-text data-i18n="html.p.wir.schauen.wie.viel.platz.belegt.ist"></p> <p data-storage-site-text data-i18n="storage.loading">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> <meter data-storage-local-meter min="0" max="5242880" value="0" aria-label="Belegter Platz für Texte und Einstellungen; der Balken zeigt einen ungefähren Richtwert" data-i18n-aria-label="storage.local-meter-label"></meter>
<p data-storage-local-text></p> <p data-storage-local-text></p>
<p data-i18n="html.p.der.erste.balken.zeigt.den.platz.fur"></p> <p data-i18n="storage.help">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.</p>
<output data-storage-warning hidden></output> <output data-storage-warning hidden></output>
<button data-storage-refresh type="button" data-i18n="html.button.platz.prufen"></button> <button data-storage-refresh type="button" data-i18n="storage.refresh">Platz prüfen</button>
</section> </section>
<p id="storage-cleanup-status" class="storage-cleanup-status" hidden></p> <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> <progress id="bootstrap-progress" class="bootstrap-state-model" max="7" value="0" hidden></progress>
<ol class="bootstrap-state-model" hidden> <ol class="bootstrap-state-model" hidden>
<li data-step="storage" data-i18n="html.li.gespeicherte.inhalte.prufen"></li> <li data-step="storage" data-i18n="bootstrap-loader.steps.storage">Gespeicherte Inhalte prüfen …</li>
<li data-step="jquery" data-i18n="html.li.wegwichtel.vorbereiten"></li> <li data-step="jquery" data-i18n="bootstrap-loader.steps.jquery">Wegwichtel vorbereiten …</li>
<li data-step="jquery-ui" data-i18n="html.li.bedienung.vorbereiten"></li> <li data-step="jquery-ui" data-i18n="bootstrap-loader.steps.jquery-ui">Die Bedienung vorbereiten …</li>
<li data-step="modules" data-i18n="html.li.app.vorbereiten"></li> <li data-step="modules" data-i18n="bootstrap-loader.steps.modules">Die App vorbereiten …</li>
<li data-step="server" data-i18n="html.li.verbindung.prufen"></li> <li data-step="server" data-i18n="bootstrap-loader.steps.server">Verbindung prüfen …</li>
<li data-step="routes" data-i18n="html.li.strecken.laden"></li> <li data-step="routes" data-i18n="bootstrap-loader.steps.routes">Strecken laden …</li>
<li data-step="location" data-i18n="html.li.deinen.standort.suchen"></li> <li data-step="location" data-i18n="bootstrap-loader.steps.location">Deinen Standort suchen …</li>
</ol> </ol>
</section> </section>
</div> </div>
<main id="app-shell" hidden> <main id="app-shell" hidden>
<div class="route-control"> <div class="route-control">
<div class="route-control-summary" aria-live="polite"> <div class="route-control-summary" aria-live="polite">
<strong id="route-control-title" data-i18n="app.strecke.wahlen"></strong> <strong id="route-control-title" data-i18n="app.choose-route">Strecke wählen</strong>
<small id="route-control-next" data-i18n="app.strecken.fur.alle.und.meine.entwurfe"></small> <small id="route-control-next" data-i18n="app.route-menu">Strecken für alle und meine Entwürfe</small>
</div> </div>
<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"> <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" data-i18n-title="routes.open-menu" data-i18n-aria-label="routes.open-menu">
<img src="images/icons/route-management.svg" alt="" aria-hidden="true"> <img src="images/icons/route-management.svg" alt="" aria-hidden="true">
</button> </button>
<nav id="route-control-dropdown" class="route-control-dropdown" aria-label="" hidden data-i18n-aria-label="html.route-control-dropdown.strecken.auswahlen.und.bearbeiten"> <nav id="route-control-dropdown" class="route-control-dropdown" aria-label="Strecken auswählen und bearbeiten" hidden data-i18n-aria-label="routes.menu-label">
<section aria-labelledby="route-control-server-title"> <section aria-labelledby="route-control-server-title">
<h2 id="route-control-server-title" data-i18n="editor-ui.strecken.fur.alle"></h2> <h2 id="route-control-server-title" data-i18n="editor.public-routes">Strecken für alle</h2>
<ul id="route-control-server-list" class="route-control-list" aria-live="polite"></ul> <ul id="route-control-server-list" class="route-control-list" aria-live="polite"></ul>
</section> </section>
<section aria-labelledby="route-control-local-title"> <section aria-labelledby="route-control-local-title">
<h2 id="route-control-local-title" data-i18n="editor-ui.meine.entwurfe"></h2> <h2 id="route-control-local-title" data-i18n="editor.drafts">Meine Entwürfe</h2>
<ul id="route-control-local-list" class="route-control-list" aria-live="polite"></ul> <ul id="route-control-local-list" class="route-control-list" aria-live="polite"></ul>
</section> </section>
<div class="route-control-actions"> <div class="route-control-actions">
<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="editor-button" class="editor-button" type="button" aria-label="Strecken bearbeiten" title="Strecken bearbeiten" aria-haspopup="dialog" aria-expanded="false" aria-controls="editor-dialog" data-i18n-title="editor.open" data-i18n-aria-label="editor.open" data-i18n="editor.open">Strecken bearbeiten</button>
<button id="route-control-new" class="editor-primary-button" type="button" data-i18n="editor-ui.neue.strecke"></button> <button id="route-control-new" class="editor-primary-button" type="button" data-i18n="editor.new-route">Neue Strecke</button>
</div> </div>
</nav> </nav>
</div> </div>
<section id="routes-page" class="app-page"> <section id="routes-page" class="app-page">
<header> <header>
<h1 data-i18n="html.title.wegwichtel"></h1> <h1 data-i18n="common.app-name">Wegwichtel</h1>
</header> </header>
<div class="content"> <div class="content">
<p id="location-hint" class="notice" data-i18n="html.location-hint.wir.suchen.strecken.in.deiner.nahe.dein"></p> <p id="location-hint" class="notice" data-i18n="routes.location-hint">Wir suchen Strecken in deiner Nähe. Dein Standort bleibt auf diesem Gerät.</p>
<section> <section>
<h2 data-i18n="html.h2.strecken.in.deiner.nahe"></h2> <h2 data-i18n="routes.nearby">Strecken in deiner Nähe</h2>
<ul id="nearby-route-list" class="route-list" aria-live="polite"> <ul id="nearby-route-list" class="route-list" aria-live="polite">
<li><p data-i18n="editor-ui.strecken.werden.geladen"></p></li> <li><p data-i18n="editor.loading-routes">Strecken werden geladen …</p></li>
</ul> </ul>
</section> </section>
<section> <section>
<h2 data-i18n="html.h2.alle.strecken"></h2> <h2 data-i18n="routes.all">Alle Strecken</h2>
<label for="route-search" data-i18n="html.label.strecken.durchsuchen"></label> <label for="route-search" data-i18n="routes.search-label">Strecken durchsuchen</label>
<div class="search-row"> <div class="search-row">
<input id="route-search" type="search" autocomplete="off" inputmode="search" placeholder="" data-i18n-placeholder="html.route-search.name.schule.oder.beschreibung"> <input id="route-search" type="search" autocomplete="off" inputmode="search" placeholder="Name, Schule oder Beschreibung" data-i18n-placeholder="routes.search-placeholder">
<button id="clear-route-search" type="button" data-i18n="html.clear-route-search.suche.leeren"></button> <button id="clear-route-search" type="button" data-i18n="routes.clear-search">Suche leeren</button>
</div> </div>
<p id="all-routes-summary" aria-live="polite" data-i18n="editor-ui.strecken.werden.geladen"></p> <p id="all-routes-summary" aria-live="polite" data-i18n="editor.loading-routes">Strecken werden geladen …</p>
<ul id="all-route-list" class="route-list" aria-live="polite"> <ul id="all-route-list" class="route-list" aria-live="polite">
<li><p data-i18n="editor-ui.strecken.werden.geladen"></p></li> <li><p data-i18n="editor.loading-routes">Strecken werden geladen …</p></li>
</ul> </ul>
</section> </section>
<section aria-labelledby="local-route-title"> <section aria-labelledby="local-route-title">
<h2 id="local-route-title" data-i18n="editor-ui.meine.entwurfe"></h2> <h2 id="local-route-title" data-i18n="editor.drafts">Meine Entwürfe</h2>
<p data-i18n="html.p.diese.strecken.hast.du.auf.diesem.gerat"></p> <p data-i18n="routes.drafts-hint">Diese Strecken hast du auf diesem Gerät angelegt. Andere können sie noch nicht sehen.</p>
<ul id="local-route-list" class="route-list" aria-live="polite"> <ul id="local-route-list" class="route-list" aria-live="polite">
<li><p data-i18n="html.p.meine.entwurfe.werden.geladen"></p></li> <li><p data-i18n="routes.loading-drafts">Meine Entwürfe werden geladen …</p></li>
</ul> </ul>
</section> </section>
</div> </div>
</section> </section>
<section id="route-page" class="app-page" hidden> <section id="route-page" class="app-page" hidden>
<header class="has-back-button"> <header class="has-back-button">
<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"> <button class="back-button" type="button" data-page-target="#routes-page" aria-label="Zurück zur Streckenauswahl" title="Zurück zur Streckenauswahl" data-i18n-title="routes.back" data-i18n-aria-label="routes.back">
<img src="images/icons/back.svg" alt=""> <img src="images/icons/back.svg" alt="">
</button> </button>
<h1 id="route-title" data-i18n="app.strecke"></h1> <h1 id="route-title" data-i18n="app.route">Strecke</h1>
</header> </header>
<div class="content"> <div class="content">
<p id="route-description"></p> <p id="route-description"></p>
<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"> <section id="route-offline-controls" class="route-offline-controls" aria-label="Strecke auf dem Gerät speichern" hidden data-i18n-aria-label="offline.route-controls-label">
<button id="route-offline-save" type="button" data-i18n="offline-cache.fur.unterwegs.speichern"></button> <button id="route-offline-save" type="button" data-i18n="offline.save-route">Für unterwegs speichern</button>
<button id="route-offline-remove" type="button" hidden data-i18n="html.route-offline-remove.gespeicherte.strecke.entfernen"></button> <button id="route-offline-remove" type="button" hidden data-i18n="offline.remove-route">Gespeicherte Strecke entfernen</button>
<a id="route-gpx-download" download hidden data-i18n="html.route-gpx-download.weg.als.datei.herunterladen"></a> <a id="route-gpx-download" download hidden data-i18n="offline.export-path">Weg als Datei herunterladen</a>
<output id="route-offline-status" aria-live="polite"></output> <output id="route-offline-status" aria-live="polite"></output>
<p data-i18n="html.p.fur.unterwegs.speichern.nimmt.diese.strecke.mit"></p> <p data-i18n="offline.route-actions-help">„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.</p>
</section> </section>
<dl> <dl>
<div><dt data-i18n="html.dt.lange"></dt><dd id="route-distance">–</dd></div> <div><dt data-i18n="route.length">Länge</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="route.elevation">Höhenmeter</dt><dd id="route-elevation">–</dd></div>
<div><dt data-i18n="html.dt.stationen"></dt><dd id="route-poi-count">–</dd></div> <div><dt data-i18n="route.station-count">Stationen</dt><dd id="route-poi-count">–</dd></div>
</dl> </dl>
<fieldset> <fieldset>
<legend data-i18n="html.legend.unterwegs"></legend> <legend data-i18n="route.controls-label">Unterwegs</legend>
<button id="start-route" type="button" aria-label="" title="" data-i18n-title="app.strecke.starten" data-i18n-aria-label="app.strecke.starten"> <button id="start-route" type="button" aria-label="Strecke starten" title="Strecke starten" data-i18n-title="app.start-route" data-i18n-aria-label="app.start-route">
<img src="images/icons/play.svg" alt="" data-i18n-alt="app.strecke.starten"> <img src="images/icons/play.svg" alt="Strecke starten" data-i18n-alt="app.start-route">
</button> </button>
<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"> <button id="pause-route" type="button" aria-label="Strecke pausieren" title="Strecke pausieren" disabled data-i18n-title="route.pause" data-i18n-aria-label="route.pause">
<img src="images/icons/pause.svg" alt="" data-i18n-alt="html.pause-route.strecke.pausieren"> <img src="images/icons/pause.svg" alt="Strecke pausieren" data-i18n-alt="route.pause">
</button> </button>
<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"> <button id="stop-route" type="button" aria-label="Strecke beenden" title="Strecke beenden" disabled data-i18n-title="route.stop" data-i18n-aria-label="route.stop">
<img src="images/icons/stop.svg" alt="" data-i18n-alt="html.stop-route.strecke.beenden"> <img src="images/icons/stop.svg" alt="Strecke beenden" data-i18n-alt="route.stop">
</button> </button>
</fieldset> </fieldset>
<p id="tracking-status" class="notice" hidden></p> <p id="tracking-status" class="notice" hidden></p>
<h2 data-i18n="html.dt.stationen"></h2> <h2 data-i18n="route.station-count">Stationen</h2>
<ul id="poi-list" class="route-list"></ul> <ul id="poi-list" class="route-list"></ul>
</div> </div>
</section> </section>
<section id="poi-page" class="app-page" hidden> <section id="poi-page" class="app-page" hidden>
<header class="has-back-button"> <header class="has-back-button">
<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"> <button class="back-button" type="button" data-page-target="#route-page" aria-label="Zurück zur Strecke" title="Zurück zur Strecke" data-i18n-title="route.back" data-i18n-aria-label="route.back">
<img src="images/icons/back.svg" alt=""> <img src="images/icons/back.svg" alt="">
</button> </button>
<h1 id="poi-title" data-i18n="html.poi-title.station"></h1> <h1 id="poi-title" data-i18n="station.title">Station</h1>
</header> </header>
<div class="content"> <div class="content">
<div id="slideshow" aria-live="polite"> <div id="slideshow" aria-live="polite">
@@ -266,21 +266,21 @@
<img id="slide-image" alt=""> <img id="slide-image" alt="">
</figure> </figure>
<fieldset> <fieldset>
<legend data-i18n="html.legend.bildnavigation"></legend> <legend data-i18n="slideshow.controls-label">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"> <button id="slide-prev" type="button" aria-label="Vorheriges Bild" title="Vorheriges Bild" data-i18n-title="slideshow.previous" data-i18n-aria-label="slideshow.previous">
<img src="images/icons/chevron-left.svg" alt="" data-i18n-alt="html.slide-prev.vorheriges.bild"> <img src="images/icons/chevron-left.svg" alt="Vorheriges Bild" data-i18n-alt="slideshow.previous">
</button> </button>
<div> <div>
<p id="slide-caption"></p> <p id="slide-caption"></p>
<span id="slide-position">0 / 0</span> <span id="slide-position">0 / 0</span>
</div> </div>
<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"> <button id="slide-next" type="button" aria-label="Nächstes Bild" title="Nächstes Bild" data-i18n-title="slideshow.next" data-i18n-aria-label="slideshow.next">
<img src="images/icons/chevron-right.svg" alt="" data-i18n-alt="html.slide-next.nachstes.bild"> <img src="images/icons/chevron-right.svg" alt="Nächstes Bild" data-i18n-alt="slideshow.next">
</button> </button>
</fieldset> </fieldset>
</div> </div>
<p id="poi-description"></p> <p id="poi-description"></p>
<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> <audio id="poi-audio" controls preload="metadata" aria-label="Ton zur Station" hidden data-i18n-aria-label="audio.station-label" data-i18n="audio.unsupported">Dieses Gerät kann den Ton leider nicht abspielen.</audio>
</div> </div>
</section> </section>
<footer id="navigation-footer" hidden aria-live="polite"> <footer id="navigation-footer" hidden aria-live="polite">
@@ -288,7 +288,7 @@
<path d="M32 4 53 54 32 44 11 54Z"></path> <path d="M32 4 53 54 32 44 11 54Z"></path>
</svg> </svg>
<p> <p>
<span id="navigation-mode" data-i18n="html.navigation-mode.richtung.zum.nachsten.station"></span> <span id="navigation-mode" data-i18n="navigation.station-direction">Richtung zum nächsten Station</span>
<strong id="navigation-distance">–</strong> <strong id="navigation-distance">–</strong>
</p> </p>
</footer> </footer>
@@ -296,94 +296,94 @@
<dialog id="info-dialog" class="info-dialog" aria-labelledby="info-dialog-title"> <dialog id="info-dialog" class="info-dialog" aria-labelledby="info-dialog-title">
<div class="info-dialog-shell"> <div class="info-dialog-shell">
<header class="info-dialog-header"> <header class="info-dialog-header">
<h2 id="info-dialog-title" data-i18n="legal-ui.informationen"></h2> <h2 id="info-dialog-title" data-i18n="info.title">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> <button id="info-close" type="button" aria-label="Informationsfenster schließen" title="Schließen" data-i18n-title="info.close" data-i18n-aria-label="info.close-label">×</button>
</header> </header>
<div class="info-dialog-content"> <div class="info-dialog-content">
<article class="info-page" data-info-page="privacy" hidden> <article class="info-page" data-info-page="privacy" hidden>
<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> <p class="legal-warning"><strong data-i18n="common.notice">Hinweis:</strong><span data-i18n="privacy.incomplete-notice">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.</span></p>
<h3 data-i18n="html.h3.1.verantwortliche.stelle.und.datenschutzkontakt"></h3> <h3 data-i18n="privacy.controller-title">1. Verantwortliche Stelle und Datenschutzkontakt</h3>
<p> <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> <strong data-i18n="contacts.school.name">Staatliche Grundschule Herpf</strong><br><span data-i18n="contacts.school.address">Zum Eichig 3</span><br><span data-i18n="contacts.school.city">98617 Meiningen / OT Herpf</span><br><span data-i18n="contacts.school.phone">Telefon: (036943) 63722</span><br><span data-i18n="contacts.school.email">E-Mail: info@grundschule-herpf.de</span></p>
<p><span data-i18n="html.p.datenschutzangaben.der.schule"></span><br> <p><span data-i18n="contacts.school.privacy-label">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> <strong data-i18n="contacts.privacy.name">Staatliches Schulamt Südthüringen</strong><br><span data-i18n="contacts.privacy.role">Datenschutzbeauftragter</span><br><span data-i18n="contacts.privacy.address">Hölderlinstraße 1</span><br><span data-i18n="contacts.privacy.city">98527 Suhl</span><br><span data-i18n="contacts.privacy.phone">Telefon: 03681 / 7341 25</span><br><span data-i18n="contacts.privacy.fax">Fax: 03681 / 7341 09</span><br><span data-i18n="contacts.privacy.email">E-Mail: datenschutz.suedthueriungen@schulamt.thueringen.de</span></p>
<p data-i18n="html.p.der.datenschutzbeauftragte.kann.auerdem.uber.die.postadresse"></p> <p data-i18n="contacts.privacy.postal-hint">Der Datenschutzbeauftragte kann außerdem über die Postadresse der Schule mit dem Zusatz „der Datenschutzbeauftragte“ erreicht werden.</p>
<h3 data-i18n="html.h3.2.wenn.du.wegwichtel.offnest"></h3> <h3 data-i18n="privacy.connection-title">2. Wenn du Wegwichtel öffnest</h3>
<p data-i18n="html.p.wenn.du.wegwichtel.offnest.konnen.verbindungsdaten.verarbeitet"></p> <p data-i18n="privacy.connection-data">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.</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> <p><span data-i18n="privacy.provider-label">Wer die Website bereitstellt und welche Verbindungsdaten wie lange gespeichert werden:</span><strong data-i18n="privacy.provider-placeholder">[Anbieter, Anschrift, gespeicherte Daten und Speicherdauer ergänzen]</strong></p>
<h3 data-i18n="html.h3.3.standortdaten"></h3> <h3 data-i18n="privacy.location-title">3. Standortdaten</h3>
<p data-i18n="html.p.du.entscheidest.ob.wegwichtel.deinen.standort.nutzen"></p> <p data-i18n="privacy.location-data">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.</p>
<h3 data-i18n="html.h3.4.deinen.fortschritt.merken"></h3> <h3 data-i18n="privacy.progress-title">4. Deinen Fortschritt merken</h3>
<p data-i18n="html.p.wenn.du.meinen.fortschritt.merken.einschaltest.speichert"></p> <p data-i18n="privacy.progress-data">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.</p>
<p data-i18n="html.p.wenn.du.deinen.fortschritt.mindestens.einen.tag"></p> <p data-i18n="privacy.progress-deletion">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.</p>
<label class="switch-row compact-switch"> <label for="progress-storage-toggle-2" class="switch-row compact-switch">
<span> <span>
<strong data-i18n="html.strong.meinen.fortschritt.merken"></strong> <strong data-i18n="progress.remember">Meinen Fortschritt merken</strong>
<small data-i18n="html.small.du.entscheidest.ob.sich.wegwichtel.deinen.fortschritt"></small> <small data-i18n="progress.choice-hint">Du entscheidest, ob sich Wegwichtel deinen Fortschritt merkt.</small>
</span> </span>
<input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false"> <input id="progress-storage-toggle-2" class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span> <span class="switch-track" aria-hidden="true"></span>
</label> </label>
<h3 data-i18n="html.h3.5.deine.eigenen.strecken"></h3> <h3 data-i18n="privacy.drafts-title">5. Deine eigenen Strecken</h3>
<p data-i18n="html.p.neue.strecken.bleiben.zuerst.auf.deinem.gerat"></p> <p data-i18n="privacy.draft-data">Neue Strecken bleiben zuerst auf deinem Gerät. Gespeichert werden der Weg, die Orte der Stationen, Texte sowie ausgewählte Bilder und Tonaufnahmen.</p>
<p data-i18n="html.p.deine.entwurfe.bleiben.erhalten.wenn.alter.fortschritt"></p> <p data-i18n="privacy.draft-deletion">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.</p>
<h3 data-i18n="html.h3.6.werbung.und.gespeicherte.einstellungen"></h3> <h3 data-i18n="privacy.settings-title">6. Werbung und gespeicherte Einstellungen</h3>
<p data-i18n="html.p.wegwichtel.verwendet.keine.cookies.es.zahlt.keine"></p> <p data-i18n="privacy.settings-data">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.</p>
<h3 data-i18n="html.h3.7.inhalte.von.schulerinnen.und.schulern"></h3> <h3 data-i18n="privacy.children-title">7. Inhalte von Schülerinnen und Schülern</h3>
<p data-i18n="html.p.bilder.texte.und.tonaufnahmen.durfen.nur.geteilt"></p> <p data-i18n="privacy.children-permission">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.</p>
<h3 data-i18n="html.h3.8.deine.rechte"></h3> <h3 data-i18n="privacy.rights-title">8. Deine Rechte</h3>
<p data-i18n="html.p.du.kannst.fragen.welche.daten.uber.dich"></p> <p data-i18n="privacy.rights-data">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.</p>
<p><span data-i18n="html.p.zustandige.datenschutzaufsicht"></span><br> <p><span data-i18n="contacts.authority.label">Zuständige 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> <strong data-i18n="contacts.authority.name">Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)</strong><br><span data-i18n="contacts.authority.postal-address">Postfach 90 04 55, 99107 Erfurt</span><br><span data-i18n="contacts.authority.address">Häßlerstraße 8, 99096 Erfurt</span><br><span data-i18n="contacts.authority.phone">Telefon: 0361 / 57 311 29 00</span><br><span data-i18n="contacts.authority.fax">Telefax: 0361 / 57 311 29 04</span><br><span data-i18n="contacts.authority.email">E-Mail: poststelle@datenschutz.thueringen.de</span><br><span data-i18n="contacts.authority.website">Website: www.tlfdi.de</span></p>
</article> </article>
<article class="info-page" data-info-page="imprint" hidden> <article class="info-page" data-info-page="imprint" hidden>
<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> <p class="legal-warning"><strong data-i18n="common.notice">Hinweis:</strong><span data-i18n="imprint.incomplete-notice">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.</span></p>
<h3 data-i18n="html.h3.wer.fur.wegwichtel.verantwortlich.ist"></h3> <h3 data-i18n="imprint.controller-title">Wer für Wegwichtel verantwortlich ist</h3>
<p> <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></p> <strong data-i18n="contacts.school.name">Staatliche Grundschule Herpf</strong><br><span data-i18n="contacts.school.address">Zum Eichig 3</span><br><span data-i18n="contacts.school.city">98617 Meiningen / OT Herpf</span></p>
<h3 data-i18n="html.h3.kontakt"></h3> <h3 data-i18n="imprint.contact-title">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> <p><span data-i18n="contacts.school.phone">Telefon: (036943) 63722</span><br><span data-i18n="contacts.school.fax">Telefax: (036943) 63843</span><br><span data-i18n="contacts.school.email">E-Mail: info@grundschule-herpf.de</span></p>
<h3 data-i18n="html.h3.leitung.der.schule"></h3> <h3 data-i18n="imprint.head-title">Leitung der Schule</h3>
<p data-i18n="html.p.frau.wichert.schulleiterin"></p> <p data-i18n="contacts.school.head">Frau Wichert, Schulleiterin</p>
<h3 data-i18n="html.h3.schultrager"></h3> <h3 data-i18n="imprint.sponsor-title">Schulträger</h3>
<p> <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> <strong data-i18n="contacts.sponsor.name">Landkreis Schmalkalden-Meiningen</strong><br><span data-i18n="contacts.sponsor.role">Landratsamt</span><br><span data-i18n="contacts.sponsor.address">Obertshäuser Platz 1</span><br><span data-i18n="contacts.sponsor.city">98617 Meiningen</span></p>
<h3 data-i18n="html.h3.inhaltlich.verantwortlich"></h3> <h3 data-i18n="imprint.content-title">Inhaltlich verantwortlich</h3>
<p data-i18n="html.p.frau.wichert"></p> <p data-i18n="contacts.school.content-owner">Frau Wichert</p>
<h3 data-i18n="html.h3.weitere.pflichtangaben"></h3> <h3 data-i18n="imprint.required-title">Weitere Pflichtangaben</h3>
<p data-i18n="html.p.je.nachdem.wer.wegwichtel.betreibt.konnen.weitere"></p> <p data-i18n="imprint.required-data">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.</p>
<h3 data-i18n="html.h3.haftung.fur.inhalte.und.verweise"></h3> <h3 data-i18n="imprint.liability-title">Haftung für Inhalte und Verweise</h3>
<p data-i18n="html.p.die.inhalte.werden.sorgfaltig.erstellt.und.regelmaig"></p> <p data-i18n="imprint.liability-data">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.</p>
<h3 data-i18n="html.h3.urheber.und.nutzungsrechte"></h3> <h3 data-i18n="imprint.copyright-title">Urheber- und Nutzungsrechte</h3>
<p data-i18n="html.p.wenn.nichts.anderes.angegeben.ist.liegen.die"></p> <p data-i18n="imprint.copyright-data">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.</p>
</article> </article>
<article class="info-page" data-info-page="technology" hidden> <article class="info-page" data-info-page="technology" hidden>
<h3 data-i18n="html.h3.was.merkt.sich.wegwichtel"></h3> <h3 data-i18n="storage-help.progress-title">Was merkt sich Wegwichtel?</h3>
<p data-i18n="html.p.du.kannst.deinen.fortschritt.auf.diesem.gerat"></p> <p data-i18n="storage-help.progress-data">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.</p>
<p data-i18n="html.p.wenn.du.einen.tag.lang.nichts.mehr"></p> <p data-i18n="storage-help.progress-deletion">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.</p>
<label class="switch-row compact-switch"> <label for="progress-storage-toggle-3" class="switch-row compact-switch">
<span> <span>
<strong data-i18n="html.strong.meinen.fortschritt.merken"></strong> <strong data-i18n="progress.remember">Meinen Fortschritt merken</strong>
<small data-i18n="html.small.die.anderung.gilt.sofort.auf.diesem.gerat"></small> <small data-i18n="progress.change-hint">Die Änderung gilt sofort auf diesem Gerät.</small>
</span> </span>
<input class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false"> <input id="progress-storage-toggle-3" class="progress-storage-toggle" type="checkbox" role="switch" aria-checked="false">
<span class="switch-track" aria-hidden="true"></span> <span class="switch-track" aria-hidden="true"></span>
</label> </label>
<h3 data-i18n="html.h3.ohne.internet.unterwegs"></h3> <h3 data-i18n="storage-help.offline-title">Ohne Internet unterwegs</h3>
<p data-i18n="html.p.mit.app.ohne.internet.offnen.speicherst.du"></p> <p data-i18n="storage-help.offline-data">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.</p>
<p data-i18n="html.p.mit.weg.als.datei.herunterladen.kannst.du"></p> <p data-i18n="storage-help.export-data">Mit „Weg als Datei herunterladen“ kannst du den Weg in einer anderen Wander-App öffnen. Bilder und Tonaufnahmen sind in dieser Datei nicht enthalten.</p>
<h3 data-i18n="html.h3.deine.eigenen.strecken"></h3> <h3 data-i18n="storage-help.drafts-title">Deine eigenen Strecken</h3>
<p data-i18n="html.p.neue.strecken.bleiben.zuerst.auf.diesem.gerat"></p> <p data-i18n="storage-help.drafts-data">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.</p>
<p data-i18n="html.p.deine.entwurfe.bleiben.gespeichert.bis.du.sie"></p> <p data-i18n="storage-help.drafts-deletion">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.</p>
<h3 data-i18n="html.h3.welche.erlaubnisse.braucht.die.app"></h3> <h3 data-i18n="storage-help.permissions-title">Welche Erlaubnisse braucht die App?</h3>
<ul> <ul>
<li data-i18n="html.li.deinen.standort.um.strecken.in.deiner.nahe"></li> <li data-i18n="storage-help.permissions.location">Deinen Standort, um Strecken in deiner Nähe und den Weg zur nächsten Station zu zeigen.</li>
<li data-i18n="html.li.die.richtung.deines.gerats.damit.sich.der"></li> <li data-i18n="storage-help.permissions.direction">Die Richtung deines Geräts, damit sich der Pfeil mitdrehen kann.</li>
<li data-i18n="html.li.zugriff.auf.ausgewahlte.bilder.und.tonaufnahmen.wenn"></li> <li data-i18n="storage-help.permissions.files">Zugriff auf ausgewählte Bilder und Tonaufnahmen, wenn du sie zu einer Strecke hinzufügen möchtest.</li>
</ul> </ul>
<p data-i18n="html.p.du.kannst.diese.erlaubnisse.ablehnen.dann.fehlen"></p> <p data-i18n="storage-help.permissions.optional">Du kannst diese Erlaubnisse ablehnen. Dann fehlen die jeweiligen Funktionen. Strecken und Stationen kannst du weiterhin selbst auswählen.</p>
<h3 data-i18n="html.h3.werbung.und.andere.anbieter"></h3> <h3 data-i18n="storage-help.providers-title">Werbung und andere Anbieter</h3>
<p data-i18n="html.p.wegwichtel.zeigt.keine.werbung.und.verfolgt.nicht"></p> <p data-i18n="storage-help.providers-data">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.</p>
</article> </article>
</div> </div>
</div> </div>
@@ -391,58 +391,58 @@
<dialog id="editor-dialog" class="editor-dialog" aria-labelledby="editor-dialog-title"> <dialog id="editor-dialog" class="editor-dialog" aria-labelledby="editor-dialog-title">
<div class="editor-dialog-shell"> <div class="editor-dialog-shell">
<header class="editor-dialog-header"> <header class="editor-dialog-header">
<h2 id="editor-dialog-title" data-i18n="html.editor-button.strecken.bearbeiten"></h2> <h2 id="editor-dialog-title" data-i18n="editor.open">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> <button id="editor-close" type="button" aria-label="Fenster zum Bearbeiten schließen" title="Schließen" data-i18n-title="info.close" data-i18n-aria-label="editor.close">×</button>
</header> </header>
<output id="editor-status" class="editor-status" aria-live="polite" hidden></output> <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"> <section id="editor-route-list-view" class="editor-view" aria-labelledby="editor-route-list-title">
<div class="editor-toolbar"> <div class="editor-toolbar">
<h3 id="editor-route-list-title" data-i18n="html.editor-route-list-title.deine.strecken"></h3> <h3 id="editor-route-list-title" data-i18n="editor.routes-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"> <button id="editor-new-route" class="editor-round-button" type="button" aria-label="Neue Strecke anlegen" title="Neue Strecke anlegen" data-i18n-title="editor.create-route" data-i18n-aria-label="editor.create-route">
<span aria-hidden="true">+</span> <span aria-hidden="true">+</span>
</button> </button>
</div> </div>
<section class="storage-usage" data-storage-usage aria-label="" data-i18n-aria-label="html.section.platz.fur.wegwichtel"> <section class="storage-usage" data-storage-usage aria-label="Platz für Wegwichtel" data-i18n-aria-label="storage.title">
<h3 data-i18n="html.section.platz.fur.wegwichtel"></h3> <h3 data-i18n="storage.title">Platz für 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> <meter data-storage-site-meter min="0" max="1" value="0" aria-label="Belegter Platz für App, Bilder und Ton" hidden data-i18n-aria-label="storage.site-meter-label"></meter>
<p data-storage-site-text data-i18n="html.p.wir.schauen.wie.viel.platz.belegt.ist"></p> <p data-storage-site-text data-i18n="storage.loading">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> <meter data-storage-local-meter min="0" max="5242880" value="0" aria-label="Belegter Platz für Texte und Einstellungen; der Balken zeigt einen ungefähren Richtwert" data-i18n-aria-label="storage.local-meter-label"></meter>
<p data-storage-local-text></p> <p data-storage-local-text></p>
<p data-i18n="html.p.der.erste.balken.zeigt.den.platz.fur"></p> <p data-i18n="storage.help">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.</p>
<output data-storage-warning hidden></output> <output data-storage-warning hidden></output>
<button data-storage-refresh type="button" data-i18n="html.button.platz.prufen"></button> <button data-storage-refresh type="button" data-i18n="storage.refresh">Platz prüfen</button>
</section> </section>
<p class="editor-muted" data-i18n="html.p.hier.findest.du.strecken.fur.alle.und"></p> <p class="editor-muted" data-i18n="editor.routes-hint">Hier findest du Strecken für alle und deine eigenen Entwürfe.</p>
<ul id="editor-route-list" class="editor-route-list" aria-live="polite"></ul> <ul id="editor-route-list" class="editor-route-list" aria-live="polite"></ul>
</section> </section>
<section id="editor-route-detail-view" class="editor-view" aria-labelledby="editor-route-detail-title" hidden> <section id="editor-route-detail-view" class="editor-view" aria-labelledby="editor-route-detail-title" hidden>
<div class="editor-toolbar editor-toolbar-back"> <div class="editor-toolbar editor-toolbar-back">
<button id="editor-route-back" class="editor-secondary-button" type="button" data-i18n="html.editor-route-back.zuruck"></button> <button id="editor-route-back" class="editor-secondary-button" type="button" data-i18n="common.back">Zurück</button>
<h3 id="editor-route-detail-title" data-i18n="html.editor-route-detail-title.deine.strecke"></h3> <h3 id="editor-route-detail-title" data-i18n="editor.route-title">Deine Strecke</h3>
</div> </div>
<form id="editor-route-form" class="editor-form"> <form id="editor-route-form" class="editor-form">
<input id="editor-route-id" type="hidden"> <input id="editor-route-id" type="hidden">
<label for="editor-route-name" data-i18n="html.label.name.der.strecke"></label> <label for="editor-route-name" data-i18n="editor.route-name">Name der Strecke</label>
<input id="editor-route-name" type="text" autocomplete="off" required> <input id="editor-route-name" type="text" autocomplete="off" required>
<label for="editor-route-description" data-i18n="html.label.beschreibung"></label> <label for="editor-route-description" data-i18n="editor.description">Beschreibung</label>
<textarea id="editor-route-description" rows="4"></textarea> <textarea id="editor-route-description" rows="4"></textarea>
<div id="editor-route-gpx-group" class="editor-gpx-group"> <div id="editor-route-gpx-group" class="editor-gpx-group">
<label for="editor-route-gpx" data-i18n="html.label.weg.aus.einer.datei.ubernehmen"></label> <label for="editor-route-gpx" data-i18n="editor.import-path">Weg aus einer Datei übernehmen</label>
<input id="editor-route-gpx" type="file" accept=".gpx,application/gpx+xml,text/xml,application/xml"> <input id="editor-route-gpx" type="file" accept=".gpx,application/gpx+xml,text/xml,application/xml">
</div> </div>
<p id="editor-route-activated" class="editor-muted" data-i18n="html.editor-route-activated.deine.neue.strecke.bleibt.zuerst.auf.diesem"></p> <p id="editor-route-activated" class="editor-muted" data-i18n="editor.new-draft-hint">Deine neue Strecke bleibt zuerst auf diesem Gerät. Mit „Zur Prüfung senden“ kannst du sie zur Freigabe einreichen.</p>
<div class="editor-form-actions"> <div class="editor-form-actions">
<button id="editor-route-save" class="editor-primary-button" type="submit" data-i18n="html.editor-route-save.strecke.speichern"></button> <button id="editor-route-save" class="editor-primary-button" type="submit" data-i18n="editor.save-route">Strecke speichern</button>
<button id="editor-route-upload" class="editor-secondary-button" type="button" hidden data-i18n="editor-ui.zur.prufung.senden"></button> <button id="editor-route-upload" class="editor-secondary-button" type="button" hidden data-i18n="editor.submit">Zur Prüfung senden</button>
</div> </div>
</form> </form>
<div class="editor-toolbar editor-poi-toolbar"> <div class="editor-toolbar editor-poi-toolbar">
<h3 data-i18n="html.dt.stationen"></h3> <h3 data-i18n="route.station-count">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"> <button id="editor-add-poi" class="editor-round-button" type="button" aria-label="Station hinzufügen" title="Station hinzufügen" data-i18n-title="editor.add-station" data-i18n-aria-label="editor.add-station">
<span aria-hidden="true">+</span> <span aria-hidden="true">+</span>
</button> </button>
</div> </div>
@@ -450,49 +450,49 @@
</section> </section>
<section id="editor-poi-form-view" class="editor-view" aria-labelledby="editor-poi-form-title" hidden> <section id="editor-poi-form-view" class="editor-view" aria-labelledby="editor-poi-form-title" hidden>
<div class="editor-toolbar editor-toolbar-back"> <div class="editor-toolbar editor-toolbar-back">
<button id="editor-poi-cancel" class="editor-secondary-button" type="button" data-i18n="html.button.zuruck.zur.strecke"></button> <button id="editor-poi-cancel" class="editor-secondary-button" type="button" data-i18n="route.back">Zurück zur Strecke</button>
<h3 id="editor-poi-form-title" data-i18n="editor-ui.station.bearbeiten"></h3> <h3 id="editor-poi-form-title" data-i18n="editor.edit-station">Station bearbeiten</h3>
</div> </div>
<form id="editor-poi-form" class="editor-form"> <form id="editor-poi-form" class="editor-form">
<input id="editor-poi-id" type="hidden"> <input id="editor-poi-id" type="hidden">
<label for="editor-poi-title" data-i18n="html.label.name"></label> <label for="editor-poi-title" data-i18n="editor.station-name">Name</label>
<input id="editor-poi-title" type="text" autocomplete="off" required> <input id="editor-poi-title" type="text" autocomplete="off" required>
<label for="editor-poi-description" data-i18n="html.label.text"></label> <label for="editor-poi-description" data-i18n="editor.station-text">Text</label>
<textarea id="editor-poi-description" rows="6"></textarea> <textarea id="editor-poi-description" rows="6"></textarea>
<input id="editor-poi-lat" type="hidden"> <input id="editor-poi-lat" type="hidden">
<input id="editor-poi-lon" type="hidden"> <input id="editor-poi-lon" type="hidden">
<input id="editor-poi-radius" type="hidden" value="25"> <input id="editor-poi-radius" type="hidden" value="25">
<div class="editor-media-actions" aria-label="" data-i18n-aria-label="html.div.bilder.und.ton.zur.station.hinzufugen"> <div class="editor-media-actions" aria-label="Bilder und Ton zur Station hinzufügen" data-i18n-aria-label="editor.media-actions-label">
<button id="editor-poi-audio-add" class="editor-media-button" type="button" title="" data-i18n-title="html.editor-poi-audio-add.ton.hinzufugen"> <button id="editor-poi-audio-add" class="editor-media-button" type="button" title="Ton hinzufügen" data-i18n-title="editor.add-audio">
<svg class="editor-media-icon" viewbox="0 0 32 32" aria-hidden="true" focusable="false"> <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="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> <path d="M6 4v6M3 7h6"></path>
</svg> </svg>
<span data-i18n="html.editor-poi-audio-add.ton.hinzufugen"></span> <span data-i18n="editor.add-audio">Ton hinzufügen</span>
</button> </button>
<button id="editor-poi-images-add" class="editor-media-button" type="button" title="" data-i18n-title="html.editor-poi-images-add.bild.hinzufugen"> <button id="editor-poi-images-add" class="editor-media-button" type="button" title="Bild hinzufügen" data-i18n-title="editor.add-image">
<svg class="editor-media-icon" viewbox="0 0 32 32" aria-hidden="true" focusable="false"> <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> <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> <circle cx="16" cy="18" r="5"></circle>
<path d="M6 3v6M3 6h6"></path> <path d="M6 3v6M3 6h6"></path>
</svg> </svg>
<span data-i18n="html.editor-poi-images-add.bild.hinzufugen"></span> <span data-i18n="editor.add-image">Bild hinzufügen</span>
</button> </button>
</div> </div>
<input id="editor-poi-audio" type="file" accept="audio/*" hidden> <input id="editor-poi-audio" type="file" accept="audio/*" hidden>
<input id="editor-poi-images" type="file" accept="image/*" multiple hidden> <input id="editor-poi-images" type="file" accept="image/*" multiple hidden>
<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> <div id="editor-poi-media-preview" class="editor-media-preview" aria-label="Bilder und Ton ansehen" data-i18n-aria-label="editor.media-preview-label"></div>
<p id="editor-poi-media-status" class="editor-muted" aria-live="polite"></p> <p id="editor-poi-media-status" class="editor-muted" aria-live="polite"></p>
<div class="editor-form-actions"> <div class="editor-form-actions">
<button id="editor-poi-save" class="editor-primary-button" type="submit" data-i18n="html.editor-poi-save.station.speichern"></button> <button id="editor-poi-save" class="editor-primary-button" type="submit" data-i18n="editor.save-station">Station speichern</button>
<button id="editor-poi-delete" class="editor-danger-button" type="button" hidden> <button id="editor-poi-delete" class="editor-danger-button" type="button" hidden>
<span class="editor-danger-icon" aria-hidden="true">−</span> <span class="editor-danger-icon" aria-hidden="true">−</span>
<span data-i18n="html.span.station.entfernen"></span> <span data-i18n="editor.remove-station">Station entfernen</span>
</button> </button>
</div> </div>
</form> </form>
@@ -500,6 +500,7 @@
</div> </div>
</dialog> </dialog>
<script src="js/texts-de.js?v=0.12.14"></script> <script src="js/texts-de.js?v=0.12.14"></script>
<script src="js/texts-en.js?v=0.12.14"></script>
<script src="js/i18n.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/user-messages.js?v=0.12.14"></script>
<script src="js/local-routes.js?v=0.12.14"></script> <script src="js/local-routes.js?v=0.12.14"></script>
+43 -43
View File
@@ -58,7 +58,7 @@
if (state.routeState === 'paused') { if (state.routeState === 'paused') {
$('#tracking-status') $('#tracking-status')
.prop('hidden', false) .prop('hidden', false)
.text(ns.I18n.t("app.du.kannst.dort.weitermachen.wo.du.aufgehort")); .text(ns.I18n.t("app.resume-hint"));
} else { } else {
$('#tracking-status').prop('hidden', true).text(''); $('#tracking-status').prop('hidden', true).text('');
} }
@@ -115,7 +115,7 @@
const proximity = route.proximityM == null const proximity = route.proximityM == null
? '' ? ''
: `<small>${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</small>`; : `<small>${escapeHtml(ns.Distance.format(route.proximityM))} entfernt</small>`;
const sourceLabel = source === 'local' ? ("<small>" + escapeHtml(ns.I18n.t("app.eigener.entwurf")) + "</small>") : ''; const sourceLabel = source === 'local' ? ("<small>" + escapeHtml(ns.I18n.t("app.draft")) + "</small>") : '';
return ` return `
<li> <li>
<button type="button" data-route-select data-route-id="${escapeHtml(route.id)}" data-route-source="${source}"> <button type="button" data-route-select data-route-id="${escapeHtml(route.id)}" data-route-source="${source}">
@@ -157,21 +157,21 @@
function updateRouteControlSummary() { function updateRouteControlSummary() {
if (!state.route) { if (!state.route) {
setRouteControlSummary(ns.I18n.t("app.strecke.wahlen"), ns.I18n.t("app.strecken.fur.alle.und.meine.entwurfe")); setRouteControlSummary(ns.I18n.t("app.choose-route"), ns.I18n.t("app.route-menu"));
return; return;
} }
const titleText = state.route.name || ns.I18n.t("app.strecke"); const titleText = state.route.name || ns.I18n.t("app.route");
const metric = nextPoiMetric(); const metric = nextPoiMetric();
if (!metric) { if (!metric) {
const detailText = (state.route.pois || []).length const detailText = (state.route.pois || []).length
? ns.I18n.t("app.alle.stationen.erreicht") ? ns.I18n.t("app.stations-complete")
: ns.I18n.t("app.keine.stationen.vorhanden"); : ns.I18n.t("app.no-stations");
setRouteControlSummary(titleText, detailText); setRouteControlSummary(titleText, detailText);
return; return;
} }
const distance = metric.distance == null ? '' : ` · ${Math.round(metric.distance)} m`; const distance = metric.distance == null ? '' : ` · ${Math.round(metric.distance)} m`;
setRouteControlSummary(titleText, ns.I18n.t("app.nachste.station", { value1: metric.poi.title, value2: distance })); setRouteControlSummary(titleText, ns.I18n.t("app.next-station-summary", { value1: metric.poi.title, value2: distance }));
} }
function renderRouteControlList(selector, routes, source, emptyText) { function renderRouteControlList(selector, routes, source, emptyText) {
@@ -187,7 +187,7 @@
<li> <li>
<button type="button" data-route-select data-route-id="${escapeHtml(route.id)}" data-route-source="${source}" ${current ? 'aria-current="true"' : ''}> <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> <strong>${escapeHtml(route.name)}</strong>
<small>${source === 'local' ? ns.I18n.t("app.eigener.entwurf") : ns.I18n.t("app.veroffentlicht")}</small> <small>${source === 'local' ? ns.I18n.t("app.draft") : ns.I18n.t("app.published")}</small>
</button> </button>
</li> </li>
`); `);
@@ -198,22 +198,22 @@
const routeControlLists = $('.route-control-list'); const routeControlLists = $('.route-control-list');
routeControlLists.attr('aria-busy', 'true'); routeControlLists.attr('aria-busy', 'true');
$('#route-control-server-title').text(ns.I18n.t("app.strecken.fur.alle", { value1: state.routes.length })); $('#route-control-server-title').text(ns.I18n.t("app.public-route-count", { value1: state.routes.length }));
$('#route-control-local-title').text(ns.I18n.t("app.meine.entwurfe", { value1: state.localRoutes.length })); $('#route-control-local-title').text(ns.I18n.t("app.draft-count", { value1: state.localRoutes.length }));
$('#local-route-title').text(ns.I18n.t("app.meine.entwurfe", { value1: state.localRoutes.length })); $('#local-route-title').text(ns.I18n.t("app.draft-count", { value1: state.localRoutes.length }));
updateRouteControlSummary(); updateRouteControlSummary();
renderRouteControlList( renderRouteControlList(
'#route-control-server-list', '#route-control-server-list',
state.routes, state.routes,
'server', 'server',
ns.I18n.t("app.hier.gibt.es.noch.keine.strecken.fur") ns.I18n.t("app.no-public-route")
); );
renderRouteControlList( renderRouteControlList(
'#route-control-local-list', '#route-control-local-list',
state.localRoutes, state.localRoutes,
'local', 'local',
state.localRoutesError || ns.I18n.t("app.du.hast.noch.keine.eigene.strecke.angelegt") state.localRoutesError || ns.I18n.t("app.no-route")
); );
routeControlLists.attr('aria-busy', 'false'); routeControlLists.attr('aria-busy', 'false');
@@ -239,26 +239,26 @@
.sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de')); .sort((a, b) => String(a.name ?? '').localeCompare(String(b.name ?? ''), 'de'));
const nearbyEmptyText = state.position const nearbyEmptyText = state.position
? ns.I18n.t("app.keine.strecke.im.umkreis.von.km.gefunden", { value1: ns.Config.routeRadiusKm }) ? ns.I18n.t("app.no-nearby-route", { value1: ns.Config.routeRadiusKm })
: ns.I18n.t("app.erlaube.deinen.standort.um.strecken.in.deiner"); : ns.I18n.t("app.location-permission-hint");
renderRouteItems('#nearby-route-list', nearbyRoutes, nearbyEmptyText); renderRouteItems('#nearby-route-list', nearbyRoutes, nearbyEmptyText);
renderRouteItems( renderRouteItems(
'#all-route-list', '#all-route-list',
allRoutes, allRoutes,
searchTerm ? ns.I18n.t("app.keine.passende.strecke.gefunden.versuche.ein.anderes") : ns.I18n.t("app.hier.gibt.es.noch.keine.strecken.fur") searchTerm ? ns.I18n.t("app.no-search-result") : ns.I18n.t("app.no-public-route")
); );
renderRouteItems( renderRouteItems(
'#local-route-list', '#local-route-list',
localRoutes, localRoutes,
state.localRoutesError || ns.I18n.t("app.du.hast.noch.keine.eigene.strecke.angelegt"), state.localRoutesError || ns.I18n.t("app.no-route"),
'local' 'local'
); );
$('#all-routes-summary').text( $('#all-routes-summary').text(
searchTerm searchTerm
? ns.I18n.t("app.von.strecken.gefunden", { value1: allRoutes.length, value2: routes.length }) ? ns.I18n.t("app.search-result-count", { value1: allRoutes.length, value2: routes.length })
: ns.I18n.t("app.strecken.zur.auswahl", { value1: routes.length }) : ns.I18n.t("app.route-count", { value1: routes.length })
); );
renderRouteControl(); renderRouteControl();
} }
@@ -305,7 +305,7 @@
function updateRouteControls() { function updateRouteControls() {
const running = state.routeState === 'running'; const running = state.routeState === 'running';
const paused = state.routeState === 'paused'; const paused = state.routeState === 'paused';
const startLabel = paused ? ns.I18n.t("app.strecke.fortsetzen") : ns.I18n.t("app.strecke.starten"); const startLabel = paused ? ns.I18n.t("app.resume-route") : ns.I18n.t("app.start-route");
$('#start-route') $('#start-route')
.prop('disabled', running) .prop('disabled', running)
.attr({ 'aria-label': startLabel, title: startLabel }); .attr({ 'aria-label': startLabel, title: startLabel });
@@ -333,7 +333,7 @@
setActivePoi(null); setActivePoi(null);
hideNavigation(); hideNavigation();
if (announce && wasActive && !returnToRoutes) { if (announce && wasActive && !returnToRoutes) {
$('#tracking-status').prop('hidden', false).text(ns.I18n.t("app.strecke.beendet")); $('#tracking-status').prop('hidden', false).text(ns.I18n.t("app.route-finished"));
} else { } else {
$('#tracking-status').prop('hidden', true).text(''); $('#tracking-status').prop('hidden', true).text('');
} }
@@ -357,8 +357,8 @@
ns.AudioPlayer.pause(); ns.AudioPlayer.pause();
$('#tracking-status') $('#tracking-status')
.prop('hidden', false) .prop('hidden', false)
.text(ns.I18n.t("app.du.machst.eine.pause.die.app.sucht")); .text(ns.I18n.t("app.pause-hint"));
showNavigation(ns.I18n.t("app.strecke.pausiert"), $('#navigation-distance').text() || '–'); showNavigation(ns.I18n.t("app.route-paused"), $('#navigation-distance').text() || '–');
updateRouteControls(); updateRouteControls();
updateRouteControlSummary(); updateRouteControlSummary();
} }
@@ -384,7 +384,7 @@
updateRouteControls(); updateRouteControls();
const list = $('#poi-list').empty(); const list = $('#poi-list').empty();
if (!route.pois.length) { if (!route.pois.length) {
list.append(("<li><p>" + escapeHtml(ns.I18n.t("app.diese.strecke.hat.noch.keine.stationen")) + "</p></li>")); list.append(("<li><p>" + escapeHtml(ns.I18n.t("app.empty-route")) + "</p></li>"));
updateRouteControlSummary(); updateRouteControlSummary();
return; return;
} }
@@ -537,13 +537,13 @@
$('#navigation-arrow').css('transform', `rotate(${metric.rotation.toFixed(1)}deg)`); $('#navigation-arrow').css('transform', `rotate(${metric.rotation.toFixed(1)}deg)`);
showNavigation( showNavigation(
heading == null heading == null
? ns.I18n.t("app.norden.oben.nachste.station", { value1: metric.poi.title }) ? ns.I18n.t("app.next-station-north", { value1: metric.poi.title })
: ns.I18n.t("app.nachste.station.2", { value1: metric.poi.title }), : ns.I18n.t("app.next-station", { value1: metric.poi.title }),
`${distance} m` `${distance} m`
); );
$('#navigation-footer').attr( $('#navigation-footer').attr(
'aria-label', 'aria-label',
ns.I18n.t("app.nachste.station.in.metern", { value1: metric.poi.title, value2: distance }) ns.I18n.t("app.next-station-distance", { value1: metric.poi.title, value2: distance })
); );
} }
@@ -556,8 +556,8 @@
if (progress.reached) { if (progress.reached) {
$('#navigation-arrow').css('transform', 'rotate(0deg)'); $('#navigation-arrow').css('transform', 'rotate(0deg)');
showNavigation(ns.I18n.t("app.alle.stationen.und.das.ziel.erreicht"), '0 m'); showNavigation(ns.I18n.t("app.destination-reached"), '0 m');
$('#navigation-footer').attr('aria-label', ns.I18n.t("app.alle.stationen.und.das.ziel.erreicht")); $('#navigation-footer').attr('aria-label', ns.I18n.t("app.destination-reached"));
return; return;
} }
const bearing = ns.Distance.bearing(position, progress.target); const bearing = ns.Distance.bearing(position, progress.target);
@@ -568,12 +568,12 @@
const distance = Math.round(progress.distance); const distance = Math.round(progress.distance);
$('#navigation-arrow').css('transform', `rotate(${rotation.toFixed(1)}deg)`); $('#navigation-arrow').css('transform', `rotate(${rotation.toFixed(1)}deg)`);
showNavigation( showNavigation(
heading == null ? ns.I18n.t("app.norden.oben.nachster.punkt.auf.dem.weg") : ns.I18n.t("app.nachster.punkt.auf.dem.weg"), heading == null ? ns.I18n.t("app.next-path-point-north") : ns.I18n.t("app.next-path-point"),
`${distance} m` `${distance} m`
); );
$('#navigation-footer').attr( $('#navigation-footer').attr(
'aria-label', 'aria-label',
ns.I18n.t("app.nachster.punkt.auf.dem.weg.in.metern", { value1: distance }) ns.I18n.t("app.next-path-point-distance", { value1: distance })
); );
} }
@@ -608,8 +608,8 @@
const nearest = nearestPendingPoi(metrics); const nearest = nearestPendingPoi(metrics);
updateNavigation(position); updateNavigation(position);
$('#tracking-status').text( $('#tracking-status').text(
ns.I18n.t("app.dein.standort.wird.gesucht.auf.etwa.m", { value1: Math.round(position.accuracy) }) ns.I18n.t("app.location-accuracy", { 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")) + (nearest ? ns.I18n.t("app.nearest-station-distance", { value1: Math.round(nearest.distance) }) : ns.I18n.t("app.stations-complete-suffix"))
); );
if (nearest && nearest.distance <= nearest.poi.triggerRadiusM) { if (nearest && nearest.distance <= nearest.poi.triggerRadiusM) {
state.triggered.add(nearest.poi.id); state.triggered.add(nearest.poi.id);
@@ -636,11 +636,11 @@
updateRouteControlSummary(); updateRouteControlSummary();
$('#tracking-status') $('#tracking-status')
.prop('hidden', false) .prop('hidden', false)
.text(resumed ? ns.I18n.t("app.strecke.wird.fortgesetzt") : ns.I18n.t("app.location.start")); .text(resumed ? ns.I18n.t("app.resuming") : ns.I18n.t("app.location.start"));
showNavigation(resumed ? ns.I18n.t("app.strecke.wird.fortgesetzt.2") : ns.I18n.t("app.location.searching")); showNavigation(resumed ? ns.I18n.t("app.resuming-title") : ns.I18n.t("app.location.searching"));
ns.Geo.start( ns.Geo.start(
evaluatePosition, evaluatePosition,
() => $('#tracking-status').text(ns.I18n.t("app.dein.standort.konnte.nicht.gefunden.werden.prufe")) () => $('#tracking-status').text(ns.I18n.t("app.error.location"))
); );
const orientationAvailable = await ns.Orientation.start(updateHeading); const orientationAvailable = await ns.Orientation.start(updateHeading);
@@ -648,12 +648,12 @@
ns.Orientation.stop(); ns.Orientation.stop();
return; return;
} }
if (!orientationAvailable) $('#navigation-mode').text(ns.I18n.t("app.norden.oben.navigation")); if (!orientationAvailable) $('#navigation-mode').text(ns.I18n.t("app.navigation-north"));
} }
function routesFromResponse(response) { function routesFromResponse(response) {
if (!Array.isArray(response?.routes)) { if (!Array.isArray(response?.routes)) {
throw new TypeError(ns.I18n.t("app.die.strecken.konnten.nicht.geladen.werden.bitte")); throw new TypeError(ns.I18n.t("app.error.load-routes"));
} }
return response.routes return response.routes
.filter(route => route?.id != null && route?.name != null) .filter(route => route?.id != null && route?.name != null)
@@ -666,7 +666,7 @@
state.localRoutesError = null; state.localRoutesError = null;
} catch (error) { } catch (error) {
state.localRoutes = []; state.localRoutes = [];
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"); state.localRoutesError = ns.UserMessages?.error(error, ns.I18n.t("app.error.load-drafts")) || ns.I18n.t("app.error.load-drafts");
} }
} }
@@ -685,12 +685,12 @@
state.position = position; state.position = position;
updateRouteProximity(position); updateRouteProximity(position);
$('#location-hint').text( $('#location-hint').text(
ns.I18n.t("app.dein.standort.wurde.auf.etwa.m.genau", { value1: Math.round(position.accuracy) }) ns.I18n.t("app.location-found", { value1: Math.round(position.accuracy) })
+ ns.I18n.t("app.wir.zeigen.dir.strecken.bis.km.entfernung", { value1: ns.Config.routeRadiusKm }) + ns.I18n.t("app.search-radius", { value1: ns.Config.routeRadiusKm })
); );
} catch { } catch {
$('#location-hint').text( $('#location-hint').text(
ns.I18n.t("app.dein.standort.konnte.nicht.gefunden.werden.du") ns.I18n.t("app.location-optional")
); );
} finally { } finally {
context.markStep('location', 'done'); context.markStep('location', 'done');
@@ -746,7 +746,7 @@
try { try {
await selectRoute(String(button.data('route-source') || 'server'), String(button.data('route-id'))); await selectRoute(String(button.data('route-source') || 'server'), String(button.data('route-id')));
} catch (error) { } catch (error) {
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")); alert(ns.UserMessages?.error(error, ns.I18n.t("app.error.open-route")) || ns.I18n.t("app.error.open-draft"));
} finally { } finally {
button.prop('disabled', false); button.prop('disabled', false);
} }
+11 -19
View File
@@ -3,15 +3,7 @@
const versions = Object.freeze({ app: '0.12.14', jquery: '4.0.0', jqueryUi: '1.14.2' }); 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 steps = ['storage', 'jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location'];
const stepLabels = Object.freeze({ const stepLabel = step => globalThis.Wegwichtel.I18n.t(`bootstrap-loader.steps.${step}`);
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 screen = document.getElementById('bootstrap-screen');
const progress = document.getElementById('bootstrap-progress'); const progress = document.getElementById('bootstrap-progress');
const errorBox = document.getElementById('bootstrap-error'); const errorBox = document.getElementById('bootstrap-error');
@@ -43,8 +35,8 @@
androidBrowserHint.hidden = false; androidBrowserHint.hidden = false;
androidBrowserHint.dataset.browser = isChrome ? 'chrome' : 'other'; androidBrowserHint.dataset.browser = isChrome ? 'chrome' : 'other';
androidBrowserHint.textContent = isChrome androidBrowserHint.textContent = isChrome
? globalThis.Wegwichtel.I18n.t("bootstrap-loader.auf.diesem.gerat.kannst.du.bilder.und") ? globalThis.Wegwichtel.I18n.t("bootstrap-loader.hints.chrome")
: globalThis.Wegwichtel.I18n.t("bootstrap-loader.tipp.fur.android.mit.chrome.lassen.sich"); : globalThis.Wegwichtel.I18n.t("bootstrap-loader.hints.android");
} }
function schedule(callback, delay = 0) { function schedule(callback, delay = 0) {
@@ -77,7 +69,7 @@
} }
function announceStep(step) { function announceStep(step) {
if (stepLabels[step]) setStatusText(stepLabels[step]); if (steps.includes(step)) setStatusText(stepLabel(step));
} }
function mark(step, state) { function mark(step, state) {
@@ -93,7 +85,7 @@
if (progress) progress.value = completedSteps.size; if (progress) progress.value = completedSteps.size;
if (state === 'failed') { if (state === 'failed') {
setStatusText(globalThis.Wegwichtel.I18n.t("bootstrap.step.failed", { value1: stepLabels[step] || step })); setStatusText(globalThis.Wegwichtel.I18n.t("bootstrap-loader.error.step-error", { value1: stepLabel(step) }));
return; return;
} }
if (state === 'done') { if (state === 'done') {
@@ -108,10 +100,10 @@
script.src = versioned(src); script.src = versioned(src);
script.async = false; script.async = false;
script.onload = () => { script.onload = () => {
if (test && !test()) reject(new Error(globalThis.Wegwichtel.I18n.t("bootstrap-loader.ein.teil.der.app.konnte.nicht.starten"))); if (test && !test()) reject(new Error(globalThis.Wegwichtel.I18n.t("bootstrap-loader.error.start-error")));
else resolve(); else resolve();
}; };
script.onerror = () => reject(new Error(globalThis.Wegwichtel.I18n.t("bootstrap-loader.ein.teil.der.app.konnte.nicht.geladen"))); script.onerror = () => reject(new Error(globalThis.Wegwichtel.I18n.t("bootstrap-loader.error.load-error")));
document.head.appendChild(script); document.head.appendChild(script);
}); });
} }
@@ -127,7 +119,7 @@
} }
function updateCleanupProgress(removed, total) { function updateCleanupProgress(removed, total) {
cleanupStatus.textContent = globalThis.Wegwichtel.I18n.t("bootstrap-loader.alten.fortschritt.loschen", { value1: removed, value2: total }); cleanupStatus.textContent = globalThis.Wegwichtel.I18n.t("bootstrap-loader.cleanup-progress", { value1: removed, value2: total });
cleanupStatus.hidden = false; cleanupStatus.hidden = false;
} }
@@ -161,7 +153,7 @@
screen.classList.add('ready'); screen.classList.add('ready');
readyText.hidden = false; readyText.hidden = false;
setStatusText(globalThis.Wegwichtel.I18n.t("bootstrap.ready")); setStatusText(globalThis.Wegwichtel.I18n.t("bootstrap-loader.ready"));
actionStatus?.classList.add('is-leaving'); actionStatus?.classList.add('is-leaving');
enter.hidden = false; enter.hidden = false;
const showEnter = () => enter.classList.add('is-visible'); const showEnter = () => enter.classList.add('is-visible');
@@ -194,7 +186,7 @@
try { try {
await globalThis.Wegwichtel.OfflineCache?.initialize(); await globalThis.Wegwichtel.OfflineCache?.initialize();
setStatusText(stepLabels.storage, true); setStatusText(stepLabel('storage'), true);
await cleanupOldProgress(); await cleanupOldProgress();
mark('storage', 'done'); mark('storage', 'done');
@@ -222,7 +214,7 @@
console.error(error); console.error(error);
const current = nextPendingStep(); const current = nextPendingStep();
if (current) mark(current, 'failed'); if (current) mark(current, 'failed');
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.textContent = globalThis.Wegwichtel.UserMessages?.error(error, globalThis.Wegwichtel.I18n.t("bootstrap-loader.error.connection-error")) || globalThis.Wegwichtel.I18n.t("bootstrap-loader.error.init-error");
errorBox.hidden = false; errorBox.hidden = false;
retry.hidden = false; retry.hidden = false;
} }
+68 -68
View File
@@ -141,17 +141,17 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
const next = highest + 1; const next = highest + 1;
if (!Number.isSafeInteger(next)) { if (!Number.isSafeInteger(next)) {
throw new TypeError(ns.I18n.t("editor-ui.zu.dieser.strecke.kann.gerade.keine.weitere")); throw new TypeError(ns.I18n.t("editor.error.add-station"));
} }
return next; return next;
} }
function routeName(route) { function routeName(route) {
return text(route?.name || route?.title || ns.I18n.t("editor-ui.unbenannte.strecke")); return text(route?.name || route?.title || ns.I18n.t("editor.untitled-route"));
} }
function poiTitle(poi) { function poiTitle(poi) {
return text(poi?.title || poi?.name || ns.I18n.t("editor-ui.station.ohne.namen")); return text(poi?.title || poi?.name || ns.I18n.t("editor.untitled-station"));
} }
function apiBase() { function apiBase() {
@@ -196,9 +196,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function routeDialogTitle() { function routeDialogTitle() {
if (state.currentSource === 'local') { if (state.currentSource === 'local') {
return routeId(state.currentRoute) ? ns.I18n.t("editor-ui.eigene.strecke.bearbeiten") : ns.I18n.t("editor-ui.neue.strecke"); return routeId(state.currentRoute) ? ns.I18n.t("editor.edit-draft") : ns.I18n.t("editor.new-route");
} }
return ns.I18n.t("editor-ui.strecke.ansehen"); return ns.I18n.t("editor.view-route");
} }
function showListView() { function showListView() {
@@ -216,7 +216,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
function showPoiView() { function showPoiView() {
setDialogTitle(state.currentPoi ? ns.I18n.t("editor-ui.station.bearbeiten") : ns.I18n.t("editor-ui.station.hinzufugen")); setDialogTitle(state.currentPoi ? ns.I18n.t("editor.edit-station") : ns.I18n.t("editor.add-station"));
setHidden(byId(ids.routeListView), true); setHidden(byId(ids.routeListView), true);
setHidden(byId(ids.routeDetailView), true); setHidden(byId(ids.routeDetailView), true);
setHidden(byId(ids.poiFormView), false); setHidden(byId(ids.poiFormView), false);
@@ -265,7 +265,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function handleResponse(response) { async function handleResponse(response) {
if (response.ok) return parseJson(response); if (response.ok) return parseJson(response);
const payload = await parseJson(response); const payload = await parseJson(response);
const error = new Error(payload.message || ns.I18n.t("editor-ui.die.aktion.konnte.nicht.ausgefuhrt.werden.bitte")); const error = new Error(payload.message || ns.I18n.t("editor.error.action"));
error.status = response.status; error.status = response.status;
throw error; throw error;
} }
@@ -354,13 +354,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const label = document.createElement('div'); const label = document.createElement('div');
appendText(label, 'strong', routeName(route)); appendText(label, 'strong', routeName(route));
appendText(label, 'small', routeSource(route) === 'local' ? ns.I18n.t("app.eigener.entwurf") : ns.I18n.t("editor-ui.veroffentlichte.strecke")); appendText(label, 'small', routeSource(route) === 'local' ? ns.I18n.t("app.draft") : ns.I18n.t("editor.published-route"));
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = classes.iconRow; actions.className = classes.iconRow;
actions.appendChild(iconButton(ns.I18n.t("editor-ui.strecke.bearbeiten"), '✎', 'edit-route')); actions.appendChild(iconButton(ns.I18n.t("editor.edit-route"), '✎', 'edit-route'));
if (routeSource(route) === 'local') { if (routeSource(route) === 'local') {
actions.appendChild(iconButton(ns.I18n.t("editor-ui.strecke.loschen"), '×', 'delete-route', false, true)); actions.appendChild(iconButton(ns.I18n.t("editor.delete-route"), '×', 'delete-route', false, true));
} }
item.append(label, actions); item.append(label, actions);
return item; return item;
@@ -380,13 +380,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const title = poiTitle(poi); const title = poiTitle(poi);
const label = document.createElement('div'); const label = document.createElement('div');
appendText(label, 'strong', title); appendText(label, 'strong', title);
appendText(label, 'small', ns.I18n.t("editor-ui.station", { value1: index + 1 })); appendText(label, 'small', ns.I18n.t("editor.station-number", { value1: index + 1 }));
const actions = document.createElement('div'); const actions = document.createElement('div');
actions.className = classes.iconRow; actions.className = classes.iconRow;
actions.appendChild(iconButton(ns.I18n.t("editor.station.up", { value1: title }), '↑', 'move-poi-up', index === 0)); 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(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} bearbeiten`, '✎', 'edit-poi'));
actions.appendChild(iconButton(ns.I18n.t("editor-ui.loschen", { value1: title }), '×', 'delete-poi', false, true)); actions.appendChild(iconButton(ns.I18n.t("editor.delete-item", { value1: title }), '×', 'delete-poi', false, true));
item.append(label, actions); item.append(label, actions);
return item; return item;
} }
@@ -409,8 +409,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
list.textContent = ''; list.textContent = '';
const serverRoutes = state.routes.filter(route => routeSource(route) === 'server'); const serverRoutes = state.routes.filter(route => routeSource(route) === 'server');
const localRoutes = state.routes.filter(route => routeSource(route) === 'local'); const localRoutes = state.routes.filter(route => routeSource(route) === 'local');
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.public-routes"), serverRoutes, ns.I18n.t("app.no-public-route"));
renderRouteGroup(list, ns.I18n.t("editor-ui.meine.entwurfe"), localRoutes, ns.I18n.t("editor-ui.noch.kein.eigener.entwurf.vorhanden")); renderRouteGroup(list, ns.I18n.t("editor.drafts"), localRoutes, ns.I18n.t("editor.no-draft"));
} }
function renderPoiList() { function renderPoiList() {
@@ -420,7 +420,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (!state.pois.length) { if (!state.pois.length) {
const item = document.createElement('li'); const item = document.createElement('li');
item.className = classes.poiItem; item.className = classes.poiItem;
appendText(item, 'span', ns.I18n.t("editor-ui.noch.keine.stationen.angelegt")); appendText(item, 'span', ns.I18n.t("editor.no-stations"));
list.appendChild(item); list.appendChild(item);
return; return;
} }
@@ -437,7 +437,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
async function loadRoutes() { async function loadRoutes() {
setStatus(ns.I18n.t("editor-ui.strecken.werden.geladen")); setStatus(ns.I18n.t("editor.loading-routes"));
const payload = await readJson('/routes?activatedOnly=true'); const payload = await readJson('/routes?activatedOnly=true');
const serverRoutes = routeArray(payload).map(route => ({ ...route, _source: 'server' })); const serverRoutes = routeArray(payload).map(route => ({ ...route, _source: 'server' }));
let localRoutes = []; let localRoutes = [];
@@ -496,7 +496,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function fillRouteForm(route) { function fillRouteForm(route) {
byId(ids.routeId).value = routeId(route); byId(ids.routeId).value = routeId(route);
byId(ids.routeName).value = routeName(route) === ns.I18n.t("editor-ui.unbenannte.strecke") ? '' : routeName(route); byId(ids.routeName).value = routeName(route) === ns.I18n.t("editor.untitled-route") ? '' : routeName(route);
byId(ids.routeDescription).value = text(route?.description); byId(ids.routeDescription).value = text(route?.description);
byId(ids.routeGpx).value = ''; byId(ids.routeGpx).value = '';
setHidden(byId(ids.routeGpxGroup), state.currentSource !== 'server'); setHidden(byId(ids.routeGpxGroup), state.currentSource !== 'server');
@@ -506,16 +506,16 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const uploaded = Boolean(route?.serverRouteId); const uploaded = Boolean(route?.serverRouteId);
setHidden(uploadButton, false); setHidden(uploadButton, false);
setDisabled(uploadButton, uploaded); setDisabled(uploadButton, uploaded);
if (uploadButton) uploadButton.textContent = uploaded ? ns.I18n.t("editor-ui.schon.zur.prufung.gesendet") : ns.I18n.t("editor-ui.zur.prufung.senden"); if (uploadButton) uploadButton.textContent = uploaded ? ns.I18n.t("editor.submitted") : ns.I18n.t("editor.submit");
byId(ids.routeActivated).textContent = uploaded byId(ids.routeActivated).textContent = uploaded
? ns.I18n.t("editor-ui.diese.strecke.wurde.zur.prufung.gesendet.andere") ? ns.I18n.t("editor.submitted-hint")
: ns.I18n.t("editor-ui.dieser.entwurf.bleibt.auf.deinem.gerat.mit"); : ns.I18n.t("editor.draft-hint");
} else { } else {
setHidden(uploadButton, true); setHidden(uploadButton, true);
setDisabled(uploadButton, true); setDisabled(uploadButton, true);
byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at byId(ids.routeActivated).textContent = route?.activatedAt || route?.activated_at
? ns.I18n.t("editor-ui.diese.strecke.ist.fur.alle.freigegeben.und") ? ns.I18n.t("editor.read-only-hint")
: ns.I18n.t("editor-ui.diese.strecke.wurde.noch.nicht.fur.alle"); : ns.I18n.t("editor.unpublished-hint");
} }
} }
@@ -559,7 +559,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
select.dataset.mediaKey = key; select.dataset.mediaKey = key;
select.type = 'button'; select.type = 'button';
select.setAttribute('aria-expanded', String(state.selectedMediaKey === key)); select.setAttribute('aria-expanded', String(state.selectedMediaKey === key));
select.setAttribute('aria-label', ns.I18n.t("editor-ui.knopf.zum.entfernen", { value1: label, value2: state.selectedMediaKey === key ? 'ausblenden' : 'anzeigen' })); select.setAttribute('aria-label', ns.I18n.t("editor.remove-media-label", { value1: label, value2: state.selectedMediaKey === key ? 'ausblenden' : 'anzeigen' }));
if (kind === 'image' && previewUrl) { if (kind === 'image' && previewUrl) {
const image = document.createElement('img'); const image = document.createElement('img');
@@ -612,7 +612,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
preview.appendChild(mediaPreviewCard({ preview.appendChild(mediaPreviewCard({
key: 'pending-audio', key: 'pending-audio',
kind: 'audio', kind: 'audio',
label: state.pendingAudioFile.name || ns.I18n.t("editor-ui.neue.tonaufnahme"), label: state.pendingAudioFile.name || ns.I18n.t("editor.new-audio"),
source: 'pending' source: 'pending'
})); }));
} }
@@ -622,7 +622,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
preview.appendChild(mediaPreviewCard({ preview.appendChild(mediaPreviewCard({
key: `existing-image-${image.id ?? index}`, key: `existing-image-${image.id ?? index}`,
kind: 'image', kind: 'image',
label: image.name || image.caption || ns.I18n.t("editor-ui.bild", { value1: index + 1 }), label: image.name || image.caption || ns.I18n.t("editor.image-number", { value1: index + 1 }),
mediaId: image.id ?? '', mediaId: image.id ?? '',
mediaIndex: index, mediaIndex: index,
previewUrl: imagePreviewUrl(image), previewUrl: imagePreviewUrl(image),
@@ -634,7 +634,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
preview.appendChild(mediaPreviewCard({ preview.appendChild(mediaPreviewCard({
key: pending.key, key: pending.key,
kind: 'image', kind: 'image',
label: pending.file?.name || ns.I18n.t("editor-ui.neues.bild", { value1: index + 1 }), label: pending.file?.name || ns.I18n.t("editor.new-image", { value1: index + 1 }),
mediaIndex: index, mediaIndex: index,
previewUrl: pending.previewUrl || null, previewUrl: pending.previewUrl || null,
source: 'pending' source: 'pending'
@@ -655,8 +655,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (imageCount) parts.push(ns.I18n.t('media.image.count', { count: imageCount })); if (imageCount) parts.push(ns.I18n.t('media.image.count', { count: imageCount }));
status.textContent = parts.length status.textContent = parts.length
? ns.I18n.t("editor-ui.tippe.auf.ein.bild.oder.eine.tonaufnahme", { value1: parts.join(' · ') }) ? ns.I18n.t("editor.remove-media-hint", { value1: parts.join(' · ') })
: ns.I18n.t("editor-ui.noch.keine.bilder.oder.tonaufnahmen.vorhanden"); : ns.I18n.t("editor.no-media");
} }
function openFilePicker(inputId) { function openFilePicker(inputId) {
@@ -668,12 +668,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (kind === 'audio') { if (kind === 'audio') {
return { return {
multiple: false, multiple: false,
types: [{ description: ns.I18n.t("editor-ui.tonaufnahmen"), accept: { 'audio/*': ['.mp3', '.m4a', '.wav', '.ogg', '.webm'] } }] types: [{ description: ns.I18n.t("editor.audio-section"), accept: { 'audio/*': ['.mp3', '.m4a', '.wav', '.ogg', '.webm'] } }]
}; };
} }
return { return {
multiple: true, multiple: true,
types: [{ description: ns.I18n.t("editor-ui.bilder"), accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }] types: [{ description: ns.I18n.t("editor.image-section"), accept: { 'image/*': ['.jpg', '.jpeg', '.png', '.webp'] } }]
}; };
} }
@@ -721,7 +721,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function fillPoiForm(poi) { function fillPoiForm(poi) {
resetPendingMedia(); resetPendingMedia();
byId(ids.poiId).value = poiId(poi); byId(ids.poiId).value = poiId(poi);
byId(ids.poiTitle).value = poiTitle(poi) === ns.I18n.t("editor-ui.station.ohne.namen") ? '' : poiTitle(poi); byId(ids.poiTitle).value = poiTitle(poi) === ns.I18n.t("editor.untitled-station") ? '' : poiTitle(poi);
byId(ids.poiDescription).value = text(poi?.description || poi?.text); byId(ids.poiDescription).value = text(poi?.description || poi?.text);
byId(ids.poiLat).value = text(poi?.lat ?? poi?.latitude); byId(ids.poiLat).value = text(poi?.lat ?? poi?.latitude);
byId(ids.poiLon).value = text(poi?.lon ?? poi?.longitude); byId(ids.poiLon).value = text(poi?.lon ?? poi?.longitude);
@@ -764,7 +764,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (state.currentSource === 'local') { if (state.currentSource === 'local') {
const routePackage = await ns.LocalRoutes.getWithMedia(id, { requestPermission: true }); const routePackage = await ns.LocalRoutes.getWithMedia(id, { requestPermission: true });
if (!routePackage) throw new Error(ns.I18n.t("editor-ui.dein.entwurf.wurde.nicht.gefunden")); if (!routePackage) throw new Error(ns.I18n.t("editor.error.draft-missing"));
state.currentRoute = { state.currentRoute = {
...routePackage.route, ...routePackage.route,
id: routePackage.id, id: routePackage.id,
@@ -792,7 +792,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
id: '', id: '',
name: '', name: '',
description: '', description: '',
gpx: ns.LocalRoutes.emptyGpx(ns.I18n.t("editor-ui.neue.strecke")), gpx: ns.LocalRoutes.emptyGpx(ns.I18n.t("editor.new-route")),
_source: 'local' _source: 'local'
}; };
state.currentPoi = null; state.currentPoi = null;
@@ -800,7 +800,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
fillRouteForm(state.currentRoute); fillRouteForm(state.currentRoute);
renderPoiList(); renderPoiList();
showRouteView(); showRouteView();
setStatus(ns.I18n.t("editor-ui.deine.neue.strecke.bleibt.zuerst.auf.diesem")); setStatus(ns.I18n.t("editor.draft-status"));
} }
function formDataFromRoute(values) { function formDataFromRoute(values) {
@@ -838,12 +838,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.pois = saved.pois; state.pois = saved.pois;
await loadRoutes(); await loadRoutes();
await loadRoute({ id: saved.id, _source: 'local' }); await loadRoute({ id: saved.id, _source: 'local' });
setStatus(ns.I18n.t("editor-ui.gespeichert")); setStatus(ns.I18n.t("editor.saved"));
} }
async function saveServerRoute(values) { async function saveServerRoute(values) {
const id = routeId(state.currentRoute); const id = routeId(state.currentRoute);
if (!id) throw new Error(ns.I18n.t("editor-ui.lege.eine.neue.strecke.zuerst.auf.diesem")); if (!id) throw new Error(ns.I18n.t("editor.error.create-locally"));
const file = byId(ids.routeGpx)?.files?.[0]; const file = byId(ids.routeGpx)?.files?.[0];
const path = `/routes/${encodeURIComponent(id)}`; const path = `/routes/${encodeURIComponent(id)}`;
const payload = file const payload = file
@@ -853,12 +853,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.currentRoute = { ...saved, _source: 'server' }; state.currentRoute = { ...saved, _source: 'server' };
await loadRoutes(); await loadRoutes();
await loadRoute({ id, _source: 'server' }); await loadRoute({ id, _source: 'server' });
setStatus(ns.I18n.t("editor-ui.gespeichert")); setStatus(ns.I18n.t("editor.saved"));
} }
async function saveRoute() { async function saveRoute() {
const values = routeFromForm(); const values = routeFromForm();
if (!values.name) throw new Error(ns.I18n.t("editor-ui.gib.deiner.strecke.einen.namen")); if (!values.name) throw new Error(ns.I18n.t("editor.error.route-name"));
if (state.currentSource === 'server') await saveServerRoute(values); if (state.currentSource === 'server') await saveServerRoute(values);
else await saveLocalRoute(values); else await saveLocalRoute(values);
} }
@@ -877,7 +877,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function localRouteUploadForm(routePackage) { function localRouteUploadForm(routePackage) {
const data = new FormData(); const data = new FormData();
data.append('name', routePackage.route?.name || ns.I18n.t("editor-ui.unbenannte.strecke")); data.append('name', routePackage.route?.name || ns.I18n.t("editor.untitled-route"));
data.append('description', routePackage.route?.description || ''); data.append('description', routePackage.route?.description || '');
const uploadGpx = ns.LocalRoutes.gpxForUpload(routePackage); const uploadGpx = ns.LocalRoutes.gpxForUpload(routePackage);
const gpx = new Blob([uploadGpx], { type: 'application/gpx+xml' }); const gpx = new Blob([uploadGpx], { type: 'application/gpx+xml' });
@@ -888,7 +888,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function storedMediaBlob(media) { async function storedMediaBlob(media) {
const blob = await ns.LocalRoutes.mediaBlob(media, { requestPermission: true }); const blob = await ns.LocalRoutes.mediaBlob(media, { requestPermission: true });
if (!blob) { if (!blob) {
throw new Error(ns.I18n.t("editor-ui.die.datei.konnte.nicht.gesendet.werden.bitte", { value1: media?.name || '' }).trim()); throw new Error(ns.I18n.t("editor.error.send-file", { value1: media?.name || '' }).trim());
} }
return blob; return blob;
} }
@@ -918,12 +918,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage)); const payload = await sendForm('/routes', 'POST', localRouteUploadForm(routePackage));
const serverRoute = payload.route || payload; const serverRoute = payload.route || payload;
createdRouteId = routeId(serverRoute); createdRouteId = routeId(serverRoute);
if (!createdRouteId) throw new Error(ns.I18n.t("editor-ui.die.strecke.konnte.nicht.gesendet.werden.bitte")); if (!createdRouteId) throw new Error(ns.I18n.t("editor.error.send-route"));
const orderedPois = [...(routePackage.pois || [])] const orderedPois = [...(routePackage.pois || [])]
.sort((left, right) => Number(left.sequence) - Number(right.sequence)); .sort((left, right) => Number(left.sequence) - Number(right.sequence));
for (const [index, poi] of orderedPois.entries()) { for (const [index, poi] of orderedPois.entries()) {
const poiTitle = poi.title || ns.I18n.t("editor-ui.station", { value1: index + 1 }); const poiTitle = poi.title || ns.I18n.t("editor.station-number", { value1: index + 1 });
const poiPayload = await sendJson(`/routes/${encodeURIComponent(createdRouteId)}/pois`, 'POST', { const poiPayload = await sendJson(`/routes/${encodeURIComponent(createdRouteId)}/pois`, 'POST', {
description: poi.description || '', description: poi.description || '',
lat: poi.lat, lat: poi.lat,
@@ -934,7 +934,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}); });
const serverPoi = poiPayload.poi || poiPayload; const serverPoi = poiPayload.poi || poiPayload;
const serverPoiId = poiId(serverPoi); const serverPoiId = poiId(serverPoi);
if (!serverPoiId) throw new Error(ns.I18n.t("editor-ui.die.station.konnte.nicht.gesendet.werden.bitte", { value1: poiTitle })); if (!serverPoiId) throw new Error(ns.I18n.t("editor.error.send-station", { value1: poiTitle }));
await uploadLocalPoiMedia(createdRouteId, serverPoiId, poi); await uploadLocalPoiMedia(createdRouteId, serverPoiId, poi);
} }
return createdRouteId; return createdRouteId;
@@ -951,11 +951,11 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
async function uploadLocalRoute() { async function uploadLocalRoute() {
if (state.currentSource !== 'local') throw new Error(ns.I18n.t("editor-ui.du.kannst.nur.deine.eigenen.entwurfe.zur")); if (state.currentSource !== 'local') throw new Error(ns.I18n.t("editor.error.submit-own-draft"));
const values = routeFromForm(); const values = routeFromForm();
if (!values.name) throw new Error(ns.I18n.t("editor-ui.gib.deiner.strecke.einen.namen")); if (!values.name) throw new Error(ns.I18n.t("editor.error.route-name"));
if (state.currentRoute?.serverRouteId) { if (state.currentRoute?.serverRouteId) {
setStatus(ns.I18n.t("editor-ui.diese.strecke.wurde.schon.zur.prufung.gesendet")); setStatus(ns.I18n.t("editor.error.already-submitted"));
return; return;
} }
@@ -963,10 +963,10 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
setDisabled(uploadButton, true); setDisabled(uploadButton, true);
try { try {
await saveLocalRoute(values); await saveLocalRoute(values);
setStatus(ns.I18n.t("editor-ui.deine.strecke.wird.gesendet")); setStatus(ns.I18n.t("editor.submitting"));
const localId = routeId(state.currentRoute); const localId = routeId(state.currentRoute);
const routePackage = ns.LocalRoutes.get(localId); const routePackage = ns.LocalRoutes.get(localId);
if (!routePackage) throw new Error(ns.I18n.t("editor-ui.dein.entwurf.wurde.nicht.gefunden")); if (!routePackage) throw new Error(ns.I18n.t("editor.error.draft-missing"));
const serverRouteId = await createServerRouteFromLocal(routePackage); const serverRouteId = await createServerRouteFromLocal(routePackage);
const uploadedAt = new Date().toISOString(); const uploadedAt = new Date().toISOString();
@@ -982,7 +982,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.pois = saved.pois; state.pois = saved.pois;
fillRouteForm(state.currentRoute); fillRouteForm(state.currentRoute);
await loadRoutes(); await loadRoutes();
setStatus(ns.I18n.t("editor-ui.zur.prufung.gesendet")); setStatus(ns.I18n.t("editor.submit-success"));
} finally { } finally {
if (!state.currentRoute?.serverRouteId) setDisabled(uploadButton, false); if (!state.currentRoute?.serverRouteId) setDisabled(uploadButton, false);
} }
@@ -997,7 +997,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const lat = Number(position?.lat); const lat = Number(position?.lat);
const lon = Number(position?.lon); const lon = Number(position?.lon);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) { if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
setStatus(ns.I18n.t("editor-ui.fur.eine.neue.station.brauchen.wir.deinen"), true); setStatus(ns.I18n.t("editor.error.location-required"), true);
return; return;
} }
@@ -1010,20 +1010,20 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
lon, lon,
triggerRadiusM: Number.isFinite(accuracy) && accuracy > 0 ? accuracy : 25 triggerRadiusM: Number.isFinite(accuracy) && accuracy > 0 ? accuracy : 25
}); });
byId(ids.poiFormTitle).textContent = ns.I18n.t("editor-ui.station.hinzufugen"); byId(ids.poiFormTitle).textContent = ns.I18n.t("editor.add-station");
showPoiView(); showPoiView();
} }
function editPoi(poi) { function editPoi(poi) {
state.currentPoi = poi; state.currentPoi = poi;
fillPoiForm(poi); fillPoiForm(poi);
byId(ids.poiFormTitle).textContent = ns.I18n.t("editor-ui.station.bearbeiten"); byId(ids.poiFormTitle).textContent = ns.I18n.t("editor.edit-station");
showPoiView(); showPoiView();
} }
async function mediaFromLocalForm(existingPoi, poiIdentifier) { async function mediaFromLocalForm(existingPoi, poiIdentifier) {
const routeIdentifier = routeId(state.currentRoute); const routeIdentifier = routeId(state.currentRoute);
if (!routeIdentifier) throw new Error(ns.I18n.t("editor-ui.speichere.zuerst.die.strecke")); if (!routeIdentifier) throw new Error(ns.I18n.t("editor.error.save-route-first"));
const storedNow = []; const storedNow = [];
const replacedAfterSave = []; const replacedAfterSave = [];
@@ -1063,7 +1063,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function persistLocalCurrentRoute() { async function persistLocalCurrentRoute() {
const id = routeId(state.currentRoute); const id = routeId(state.currentRoute);
if (!id) throw new Error(ns.I18n.t("editor-ui.speichere.zuerst.die.strecke")); if (!id) throw new Error(ns.I18n.t("editor.error.save-route-first"));
const currentPoiId = poiId(state.currentPoi); const currentPoiId = poiId(state.currentPoi);
const saved = ns.LocalRoutes.save({ const saved = ns.LocalRoutes.save({
id, id,
@@ -1082,7 +1082,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function saveLocalPoi(values) { async function saveLocalPoi(values) {
if (!Number.isFinite(values.lat) || !Number.isFinite(values.lon)) { if (!Number.isFinite(values.lat) || !Number.isFinite(values.lon)) {
throw new TypeError(ns.I18n.t("editor-ui.der.ort.dieser.station.fehlt.erlaube.deinen")); throw new TypeError(ns.I18n.t("editor.error.station-location"));
} }
const currentId = numericPoiId(byId(ids.poiId).value); const currentId = numericPoiId(byId(ids.poiId).value);
const identifier = currentId ?? nextLocalPoiId(); const identifier = currentId ?? nextLocalPoiId();
@@ -1109,7 +1109,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
await Promise.allSettled(media.replacedAfterSave.map(ns.LocalRoutes.removeMedia)); await Promise.allSettled(media.replacedAfterSave.map(ns.LocalRoutes.removeMedia));
resetPendingMedia(); resetPendingMedia();
setStatus(ns.I18n.t("editor-ui.station.mit.bildern.und.ton.auf.diesem")); setStatus(ns.I18n.t("editor.station-saved-with-media"));
await loadRoute({ id: routeId(state.currentRoute), _source: 'local' }); await loadRoute({ id: routeId(state.currentRoute), _source: 'local' });
} }
@@ -1132,7 +1132,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function saveServerPoi(values) { async function saveServerPoi(values) {
const currentRouteId = routeId(state.currentRoute); const currentRouteId = routeId(state.currentRoute);
if (!currentRouteId) throw new Error(ns.I18n.t("editor-ui.speichere.zuerst.die.strecke")); if (!currentRouteId) throw new Error(ns.I18n.t("editor.error.save-route-first"));
const currentPoiId = byId(ids.poiId).value; const currentPoiId = byId(ids.poiId).value;
const path = currentPoiId const path = currentPoiId
? `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}` ? `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`
@@ -1142,13 +1142,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const saved = payload.poi || payload; const saved = payload.poi || payload;
const savedPoiId = poiId(saved) || currentPoiId; const savedPoiId = poiId(saved) || currentPoiId;
if (savedPoiId) await uploadPoiMedia(currentRouteId, savedPoiId); if (savedPoiId) await uploadPoiMedia(currentRouteId, savedPoiId);
setStatus(ns.I18n.t("editor-ui.station.gespeichert")); setStatus(ns.I18n.t("editor.station-saved"));
await loadRoute({ id: currentRouteId, _source: 'server' }); await loadRoute({ id: currentRouteId, _source: 'server' });
} }
async function savePoi() { async function savePoi() {
const values = poiFromForm(); const values = poiFromForm();
if (!values.title) throw new Error(ns.I18n.t("editor-ui.gib.deiner.station.einen.namen")); if (!values.title) throw new Error(ns.I18n.t("editor.error.station-name"));
if (state.currentSource === 'local') await saveLocalPoi(values); if (state.currentSource === 'local') await saveLocalPoi(values);
else await saveServerPoi(values); else await saveServerPoi(values);
} }
@@ -1196,13 +1196,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.selectedMediaKey = null; state.selectedMediaKey = null;
renderPoiMediaPreview(); renderPoiMediaPreview();
updatePoiMediaStatus(); updatePoiMediaStatus();
setStatus(kind === 'audio' ? ns.I18n.t("editor.audio.removed") : ns.I18n.t("editor-ui.bild.entfernt")); setStatus(kind === 'audio' ? ns.I18n.t("editor.audio.removed") : ns.I18n.t("editor.image-removed"));
} }
async function removeExistingServerMedia(kind, mediaId) { async function removeExistingServerMedia(kind, mediaId) {
const currentRouteId = routeId(state.currentRoute); const currentRouteId = routeId(state.currentRoute);
const currentPoiId = poiId(state.currentPoi); const currentPoiId = poiId(state.currentPoi);
if (!currentRouteId || !currentPoiId) throw new Error(ns.I18n.t("editor-ui.die.station.ist.noch.nicht.gespeichert")); if (!currentRouteId || !currentPoiId) throw new Error(ns.I18n.t("editor.error.station-unsaved"));
const basePath = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`; const basePath = `/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`;
const updated = { ...state.currentPoi }; const updated = { ...state.currentPoi };
@@ -1212,7 +1212,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
updated.audioUrl = null; updated.audioUrl = null;
updated.audio_path = null; updated.audio_path = null;
} else { } else {
if (!mediaId) throw new Error(ns.I18n.t("editor-ui.das.bild.konnte.nicht.entfernt.werden.lade")); if (!mediaId) throw new Error(ns.I18n.t("editor.error.remove-image"));
await sendDelete(`${basePath}/pictures/${encodeURIComponent(mediaId)}`); await sendDelete(`${basePath}/pictures/${encodeURIComponent(mediaId)}`);
updated.images = (state.currentPoi.images || []).filter(image => text(image.id) !== text(mediaId)); updated.images = (state.currentPoi.images || []).filter(image => text(image.id) !== text(mediaId));
} }
@@ -1221,7 +1221,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
state.selectedMediaKey = null; state.selectedMediaKey = null;
renderPoiMediaPreview(); renderPoiMediaPreview();
updatePoiMediaStatus(); updatePoiMediaStatus();
setStatus(kind === 'audio' ? ns.I18n.t("editor.audio.removed") : ns.I18n.t("editor-ui.bild.entfernt")); setStatus(kind === 'audio' ? ns.I18n.t("editor.audio.removed") : ns.I18n.t("editor.image-removed"));
} }
async function removeMedia(control) { async function removeMedia(control) {
@@ -1266,13 +1266,13 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await Promise.allSettled(removedMedia.map(ns.LocalRoutes.removeMedia)); await Promise.allSettled(removedMedia.map(ns.LocalRoutes.removeMedia));
renderPoiList(); renderPoiList();
showRouteView(); showRouteView();
setStatus(ns.I18n.t("editor-ui.station.entfernt.deine.strecke.wurde.gespeichert")); setStatus(ns.I18n.t("editor.station-removed-and-saved"));
return; return;
} }
await sendDelete(`/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`); await sendDelete(`/routes/${encodeURIComponent(currentRouteId)}/pois/${encodeURIComponent(currentPoiId)}`);
await loadRoute({ id: currentRouteId, _source: 'server' }); await loadRoute({ id: currentRouteId, _source: 'server' });
setStatus(ns.I18n.t("editor-ui.station.entfernt")); setStatus(ns.I18n.t("editor.station-removed"));
} }
function reorderPois(index, offset) { function reorderPois(index, offset) {
@@ -1295,7 +1295,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (!currentRouteId) return; if (!currentRouteId) return;
if (state.currentSource === 'local') { if (state.currentSource === 'local') {
await persistLocalCurrentRoute(); await persistLocalCurrentRoute();
setStatus(ns.I18n.t("editor-ui.reihenfolge.der.stationen.gespeichert")); setStatus(ns.I18n.t("editor.station-order-saved"));
return; return;
} }
const jobs = state.pois.map((poi, index) => { const jobs = state.pois.map((poi, index) => {
@@ -1340,7 +1340,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await loadRoutes(); await loadRoutes();
showListView(); showListView();
setStatus(ns.I18n.t("editor-ui.strecke.geloscht")); setStatus(ns.I18n.t("editor.route-deleted"));
} }
async function routeListAction(event) { async function routeListAction(event) {
@@ -1387,7 +1387,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
await ns.StorageUsage?.refresh(); await ns.StorageUsage?.refresh();
} catch (error) { } catch (error) {
ns.StorageUsage?.reportError(error); ns.StorageUsage?.reportError(error);
setStatus(ns.UserMessages?.error(error) || ns.I18n.t("editor-ui.das.hat.leider.nicht.geklappt.bitte.versuche"), true); setStatus(ns.UserMessages?.error(error) || ns.I18n.t("editor.error.generic"), true);
} }
} }
+1 -1
View File
@@ -25,7 +25,7 @@
} }
function unsupportedError() { function unsupportedError() {
return new Error(ns.I18n.t("geolocation.dieses.gerat.kann.deinen.standort.nicht.finden")); return new Error(ns.I18n.t("location.error.unsupported"));
} }
ns.Geo = { ns.Geo = {
+9 -9
View File
@@ -4,15 +4,15 @@ globalThis.Wegwichtel ??= {};
'use strict'; 'use strict';
const entries = Object.freeze([ const entries = Object.freeze([
['🪵', "group-symbols.holz"], ['🪵', "symbols.wood"],
['🪨', "group-symbols.felsen"], ['🪨', "symbols.rock"],
['🏮', "group-symbols.laterne"], ['🏮', "symbols.lantern"],
['🌼', "group-symbols.blume"], ['🌼', "symbols.flower"],
['🚬', "group-symbols.pfeife"], ['🚬', "symbols.pipe"],
['🐦', "group-symbols.vogel"], ['🐦', "symbols.bird"],
['🍄', "group-symbols.pilz"], ['🍄', "symbols.mushroom"],
['🍓', "group-symbols.erdbeere"], ['🍓', "symbols.strawberry"],
['🎃', "group-symbols.kurbis"] ['🎃', "symbols.pumpkin"]
].map(([unicode, labelKey]) => Object.freeze({ ].map(([unicode, labelKey]) => Object.freeze({
unicode, unicode,
get label() { return ns.I18n.t(labelKey); }, get label() { return ns.I18n.t(labelKey); },
+31 -9
View File
@@ -3,11 +3,22 @@ globalThis.Wegwichtel ??= {};
'use strict'; 'use strict';
class WegwichtelI18n { class WegwichtelI18n {
static #languages = new Map([['de', ns.TextsDe.messages]]); static #languages = new Map(ns.TextCatalogs);
static #language = 'de'; static #fallbackLanguage = this.#languages.keys().next().value;
static #language = this.#fallbackLanguage;
static get language() { return this.#language; } static get language() { return this.#language; }
static detectLanguage(preferences = globalThis.navigator?.languages ?? [globalThis.navigator?.language]) {
for (const language of preferences) {
const requested = String(language ?? '').toLowerCase();
if (this.#languages.has(requested)) return requested;
const base = requested.split('-')[0];
if (this.#languages.has(base)) return base;
}
return this.#fallbackLanguage;
}
static register(language, messages) { static register(language, messages) {
this.#languages.set(language.toLowerCase(), Object.freeze({ ...messages })); this.#languages.set(language.toLowerCase(), Object.freeze({ ...messages }));
} }
@@ -15,7 +26,7 @@ globalThis.Wegwichtel ??= {};
static setLanguage(language, root = globalThis.document) { static setLanguage(language, root = globalThis.document) {
const requested = String(language).toLowerCase(); const requested = String(language).toLowerCase();
const base = requested.split('-')[0]; const base = requested.split('-')[0];
this.#language = 'de'; this.#language = this.#fallbackLanguage;
if (this.#languages.has(base)) this.#language = base; if (this.#languages.has(base)) this.#language = base;
if (this.#languages.has(requested)) this.#language = requested; if (this.#languages.has(requested)) this.#language = requested;
this.apply(root); this.apply(root);
@@ -23,32 +34,43 @@ globalThis.Wegwichtel ??= {};
} }
static t(key, variables = {}) { static t(key, variables = {}) {
let value = this.#languages.get(this.#language)?.[key] ?? ns.TextsDe.messages[key]; let value = this.#lookup(this.#languages.get(this.#language), key)
?? this.#lookup(this.#languages.get(this.#fallbackLanguage), key);
if (value == null) throw new RangeError(`Missing translation: ${key}`); if (value == null) throw new RangeError(`Missing translation: ${key}`);
if (typeof value === 'object') { if (typeof value === 'object') {
const plural = new Intl.PluralRules(this.#language).select(Number(variables.count)); const plural = new Intl.PluralRules(this.#language).select(Number(variables.count));
value = value[plural] ?? value.other; value = value[plural] ?? value.other;
} }
return value.replace(/\{([a-zA-Z0-9_]+)\}/g, (placeholder, name) => { return value.replaceAll(/\{(\w+)\}/g, (placeholder, name) => {
if (!Object.hasOwn(variables, name)) return placeholder; if (!Object.hasOwn(variables, name)) return placeholder;
return String(variables[name] ?? ''); return String(variables[name] ?? '');
}); });
} }
static #lookup(catalog, key) {
if (Object.hasOwn(catalog, key)) return catalog[key];
let value = catalog;
for (const part of key.split('.')) {
if (!value || !Object.hasOwn(value, part)) return undefined;
value = value[part];
}
return value;
}
static apply(root = globalThis.document) { static apply(root = globalThis.document) {
if (!root?.querySelectorAll) return; if (!root?.querySelectorAll) return;
if (root.documentElement) root.documentElement.lang = this.#language; if (root.documentElement) root.documentElement.lang = this.#language;
for (const element of root.querySelectorAll('[data-i18n]')) { for (const element of root.querySelectorAll('[data-i18n]')) {
if (typeof element.getAttribute === 'function') element.textContent = this.t(element.getAttribute('data-i18n')); if (element.dataset?.i18n) element.textContent = this.t(element.dataset.i18n);
} }
for (const attribute of ['title', 'aria-label', 'placeholder', 'alt']) { for (const [attribute, key] of [['title', 'i18nTitle'], ['aria-label', 'i18nAriaLabel'], ['placeholder', 'i18nPlaceholder'], ['alt', 'i18nAlt']]) {
for (const element of root.querySelectorAll(`[data-i18n-${attribute}]`)) { 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}`))); if (element.dataset?.[key] && typeof element.setAttribute === 'function') element.setAttribute(attribute, this.t(element.dataset[key]));
} }
} }
} }
} }
ns.I18n = WegwichtelI18n; ns.I18n = WegwichtelI18n;
WegwichtelI18n.apply(); WegwichtelI18n.setLanguage(WegwichtelI18n.detectLanguage());
}(globalThis.Wegwichtel)); }(globalThis.Wegwichtel));
+16 -16
View File
@@ -5,14 +5,14 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const DROPDOWN_ANIMATION_MS = 180; const DROPDOWN_ANIMATION_MS = 180;
const pageTitles = Object.freeze({ const pageTitles = Object.freeze({
get privacy() { return ns.I18n.t("legal-ui.datenschutzerklarung"); }, get privacy() { return ns.I18n.t("info.privacy"); },
get imprint() { return ns.I18n.t("legal-ui.impressum"); }, get imprint() { return ns.I18n.t("info.imprint"); },
get technology() { return ns.I18n.t("legal-ui.speichern.und.berechtigungen"); } get technology() { return ns.I18n.t("info.storage"); }
}); });
function menuEntries() { return [ function menuEntries() { return [
['privacy', ns.I18n.t("legal-ui.datenschutzerklarung")], ['privacy', ns.I18n.t("info.privacy")],
['imprint', ns.I18n.t("legal-ui.impressum")], ['imprint', ns.I18n.t("info.imprint")],
['technology', ns.I18n.t("legal-ui.speichern.und.berechtigungen")] ['technology', ns.I18n.t("info.storage")]
]; } ]; }
const selectors = Object.freeze({ const selectors = Object.freeze({
content: '.info-dialog-content', content: '.info-dialog-content',
@@ -159,8 +159,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
id: 'info-button', id: 'info-button',
className: 'info-button', className: 'info-button',
type: 'button', type: 'button',
'aria-label': ns.I18n.t("legal-ui.hilfe.und.informationen.offnen"), 'aria-label': ns.I18n.t("info.open"),
title: ns.I18n.t("legal-ui.informationen") title: ns.I18n.t("info.title")
}); });
append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i')); append(infoButton, createElement('span', { 'aria-hidden': 'true' }, 'i'));
append(control, infoButton); append(control, infoButton);
@@ -173,7 +173,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const dropdown = createElement('nav', { const dropdown = createElement('nav', {
id: 'info-dropdown', id: 'info-dropdown',
className: 'info-dropdown', className: 'info-dropdown',
'aria-label': ns.I18n.t("legal-ui.rechtliche.und.technische.informationen") 'aria-label': ns.I18n.t("info.menu-label")
}); });
append(control, dropdown); append(control, dropdown);
return dropdown; return dropdown;
@@ -191,7 +191,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function setInfoButtonLabel(infoButton) { function setInfoButtonLabel(infoButton) {
if (typeof infoButton.getAttribute === 'function' && infoButton.getAttribute('aria-label')) return; if (typeof infoButton.getAttribute === 'function' && infoButton.getAttribute('aria-label')) return;
setAttribute(infoButton, 'aria-label', ns.I18n.t("legal-ui.hilfe.und.informationen.offnen")); setAttribute(infoButton, 'aria-label', ns.I18n.t("info.open"));
} }
function ensureInfoButtonIcon(infoButton) { function ensureInfoButtonIcon(infoButton) {
@@ -201,7 +201,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function configureDropdown(dropdown) { function configureDropdown(dropdown) {
if (!dropdown) return; if (!dropdown) return;
setAttribute(dropdown, 'aria-label', ns.I18n.t("legal-ui.rechtliche.und.technische.informationen")); setAttribute(dropdown, 'aria-label', ns.I18n.t("info.menu-label"));
setHidden(dropdown, true); setHidden(dropdown, true);
menuEntries().forEach(([target, label]) => ensureMenuButton(dropdown, target, label)); menuEntries().forEach(([target, label]) => ensureMenuButton(dropdown, target, label));
} }
@@ -277,7 +277,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function configureBackButton(backButton) { function configureBackButton(backButton) {
setAttribute(backButton, 'type', 'button'); setAttribute(backButton, 'type', 'button');
setAttribute(backButton, 'aria-label', ns.I18n.t("legal-ui.zuruck.zum.informationsmenu")); setAttribute(backButton, 'aria-label', ns.I18n.t("info.back"));
setHidden(backButton, true); setHidden(backButton, true);
return backButton; return backButton;
} }
@@ -293,8 +293,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const closeButton = createElement('button', { const closeButton = createElement('button', {
id: 'info-close', id: 'info-close',
type: 'button', type: 'button',
'aria-label': ns.I18n.t("legal-ui.informationsfenster.schliessen"), 'aria-label': ns.I18n.t("info.close-label"),
title: ns.I18n.t("legal-ui.schliessen") title: ns.I18n.t("info.close")
}, '×'); }, '×');
append(header, closeButton); append(header, closeButton);
return configureCloseButton(closeButton); return configureCloseButton(closeButton);
@@ -302,7 +302,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function configureCloseButton(closeButton) { function configureCloseButton(closeButton) {
setAttribute(closeButton, 'type', 'button'); setAttribute(closeButton, 'type', 'button');
setAttribute(closeButton, 'aria-label', ns.I18n.t("legal-ui.informationsfenster.schliessen")); setAttribute(closeButton, 'aria-label', ns.I18n.t("info.close-label"));
if (closeButton && !String(closeButton.textContent ?? '').trim()) closeButton.textContent = '×'; if (closeButton && !String(closeButton.textContent ?? '').trim()) closeButton.textContent = '×';
return closeButton; return closeButton;
} }
@@ -436,7 +436,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
function updateDialogTitle(pageName) { function updateDialogTitle(pageName) {
if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? ns.I18n.t("legal-ui.informationen"); if (elements?.title) elements.title.textContent = pageTitles[pageName] ?? ns.I18n.t("info.title");
} }
function focusSafely(element) { function focusSafely(element) {
+29 -29
View File
@@ -15,7 +15,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function storage() { function storage() {
const target = globalThis.localStorage; const target = globalThis.localStorage;
if (!target) throw new Error(ns.I18n.t("local-routes.deine.strecke.kann.auf.diesem.gerat.gerade")); if (!target) throw new Error(ns.I18n.t("local.error.route-storage"));
return target; return target;
} }
@@ -27,7 +27,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (mediaDatabasePromise) return mediaDatabasePromise; if (mediaDatabasePromise) return mediaDatabasePromise;
const indexedDb = globalThis.indexedDB; const indexedDb = globalThis.indexedDB;
if (!indexedDb || typeof indexedDb.open !== 'function') { if (!indexedDb || typeof indexedDb.open !== 'function') {
return Promise.reject(new Error(ns.I18n.t("local-routes.bilder.und.ton.konnen.auf.diesem.gerat"))); return Promise.reject(new Error(ns.I18n.t("local.error.media-storage")));
} }
mediaDatabasePromise = new Promise((resolve, reject) => { mediaDatabasePromise = new Promise((resolve, reject) => {
@@ -41,11 +41,11 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
request.onsuccess = () => resolve(request.result); request.onsuccess = () => resolve(request.result);
request.onerror = () => { request.onerror = () => {
mediaDatabasePromise = null; mediaDatabasePromise = null;
reject(request.error || new Error(ns.I18n.t("local-routes.gespeicherte.bilder.und.ton.konnten.nicht.geoffnet"))); reject(request.error || new Error(ns.I18n.t("local.error.media-read")));
}; };
request.onblocked = () => { request.onblocked = () => {
mediaDatabasePromise = null; mediaDatabasePromise = null;
reject(new Error(ns.I18n.t("local-routes.schliesse.andere.fenster.mit.wegwichtel.und.versuche"))); reject(new Error(ns.I18n.t("local.error.other-tabs")));
}; };
}); });
return mediaDatabasePromise; return mediaDatabasePromise;
@@ -59,8 +59,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const transaction = database.transaction(MEDIA_STORE_NAME, mode); const transaction = database.transaction(MEDIA_STORE_NAME, mode);
request = operation(transaction.objectStore(MEDIA_STORE_NAME)); request = operation(transaction.objectStore(MEDIA_STORE_NAME));
request.onsuccess = () => resolve(request.result ?? null); request.onsuccess = () => resolve(request.result ?? null);
request.onerror = () => reject(request.error || new Error(ns.I18n.t("local-routes.bilder.und.ton.konnten.nicht.geoffnet.oder"))); request.onerror = () => reject(request.error || new Error(ns.I18n.t("local.error.media-access")));
transaction.onabort = () => reject(transaction.error || new Error(ns.I18n.t("local-routes.das.speichern.von.bildern.und.ton.wurde"))); transaction.onabort = () => reject(transaction.error || new Error(ns.I18n.t("local.error.media-cancelled")));
} catch (error) { } catch (error) {
reject(error); reject(error);
} }
@@ -100,7 +100,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
}; };
} catch (error) { } catch (error) {
if (error?.name === 'QuotaExceededError') { if (error?.name === 'QuotaExceededError') {
throw new Error(ns.I18n.t("local-routes.der.platz.fur.bilder.und.ton.ist"), { cause: error }); throw new Error(ns.I18n.t("local.error.media-quota-hint"), { cause: error });
} }
// Manche Browser können FileSystemFileHandle noch nicht in IndexedDB serialisieren. // Manche Browser können FileSystemFileHandle noch nicht in IndexedDB serialisieren.
// In diesem Fall wird die Datei als Blob in IndexedDB gespeichert. // 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 }); await putMediaRecord({ ...metadata, storage: 'indexeddb', blob: file });
} catch (error) { } catch (error) {
const prefix = error?.name === 'QuotaExceededError' const prefix = error?.name === 'QuotaExceededError'
? ns.I18n.t("local-routes.der.platz.fur.bilder.und.ton.ist.2") ? ns.I18n.t("local.error.media-quota")
: ns.I18n.t("local-routes.das.bild.oder.die.tonaufnahme.konnte.nicht"); : ns.I18n.t("local.error.media-save");
throw new Error(ns.I18n.t("local-routes.entferne.bilder.tonaufnahmen.oder.eigene.strecken.die", { value1: prefix }), { cause: error }); throw new Error(ns.I18n.t("local.error.media-cleanup-hint", { value1: prefix }), { cause: error });
} }
return { return {
id: String(mediaId), id: String(mediaId),
@@ -135,12 +135,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
permission = await handle.requestPermission({ mode: 'read' }); permission = await handle.requestPermission({ mode: 'read' });
} }
if (permission !== 'granted') { if (permission !== 'granted') {
throw new Error(ns.I18n.t("local-routes.erlaube.noch.einmal.den.zugriff.auf.das")); throw new Error(ns.I18n.t("local.error.file-permission"));
} }
try { try {
return await handle.getFile(); return await handle.getFile();
} catch (error) { } catch (error) {
throw new Error(ns.I18n.t("local-routes.ein.ausgewahltes.bild.oder.eine.tonaufnahme.wurde"), { cause: error }); throw new Error(ns.I18n.t("local.error.file-missing"), { cause: error });
} }
} }
@@ -238,7 +238,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
if (typeof cryptoApi?.getRandomValues === 'function') { if (typeof cryptoApi?.getRandomValues === 'function') {
return `local-${uuidFromRandomValues(cryptoApi)}`; return `local-${uuidFromRandomValues(cryptoApi)}`;
} }
throw new Error(ns.I18n.t("local-routes.auf.diesem.gerat.kann.gerade.keine.neue")); throw new Error(ns.I18n.t("local.error.create-route"));
} }
function createPoiId() { function createPoiId() {
@@ -255,7 +255,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
.replaceAll("'", '&apos;'); .replaceAll("'", '&apos;');
} }
function emptyGpx(name = ns.I18n.t("editor-ui.neue.strecke")) { function emptyGpx(name = ns.I18n.t("editor.new-route")) {
const safeName = escapeXml(name); const safeName = escapeXml(name);
return `<?xml version="1.0" encoding="UTF-8"?>\n` return `<?xml version="1.0" encoding="UTF-8"?>\n`
+ `<gpx version="1.1" creator="Wegwichtel" xmlns="${GPX_NAMESPACE}" xmlns:wegwichtel="${POI_NAMESPACE}">\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 parser = new DOMParser();
const document = parser.parseFromString(String(gpx || ''), 'application/xml'); const document = parser.parseFromString(String(gpx || ''), 'application/xml');
if (document.querySelector('parsererror') || document.documentElement?.localName !== 'gpx') { if (document.querySelector('parsererror') || document.documentElement?.localName !== 'gpx') {
throw new Error(ns.I18n.t("local-routes.diese.datei.enthalt.keinen.lesbaren.weg.wahle")); throw new Error(ns.I18n.t("local.error.invalid-track"));
} }
return document; return document;
} }
@@ -300,7 +300,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const document = parseXml(gpx); const document = parseXml(gpx);
return Array.from(document.getElementsByTagNameNS('*', 'wpt')).map((node, index) => ({ return Array.from(document.getElementsByTagNameNS('*', 'wpt')).map((node, index) => ({
id: waypointId(node), id: waypointId(node),
title: textOfChild(node, 'name') || ns.I18n.t("editor-ui.station", { value1: index + 1 }), title: textOfChild(node, 'name') || ns.I18n.t("editor.station-number", { value1: index + 1 }),
description: textOfChild(node, 'desc'), description: textOfChild(node, 'desc'),
lat: numberAttribute(node, 'lat'), lat: numberAttribute(node, 'lat'),
lon: numberAttribute(node, 'lon'), lon: numberAttribute(node, 'lon'),
@@ -360,7 +360,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
function syncGpx(gpx, route, pois) { function syncGpx(gpx, route, pois) {
const routeName = route?.name || ns.I18n.t("editor-ui.neue.strecke"); const routeName = route?.name || ns.I18n.t("editor.new-route");
const document = parseXml(gpx || emptyGpx(routeName)); const document = parseXml(gpx || emptyGpx(routeName));
updateMetadataName(document, routeName); updateMetadataName(document, routeName);
@@ -374,9 +374,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const segment = ensureTrackSegment(document, routeName); const segment = ensureTrackSegment(document, routeName);
const orderedPois = [...(pois || [])].sort((left, right) => Number(left.sequence) - Number(right.sequence)); const orderedPois = [...(pois || [])].sort((left, right) => Number(left.sequence) - Number(right.sequence));
orderedPois.forEach((poi, index) => { orderedPois.forEach((poi, index) => {
const title = poi.title || ns.I18n.t("editor-ui.station", { value1: index + 1 }); const title = poi.title || ns.I18n.t("editor.station-number", { value1: index + 1 });
if (!Number.isFinite(Number(poi.lat)) || !Number.isFinite(Number(poi.lon))) { if (!Number.isFinite(Number(poi.lat)) || !Number.isFinite(Number(poi.lon))) {
throw new TypeError(ns.I18n.t("local-routes.der.ort.von.fehlt", { value1: title })); throw new TypeError(ns.I18n.t("local.error.station-location", { value1: title }));
} }
const gpxPoint = createElement(document, 'trkpt'); const gpxPoint = createElement(document, 'trkpt');
@@ -469,9 +469,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
ns.StorageUsage?.reportError(error); ns.StorageUsage?.reportError(error);
const name = error?.name === 'QuotaExceededError' const name = error?.name === 'QuotaExceededError'
? ns.I18n.t("local-routes.der.platz.fur.deine.strecken.ist.voll") ? ns.I18n.t("local.error.route-quota")
: ns.I18n.t("local-routes.deine.strecke.konnte.nicht.gespeichert.werden"); : ns.I18n.t("local.error.save-route");
throw new Error(ns.I18n.t("local-routes.entferne.eigene.strecken.die.du.nicht.mehr", { value1: name }), { cause: error }); throw new Error(ns.I18n.t("local.error.route-cleanup-hint", { value1: name }), { cause: error });
} }
notify(id, 'saved'); notify(id, 'saved');
@@ -514,9 +514,9 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
} }
function gpxForUpload(routePackage) { function gpxForUpload(routePackage) {
if (!routePackage?.gpx) throw new Error(ns.I18n.t("local-routes.der.weg.deiner.strecke.fehlt")); if (!routePackage?.gpx) throw new Error(ns.I18n.t("local.error.path-missing"));
if (!routePoints(routePackage.gpx).length) { if (!routePoints(routePackage.gpx).length) {
throw new Error(ns.I18n.t("local-routes.deine.strecke.hat.noch.keinen.aufgezeichneten.weg")); throw new Error(ns.I18n.t("local.error.path-empty"));
} }
return routePackage.gpx; return routePackage.gpx;
} }
@@ -577,7 +577,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function appPoi(poi) { function appPoi(poi) {
return { return {
id: Number(poi.id), id: Number(poi.id),
title: poi.title || ns.I18n.t("editor-ui.station.ohne.namen"), title: poi.title || ns.I18n.t("editor.untitled-station"),
description: poi.description || '', description: poi.description || '',
lat: Number(poi.lat), lat: Number(poi.lat),
lon: Number(poi.lon), lon: Number(poi.lon),
@@ -604,7 +604,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
id: String(routePackage.id), id: String(routePackage.id),
source: 'local', source: 'local',
local: true, local: true,
name: routePackage.route?.name || ns.I18n.t("editor-ui.unbenannte.strecke"), name: routePackage.route?.name || ns.I18n.t("editor.untitled-route"),
description: routePackage.route?.description || '', description: routePackage.route?.description || '',
schoolName: ns.I18n.t("local.device"), schoolName: ns.I18n.t("local.device"),
status: 'local', status: 'local',
@@ -625,7 +625,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
async function getAppRoute(id, options = {}) { async function getAppRoute(id, options = {}) {
const routePackage = await withMedia(readPackage(id), options); const routePackage = await withMedia(readPackage(id), options);
if (!routePackage) throw new Error(ns.I18n.t("local-routes.deine.strecke.wurde.nicht.gefunden")); if (!routePackage) throw new Error(ns.I18n.t("local.error.route-missing"));
return toAppRoute(routePackage); return toAppRoute(routePackage);
} }
@@ -636,7 +636,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
function readFileAsText(file) { function readFileAsText(file) {
if (!file) return Promise.resolve(''); if (!file) return Promise.resolve('');
if (typeof file.text !== 'function') { if (typeof file.text !== 'function') {
return Promise.reject(new TypeError(ns.I18n.t("local-routes.diese.datei.kann.auf.deinem.gerat.nicht"))); return Promise.reject(new TypeError(ns.I18n.t("local.error.file-unsupported")));
} }
return file.text(); return file.text();
} }
@@ -647,7 +647,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {};
const reader = new FileReader(); const reader = new FileReader();
reader.addEventListener('load', () => { reader.addEventListener('load', () => {
if (typeof reader.result !== 'string') { if (typeof reader.result !== 'string') {
reject(new TypeError(ns.I18n.t("local-routes.diese.datei.konnte.nicht.geoffnet.werden.wahle"))); reject(new TypeError(ns.I18n.t("local.error.file-read")));
return; return;
} }
resolve({ resolve({
+25 -25
View File
@@ -44,7 +44,7 @@ globalThis.Wegwichtel ??= {};
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const timeout = setTimeout(() => { const timeout = setTimeout(() => {
navigator.serviceWorker.removeEventListener('controllerchange', changed); navigator.serviceWorker.removeEventListener('controllerchange', changed);
reject(new Error(ns.I18n.t("offline-cache.das.speichern.fur.unterwegs.hat.nicht.geklappt"))); reject(new Error(ns.I18n.t("offline.error.download")));
}, 10000); }, 10000);
function changed() { function changed() {
clearTimeout(timeout); clearTimeout(timeout);
@@ -64,32 +64,32 @@ globalThis.Wegwichtel ??= {};
let stage = null; let stage = null;
try { try {
await worker(); await worker();
announce(ns.I18n.t("offline-cache.wegwichtel.wird.fur.unterwegs.vorbereitet")); announce(ns.I18n.t("offline.preparing"));
// Persistent storage is best-effort; a denial must not block downloads. // Persistent storage is best-effort; a denial must not block downloads.
await navigator.storage?.persist?.().catch(() => false); await navigator.storage?.persist?.().catch(() => false);
const options = { cache: 'no-store', headers: { 'X-Wegwichtel-Offline-Download': 'true' }, signal }; const options = { cache: 'no-store', headers: { 'X-Wegwichtel-Offline-Download': 'true' }, signal };
const manifestResponse = await fetch('/api/offline-manifest', options); const manifestResponse = await fetch('/api/offline-manifest', options);
if (!manifestResponse.ok) throw new Error(ns.I18n.t("offline-cache.wegwichtel.konnte.nicht.fur.unterwegs.geladen.werden")); if (!manifestResponse.ok) throw new Error(ns.I18n.t("offline.error.connection"));
const manifest = await manifestResponse.json(); const manifest = await manifestResponse.json();
if (!Array.isArray(manifest.resources) || !manifest.resources.length) throw new TypeError(ns.I18n.t("offline-cache.wegwichtel.konnte.nicht.fur.unterwegs.geladen.werden.2")); if (!Array.isArray(manifest.resources) || !manifest.resources.length) throw new TypeError(ns.I18n.t("offline.error.prepare"));
stage = await ns.OfflineStore.create(); stage = await ns.OfflineStore.create();
const urls = [...new Set(manifest.resources.map(url => ns.OfflineStore.key(url)))]; const urls = [...new Set(manifest.resources.map(url => ns.OfflineStore.key(url)))];
for (const [index, url] of urls.entries()) { for (const [index, url] of urls.entries()) {
signal.throwIfAborted(); signal.throwIfAborted();
announce(ns.I18n.t("offline-cache.fur.unterwegs.speichern.von", { value1: index + 1, value2: urls.length })); announce(ns.I18n.t("offline.download-progress", { value1: index + 1, value2: urls.length }));
const response = await fetch(url, options); const response = await fetch(url, options);
await ns.OfflineStore.put(stage, url, response); await ns.OfflineStore.put(stage, url, response);
} }
signal.throwIfAborted(); signal.throwIfAborted();
const saved = await ns.OfflineStore.commit(stage); const saved = await ns.OfflineStore.commit(stage);
announce(ns.I18n.t("offline-cache.wegwichtel.ist.fur.unterwegs.bereit")); announce(ns.I18n.t("offline.ready"));
return saved; return saved;
} catch (error) { } catch (error) {
if (stage) await ns.OfflineStore.remove(stage.id); if (stage) await ns.OfflineStore.remove(stage.id);
ns.StorageUsage?.reportError(error); ns.StorageUsage?.reportError(error);
const saved = await ns.OfflineStore.state(); const saved = await ns.OfflineStore.state();
if (error.name === 'AbortError') announce(ns.I18n.t("offline-cache.speichern.abgebrochen")); if (error.name === 'AbortError') announce(ns.I18n.t("offline.cancelled"));
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")}`); else announce(`${ns.UserMessages?.error(error) || ns.I18n.t("offline.error.save")} ${saved ? ns.I18n.t("offline.previous.kept") : ns.I18n.t("offline.not-ready")}`);
throw error; throw error;
} finally { } finally {
task = null; task = null;
@@ -109,7 +109,7 @@ globalThis.Wegwichtel ??= {};
if (task) await task.catch(() => {}); if (task) await task.catch(() => {});
await ns.OfflineStore.clear(); await ns.OfflineStore.clear();
await updateControls(); await updateControls();
announce(ns.I18n.t("offline-cache.die.gespeicherte.app.wurde.entfernt.deine.strecken")); announce(ns.I18n.t("offline.app-removed"));
await ns.StorageUsage?.refresh(); await ns.StorageUsage?.refresh();
} }
@@ -126,12 +126,12 @@ globalThis.Wegwichtel ??= {};
const message = document.getElementById('route-offline-status'); const message = document.getElementById('route-offline-status');
if (save) { if (save) {
save.disabled = !supported() || Boolean(routeTask); save.disabled = !supported() || Boolean(routeTask);
save.textContent = saved ? ns.I18n.t("offline-cache.gespeicherte.strecke.erneuern") : ns.I18n.t("offline-cache.fur.unterwegs.speichern"); save.textContent = saved ? ns.I18n.t("offline.renew-route") : ns.I18n.t("offline.save-route");
} }
if (remove) { remove.hidden = !saved; remove.disabled = Boolean(routeTask); } if (remove) { remove.hidden = !saved; remove.disabled = Boolean(routeTask); }
if (message && !routeTask) message.textContent = saved if (message && !routeTask) message.textContent = saved
? ns.I18n.t("offline-cache.diese.strecke.ist.fur.unterwegs.bereit") ? ns.I18n.t("offline.route-ready")
: ns.I18n.t("offline-cache.nimm.diese.strecke.mit.bildern.und.tonaufnahmen"); : ns.I18n.t("offline.route-hint");
updateGpxDownload(route); updateGpxDownload(route);
} }
@@ -147,9 +147,9 @@ globalThis.Wegwichtel ??= {};
} }
async function saveRoute(route) { async function saveRoute(route) {
if (routeTask) throw new Error(ns.I18n.t("offline-cache.bitte.warte.bis.die.andere.strecke.gespeichert")); if (routeTask) throw new Error(ns.I18n.t("offline.error.busy"));
if (!supported()) throw new Error(ns.I18n.t("offline-cache.auf.diesem.gerat.kannst.du.strecken.gerade")); if (!supported()) throw new Error(ns.I18n.t("offline.error.route-unsupported"));
if (!route || route.source === 'local') throw new Error(ns.I18n.t("offline-cache.wahle.zuerst.eine.strecke.die.fur.alle")); if (!route || route.source === 'local') throw new Error(ns.I18n.t("offline.error.select-route"));
routeTask = (async () => { routeTask = (async () => {
let stage = null; let stage = null;
const message = document.getElementById('route-offline-status'); const message = document.getElementById('route-offline-status');
@@ -159,10 +159,10 @@ globalThis.Wegwichtel ??= {};
const url = `/api/routes/${encodeURIComponent(route.id)}`; const url = `/api/routes/${encodeURIComponent(route.id)}`;
const options = { cache: 'no-store', headers: { 'X-Wegwichtel-Offline-Download': 'true' } }; const options = { cache: 'no-store', headers: { 'X-Wegwichtel-Offline-Download': 'true' } };
const response = await fetch(url, options); const response = await fetch(url, options);
if (!response.ok) throw new Error(ns.I18n.t("offline-cache.die.strecke.konnte.nicht.geladen.werden")); if (!response.ok) throw new Error(ns.I18n.t("offline.error.load-route"));
const fresh = await response.clone().json(); const fresh = await response.clone().json();
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 (String(fresh.id) !== String(route.id) || !fresh.activatedAt) throw new Error(ns.I18n.t("offline.error.published-only"));
if (!fresh.gpxUrl) throw new Error(ns.I18n.t("offline-cache.der.weg.dieser.strecke.fehlt.sie.kann")); if (!fresh.gpxUrl) throw new Error(ns.I18n.t("offline.error.path-missing"));
const resources = [fresh.gpxUrl]; const resources = [fresh.gpxUrl];
for (const poi of fresh.pois || []) { for (const poi of fresh.pois || []) {
if (poi.audioUrl) resources.push(poi.audioUrl); if (poi.audioUrl) resources.push(poi.audioUrl);
@@ -172,7 +172,7 @@ globalThis.Wegwichtel ??= {};
stage = await ns.OfflineStore.create(); stage = await ns.OfflineStore.create();
await ns.OfflineStore.put(stage, url, response); await ns.OfflineStore.put(stage, url, response);
for (const [index, resource] of urls.entries()) { for (const [index, resource] of urls.entries()) {
if (message && selectedRoute === route) message.textContent = ns.I18n.t("offline-cache.strecke.speichern.von", { value1: index + 1, value2: urls.length }); if (message && selectedRoute === route) message.textContent = ns.I18n.t("offline.route-progress", { value1: index + 1, value2: urls.length });
await ns.OfflineStore.put(stage, resource, await fetch(resource, options)); await ns.OfflineStore.put(stage, resource, await fetch(resource, options));
await ns.StorageUsage?.refresh(); await ns.StorageUsage?.refresh();
} }
@@ -193,7 +193,7 @@ globalThis.Wegwichtel ??= {};
} }
async function removeRoute(route) { async function removeRoute(route) {
if (routeTask) throw new Error(ns.I18n.t("offline-cache.bitte.warte.bis.das.speichern.fertig.ist")); if (routeTask) throw new Error(ns.I18n.t("offline.error.wait"));
await ns.OfflineStore.clearRoute(route.id); await ns.OfflineStore.clearRoute(route.id);
await renderRoute(selectedRoute); await renderRoute(selectedRoute);
await ns.StorageUsage?.refresh(); await ns.StorageUsage?.refresh();
@@ -206,7 +206,7 @@ globalThis.Wegwichtel ??= {};
return action(route).catch(error => { return action(route).catch(error => {
if (selectedRoute !== route) return; if (selectedRoute !== route) return;
const message = document.getElementById('route-offline-status'); const message = document.getElementById('route-offline-status');
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") }); if (message) message.textContent = ns.I18n.t("offline.error.previous.kept", { value1: ns.UserMessages?.error(error) || ns.I18n.t("offline.error.save") });
}); });
}; };
document.getElementById('route-offline-save')?.addEventListener('click', () => run(saveRoute)); document.getElementById('route-offline-save')?.addEventListener('click', () => run(saveRoute));
@@ -226,7 +226,7 @@ globalThis.Wegwichtel ??= {};
} }
if (!supported()) { if (!supported()) {
if (toggle) toggle.disabled = true; if (toggle) toggle.disabled = true;
announce(ns.I18n.t("offline-cache.auf.diesem.gerat.lasst.sich.wegwichtel.gerade")); announce(ns.I18n.t("offline.error.unsupported"));
return; return;
} }
// Remove old v1 snapshots that also included all routes automatically. // 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 ((await ns.OfflineStore.routes()).length) await worker();
if (saved) { if (saved) {
await worker(); await worker();
announce(ns.I18n.t("offline-cache.wegwichtel.ist.fur.unterwegs.bereit")); announce(ns.I18n.t("offline.ready"));
} else { } else {
announce(ns.I18n.t("offline-cache.speichere.zuerst.die.app.danach.kannst.du")); announce(ns.I18n.t("offline.error.save-app-first"));
} }
toggle?.addEventListener('change', () => { toggle?.addEventListener('change', () => {
toggle.setAttribute('aria-checked', String(toggle.checked)); toggle.setAttribute('aria-checked', String(toggle.checked));
@@ -249,7 +249,7 @@ globalThis.Wegwichtel ??= {};
ns.OfflineCache = Object.freeze({ ns.OfflineCache = Object.freeze({
initialize() { initialize() {
initialized ??= setup().catch(error => { announce(ns.UserMessages?.error(error) || ns.I18n.t("offline-cache.das.speichern.fur.unterwegs.hat.nicht.geklappt")); }); initialized ??= setup().catch(error => { announce(ns.UserMessages?.error(error) || ns.I18n.t("offline.error.download")); });
return initialized; return initialized;
}, },
download, download,
+2 -2
View File
@@ -10,7 +10,7 @@ globalThis.Wegwichtel ??= {};
function key(value) { function key(value) {
const url = new URL(value, globalThis.location.origin); const url = new URL(value, globalThis.location.origin);
if (url.origin !== globalThis.location.origin) throw new TypeError(ns.I18n.t("offline-storage.inhalte.von.anderen.seiten.konnen.hier.nicht")); if (url.origin !== globalThis.location.origin) throw new TypeError(ns.I18n.t("offline-storage.error.foreign-origin"));
url.searchParams.delete('v'); url.searchParams.delete('v');
if (url.pathname.startsWith('/api/')) url.searchParams.delete('_'); if (url.pathname.startsWith('/api/')) url.searchParams.delete('_');
url.hash = ''; url.hash = '';
@@ -60,7 +60,7 @@ globalThis.Wegwichtel ??= {};
} }
async function put(stage, url, response) { async function put(stage, url, response) {
if (!response.ok || response.type === 'opaque') throw new Error(ns.I18n.t("offline-storage.ein.teil.konnte.nicht.gespeichert.werden.prufe")); if (!response.ok || response.type === 'opaque') throw new Error(ns.I18n.t("offline-storage.error.response"));
const normalized = key(url); const normalized = key(url);
if (stage.mode === 'cache') { if (stage.mode === 'cache') {
const cache = await caches.open(stage.id); const cache = await caches.open(stage.id);
+2 -2
View File
@@ -41,10 +41,10 @@
const image = images[index]; const image = images[index];
$('#slide-image') $('#slide-image')
.attr('src', mediaUrl(image.url)) .attr('src', mediaUrl(image.url))
.attr('alt', image.caption || ns.I18n.t("slideshow.bild.zur.station")) .attr('alt', image.caption || ns.I18n.t("slideshow.image-label"))
.off('error.wegwichtel') .off('error.wegwichtel')
.on('error.wegwichtel', function () { .on('error.wegwichtel', function () {
$(this).removeAttr('src').attr('alt', ns.I18n.t("slideshow.bild.konnte.nicht.geladen.werden")); $(this).removeAttr('src').attr('alt', ns.I18n.t("slideshow.error.load-image"));
}); });
$('#slide-caption').text(image.caption || ''); $('#slide-caption').text(image.caption || '');
$('#slide-position').text(`${index + 1} / ${images.length}`); $('#slide-position').text(`${index + 1} / ${images.length}`);
+7 -7
View File
@@ -34,15 +34,15 @@ globalThis.Wegwichtel ??= {};
} }
function siteDescription(usage, quota, known) { function siteDescription(usage, quota, known) {
if (!known) return ns.I18n.t("storage-usage.der.freie.platz.konnte.nicht.gemessen.werden"); if (!known) return ns.I18n.t("storage.error.estimate");
const warning = usage / quota >= .9 ? ns.I18n.t("storage-usage.fast.voll.entferne.inhalte.die.du.nicht") : ''; const warning = usage / quota >= .9 ? ns.I18n.t("storage.site-warning") : '';
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 }); return ns.I18n.t("storage.site-usage", { value1: Math.round(usage / quota * 100), value2: format(Math.max(0, quota - usage)), value3: warning });
} }
function localDescription(local) { function localDescription(local) {
if (local == null) return ns.I18n.t("storage-usage.texte.und.einstellungen.konnen.hier.gerade.nicht"); if (local == null) return ns.I18n.t("storage.error.local-unavailable");
const warning = local >= LOCAL_REFERENCE * .9 ? ns.I18n.t("storage-usage.der.balken.ist.fast.voll.grosse.bilder") : ''; const warning = local >= LOCAL_REFERENCE * .9 ? ns.I18n.t("storage.local-warning") : '';
return ns.I18n.t("storage-usage.texte.und.einstellungen.ungefahr.belegt.der.balken", { value1: format(local), value2: warning }); return ns.I18n.t("storage.local-usage", { value1: format(local), value2: warning });
} }
async function refresh() { async function refresh() {
@@ -69,7 +69,7 @@ globalThis.Wegwichtel ??= {};
let current = error; let current = error;
while (current) { while (current) {
if (current.name === 'QuotaExceededError') { if (current.name === 'QuotaExceededError') {
lastError = ns.I18n.t("storage-usage.der.platz.ist.voll.entferne.gespeicherte.strecken"); lastError = ns.I18n.t("storage.error.quota");
break; break;
} }
current = current.cause; current = current.cause;
+524 -411
View File
@@ -2,418 +2,531 @@ globalThis.Wegwichtel ??= {};
(function (ns) { (function (ns) {
'use strict'; 'use strict';
class WegwichtelTextsDe { class WegwichtelTexts {
static language = 'de'; static language = 'de';
static messages = Object.freeze({ 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": {
"app.strecke.wahlen": "Strecke wählen", "resume-hint": "Du kannst dort weitermachen, wo du aufgehört hast.",
"app.strecken.fur.alle.und.meine.entwurfe": "Strecken für alle und meine Entwürfe", "draft": "Eigener Entwurf",
"app.strecke": "Strecke", "choose-route": "Strecke wählen",
"app.alle.stationen.erreicht": "Alle Stationen erreicht", "route-menu": "Strecken für alle und meine Entwürfe",
"app.keine.stationen.vorhanden": "Keine Stationen vorhanden", "route": "Strecke",
"app.nachste.station": "Nächste Station: {value1}{value2}", "stations-complete": "Alle Stationen erreicht",
"app.veroffentlicht": "Veröffentlicht", "no-stations": "Keine Stationen vorhanden",
"app.strecken.fur.alle": "Strecken für alle ({value1})", "next-station-summary": "Nächste Station: {value1}{value2}",
"app.meine.entwurfe": "Meine Entwürfe ({value1})", "published": "Veröffentlicht",
"app.hier.gibt.es.noch.keine.strecken.fur": "Hier gibt es noch keine Strecken für alle.", "public-route-count": "Strecken für alle ({value1})",
"app.du.hast.noch.keine.eigene.strecke.angelegt": "Du hast noch keine eigene Strecke angelegt.", "draft-count": "Meine Entwürfe ({value1})",
"app.keine.strecke.im.umkreis.von.km.gefunden": "Keine Strecke im Umkreis von {value1} km gefunden. Wähle unten eine beliebige Strecke aus.", "no-public-route": "Hier gibt es noch keine Strecken für alle.",
"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.", "no-route": "Du hast noch keine eigene Strecke angelegt.",
"app.keine.passende.strecke.gefunden.versuche.ein.anderes": "Keine passende Strecke gefunden. Versuche ein anderes Wort.", "no-nearby-route": "Keine Strecke im Umkreis von {value1} km gefunden. Wähle unten eine beliebige Strecke aus.",
"app.von.strecken.gefunden": "{value1} von {value2} Strecken gefunden.", "location-permission-hint": "Erlaube deinen Standort, um Strecken in deiner Nähe zu finden. Unten kannst du alle Strecken auswählen.",
"app.strecken.zur.auswahl": "{value1} Strecken zur Auswahl.", "no-search-result": "Keine passende Strecke gefunden. Versuche ein anderes Wort.",
"app.strecke.fortsetzen": "Strecke fortsetzen", "search-result-count": "{value1} von {value2} Strecken gefunden.",
"app.strecke.starten": "Strecke starten", "route-count": "{value1} Strecken zur Auswahl.",
"app.strecke.beendet": "Strecke beendet.", "resume-route": "Strecke fortsetzen",
"app.du.machst.eine.pause.die.app.sucht": "Du machst eine Pause. Die App sucht gerade nicht nach deinem Standort.", "start-route": "Strecke starten",
"app.strecke.pausiert": "Strecke pausiert", "route-finished": "Strecke beendet.",
"app.diese.strecke.hat.noch.keine.stationen": "Diese Strecke hat noch keine Stationen.", "pause-hint": "Du machst eine Pause. Die App sucht gerade nicht nach deinem Standort.",
"app.norden.oben.nachste.station": "Norden oben · nächste Station: {value1}", "route-paused": "Strecke pausiert",
"app.nachste.station.2": "Nächste Station: {value1}", "empty-route": "Diese Strecke hat noch keine Stationen.",
"app.nachste.station.in.metern": "Nächste Station {value1} in {value2} Metern", "next-station-north": "Norden oben · nächste Station: {value1}",
"app.alle.stationen.und.das.ziel.erreicht": "Alle Stationen und das Ziel erreicht", "next-station": "Nächste Station: {value1}",
"app.norden.oben.nachster.punkt.auf.dem.weg": "Norden oben · nächster Punkt auf dem Weg", "next-station-distance": "Nächste Station {value1} in {value2} Metern",
"app.nachster.punkt.auf.dem.weg": "Nächster Punkt auf dem Weg", "destination-reached": "Alle Stationen und das Ziel erreicht",
"app.nachster.punkt.auf.dem.weg.in.metern": "Nächster Punkt auf dem Weg in {value1} Metern", "next-path-point-north": "Norden oben · nächster Punkt auf dem Weg",
"app.dein.standort.wird.gesucht.auf.etwa.m": "Dein Standort wird gesucht · auf etwa {value1} m genau", "next-path-point": "Nächster Punkt auf dem Weg",
"app.nachste.station.m": " · nächste Station {value1} m", "next-path-point-distance": "Nächster Punkt auf dem Weg in {value1} Metern",
"app.alle.stationen.erreicht.2": " · alle Stationen erreicht", "location-accuracy": "Dein Standort wird gesucht · auf etwa {value1} m genau",
"app.strecke.wird.fortgesetzt": "Strecke wird fortgesetzt.", "nearest-station-distance": " · nächste Station {value1} m",
"app.strecke.wird.fortgesetzt.2": "Strecke wird fortgesetzt", "stations-complete-suffix": " · alle Stationen erreicht",
"app.dein.standort.konnte.nicht.gefunden.werden.prufe": "Dein Standort konnte nicht gefunden werden. Prüfe die Standort-Erlaubnis auf deinem Gerät.", "resuming": "Strecke wird fortgesetzt.",
"app.norden.oben.navigation": "Norden oben · Navigation", "resuming-title": "Strecke wird fortgesetzt",
"app.die.strecken.konnten.nicht.geladen.werden.bitte": "Die Strecken konnten nicht geladen werden. Bitte versuche es noch einmal.", "error": {
"app.deine.entwurfe.konnten.nicht.geoffnet.werden.bitte": "Deine Entwürfe konnten nicht geöffnet werden. Bitte versuche es noch einmal.", "location": "Dein Standort konnte nicht gefunden werden. Prüfe die Standort-Erlaubnis auf deinem Gerät.",
"app.dein.standort.wurde.auf.etwa.m.genau": "Dein Standort wurde auf etwa {value1} m genau gefunden. ", "load-routes": "Die Strecken konnten nicht geladen werden. Bitte versuche es noch einmal.",
"app.wir.zeigen.dir.strecken.bis.km.entfernung": "Wir zeigen dir Strecken bis {value1} km Entfernung.", "load-drafts": "Deine Entwürfe konnten nicht geöffnet werden. Bitte versuche es noch einmal.",
"app.dein.standort.konnte.nicht.gefunden.werden.du": "Dein Standort konnte nicht gefunden werden. Du kannst trotzdem alle Strecken auswählen.", "open-route": "Die Strecke konnte nicht geöffnet werden. Prüfe deine Verbindung und versuche es noch einmal.",
"app.die.strecke.konnte.nicht.geoffnet.werden.prufe": "Die Strecke konnte nicht geöffnet werden. Prüfe deine Verbindung und versuche es noch einmal.", "open-draft": "Die Strecke konnte nicht geöffnet werden. Bitte 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 …", "navigation-north": "Norden oben · Navigation",
"bootstrap-loader.wegwichtel.vorbereiten": "Wegwichtel vorbereiten …", "location-found": "Dein Standort wurde auf etwa {value1} m genau gefunden. ",
"bootstrap-loader.die.bedienung.vorbereiten": "Die Bedienung vorbereiten …", "search-radius": "Wir zeigen dir Strecken bis {value1} km Entfernung.",
"bootstrap-loader.die.app.vorbereiten": "Die App vorbereiten …", "location-optional": "Dein Standort konnte nicht gefunden werden. Du kannst trotzdem alle Strecken auswählen.",
"bootstrap-loader.verbindung.prufen": "Verbindung prüfen …", "page": {
"bootstrap-loader.strecken.laden": "Strecken laden …", "missing": "Diese Seite konnte nicht geöffnet werden."
"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.", "location": {
"bootstrap-loader.ein.teil.der.app.konnte.nicht.starten": "Ein Teil der App konnte nicht starten. Bitte versuche es noch einmal.", "searching": "Deinen Standort suchen",
"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.", "start": "Los geht’s! Wir suchen deinen Standort."
"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.", "distance": {
"bootstrap-loader.wegwichtel.konnte.nicht.starten.bitte.versuche.es": "Wegwichtel konnte nicht starten. Bitte versuche es noch einmal.", "spoken": "{value1} Meter entfernt",
"editor-ui.zu.dieser.strecke.kann.gerade.keine.weitere": "Zu dieser Strecke kann gerade keine weitere Station hinzugefügt werden.", "short": "{value1} m entfernt"
"editor-ui.unbenannte.strecke": "Unbenannte Strecke", },
"editor-ui.station.ohne.namen": "Station ohne Namen", "path": {
"editor-ui.eigene.strecke.bearbeiten": "Eigene Strecke bearbeiten", "missing": "Der Weg dieser Strecke fehlt."
"editor-ui.neue.strecke": "Neue Strecke", }
"editor-ui.strecke.ansehen": "Strecke ansehen", },
"editor-ui.station.bearbeiten": "Station bearbeiten", "bootstrap-loader": {
"editor-ui.station.hinzufugen": "Station hinzufügen", "steps": {
"editor-ui.die.aktion.konnte.nicht.ausgefuhrt.werden.bitte": "Die Aktion konnte nicht ausgeführt werden. Bitte versuche es noch einmal.", "storage": "Gespeicherte Inhalte prüfen …",
"editor-ui.veroffentlichte.strecke": "Veröffentlichte Strecke", "jquery": "Wegwichtel vorbereiten …",
"editor-ui.strecke.bearbeiten": "Strecke bearbeiten", "jquery-ui": "Die Bedienung vorbereiten …",
"editor-ui.strecke.loschen": "Strecke löschen", "modules": "Die App vorbereiten …",
"editor-ui.station": "Station {value1}", "server": "Verbindung prüfen …",
"editor-ui.loschen": "{value1} löschen", "routes": "Strecken laden …",
"editor-ui.strecken.fur.alle": "Strecken für alle", "location": "Deinen Standort suchen …"
"editor-ui.meine.entwurfe": "Meine Entwürfe", },
"editor-ui.noch.kein.eigener.entwurf.vorhanden": "Noch kein eigener Entwurf vorhanden.", "hints": {
"editor-ui.noch.keine.stationen.angelegt": "Noch keine Stationen angelegt.", "chrome": "Auf diesem Gerät kannst du Bilder und Tonaufnahmen direkt auswählen.",
"editor-ui.strecken.werden.geladen": "Strecken werden geladen …", "android": "Tipp für Android: Mit Chrome lassen sich Bilder und Tonaufnahmen auswählen, ohne sie noch einmal zu speichern."
"editor-ui.schon.zur.prufung.gesendet": "Schon zur Prüfung gesendet", },
"editor-ui.zur.prufung.senden": "Zur Prüfung senden", "error": {
"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.", "start-error": "Ein Teil der App konnte nicht starten. Bitte versuche es noch einmal.",
"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.", "load-error": "Ein Teil der App konnte nicht geladen werden. Prüfe deine Internetverbindung und versuche es noch einmal.",
"editor-ui.diese.strecke.ist.fur.alle.freigegeben.und": "Diese Strecke ist für alle freigegeben und kann hier nicht mehr verändert werden.", "connection-error": "Wegwichtel konnte nicht starten. Prüfe deine Verbindung und versuche es noch einmal.",
"editor-ui.diese.strecke.wurde.noch.nicht.fur.alle": "Diese Strecke wurde noch nicht für alle freigegeben.", "init-error": "Wegwichtel konnte nicht starten. Bitte versuche es noch einmal.",
"editor-ui.knopf.zum.entfernen": "{value1}: Knopf zum Entfernen {value2}", "step-error": "Das hat nicht geklappt: {value1}"
"editor-ui.neue.tonaufnahme": "Neue Tonaufnahme", },
"editor-ui.bild": "Bild {value1}", "cleanup-progress": "Alten Fortschritt löschen ({value1} / {value2})",
"editor-ui.neues.bild": "Neues Bild {value1}", "ready": "Alles ist bereit."
"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": {
"editor-ui.noch.keine.bilder.oder.tonaufnahmen.vorhanden": "Noch keine Bilder oder Tonaufnahmen vorhanden.", "error": {
"editor-ui.tonaufnahmen": "Tonaufnahmen", "add-station": "Zu dieser Strecke kann gerade keine weitere Station hinzugefügt werden.",
"editor-ui.bilder": "Bilder", "action": "Die Aktion konnte nicht ausgeführt werden. Bitte versuche es noch einmal.",
"editor-ui.dein.entwurf.wurde.nicht.gefunden": "Dein Entwurf wurde nicht gefunden.", "draft-missing": "Dein Entwurf wurde nicht gefunden.",
"editor-ui.deine.neue.strecke.bleibt.zuerst.auf.diesem": "Deine neue Strecke bleibt zuerst auf diesem Gerät.", "create-locally": "Lege eine neue Strecke zuerst auf diesem Gerät an.",
"editor-ui.gespeichert": "Gespeichert.", "route-name": "Gib deiner Strecke einen Namen.",
"editor-ui.lege.eine.neue.strecke.zuerst.auf.diesem": "Lege eine neue Strecke zuerst auf diesem Gerät an.", "send-file": "Die Datei {value1} konnte nicht gesendet werden. Bitte wähle sie noch einmal aus.",
"editor-ui.gib.deiner.strecke.einen.namen": "Gib deiner Strecke einen Namen.", "send-route": "Die Strecke konnte nicht gesendet werden. Bitte versuche es noch einmal.",
"editor-ui.die.datei.konnte.nicht.gesendet.werden.bitte": "Die Datei {value1} konnte nicht gesendet werden. Bitte wähle sie noch einmal aus.", "send-station": "Die Station {value1} konnte nicht gesendet werden. Bitte versuche es noch einmal.",
"editor-ui.die.strecke.konnte.nicht.gesendet.werden.bitte": "Die Strecke konnte nicht gesendet werden. Bitte versuche es noch einmal.", "submit-own-draft": "Du kannst nur deine eigenen Entwürfe zur Prüfung senden.",
"editor-ui.die.station.konnte.nicht.gesendet.werden.bitte": "Die Station {value1} konnte nicht gesendet werden. Bitte versuche es noch einmal.", "already-submitted": "Diese Strecke wurde schon zur Prüfung gesendet.",
"editor-ui.du.kannst.nur.deine.eigenen.entwurfe.zur": "Du kannst nur deine eigenen Entwürfe zur Prüfung senden.", "location-required": "Für eine neue Station brauchen wir deinen Standort. Erlaube den Standort und versuche es noch einmal.",
"editor-ui.diese.strecke.wurde.schon.zur.prufung.gesendet": "Diese Strecke wurde schon zur Prüfung gesendet.", "save-route-first": "Speichere zuerst die Strecke.",
"editor-ui.deine.strecke.wird.gesendet": "Deine Strecke wird gesendet …", "station-location": "Der Ort dieser Station fehlt. Erlaube deinen Standort und versuche es noch einmal.",
"editor-ui.zur.prufung.gesendet": "Zur Prüfung gesendet.", "station-name": "Gib deiner Station einen Namen.",
"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.", "station-unsaved": "Die Station ist noch nicht gespeichert.",
"editor-ui.speichere.zuerst.die.strecke": "Speichere zuerst die Strecke.", "remove-image": "Das Bild konnte nicht entfernt werden. Lade die Strecke noch einmal.",
"editor-ui.der.ort.dieser.station.fehlt.erlaube.deinen": "Der Ort dieser Station fehlt. Erlaube deinen Standort und versuche es noch einmal.", "generic": "Das hat leider nicht geklappt. Bitte 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.", "untitled-route": "Unbenannte Strecke",
"editor-ui.gib.deiner.station.einen.namen": "Gib deiner Station einen Namen.", "untitled-station": "Station ohne Namen",
"editor-ui.bild.entfernt": "Bild entfernt.", "edit-draft": "Eigene Strecke bearbeiten",
"editor-ui.die.station.ist.noch.nicht.gespeichert": "Die Station ist noch nicht gespeichert.", "new-route": "Neue Strecke",
"editor-ui.das.bild.konnte.nicht.entfernt.werden.lade": "Das Bild konnte nicht entfernt werden. Lade die Strecke noch einmal.", "view-route": "Strecke ansehen",
"editor-ui.station.entfernt.deine.strecke.wurde.gespeichert": "Station entfernt. Deine Strecke wurde gespeichert.", "edit-station": "Station bearbeiten",
"editor-ui.station.entfernt": "Station entfernt.", "add-station": "Station hinzufügen",
"editor-ui.reihenfolge.der.stationen.gespeichert": "Reihenfolge der Stationen gespeichert.", "published-route": "Veröffentlichte Strecke",
"editor-ui.strecke.geloscht": "Strecke gelöscht.", "edit-route": "Strecke bearbeiten",
"editor-ui.das.hat.leider.nicht.geklappt.bitte.versuche": "Das hat leider nicht geklappt. Bitte versuche es noch einmal.", "delete-route": "Strecke löschen",
"geolocation.dieses.gerat.kann.deinen.standort.nicht.finden": "Dieses Gerät kann deinen Standort nicht finden.", "station-number": "Station {value1}",
"group-symbols.holz": "Holz", "delete-item": "{value1} löschen",
"group-symbols.felsen": "Felsen", "public-routes": "Strecken für alle",
"group-symbols.laterne": "Laterne", "drafts": "Meine Entwürfe",
"group-symbols.blume": "Blume", "no-draft": "Noch kein eigener Entwurf vorhanden.",
"group-symbols.pfeife": "Pfeife", "no-stations": "Noch keine Stationen angelegt.",
"group-symbols.vogel": "Vogel", "loading-routes": "Strecken werden geladen …",
"group-symbols.pilz": "Pilz", "submitted": "Schon zur Prüfung gesendet",
"group-symbols.erdbeere": "Erdbeere", "submit": "Zur Prüfung senden",
"group-symbols.kurbis": "Kürbis", "submitted-hint": "Diese Strecke wurde zur Prüfung gesendet. Andere können sie erst nach der Freigabe sehen.",
"legal-ui.datenschutzerklarung": "Datenschutzerklärung", "draft-hint": "Dieser Entwurf bleibt auf deinem Gerät. Mit „Zur Prüfung senden“ reichst du ihn zur Freigabe ein.",
"legal-ui.impressum": "Impressum", "read-only-hint": "Diese Strecke ist für alle freigegeben und kann hier nicht mehr verändert werden.",
"legal-ui.speichern.und.berechtigungen": "Speichern und Berechtigungen", "unpublished-hint": "Diese Strecke wurde noch nicht für alle freigegeben.",
"legal-ui.hilfe.und.informationen.offnen": "Hilfe und Informationen öffnen", "remove-media-label": "{value1}: Knopf zum Entfernen {value2}",
"legal-ui.informationen": "Informationen", "new-audio": "Neue Tonaufnahme",
"legal-ui.rechtliche.und.technische.informationen": "Rechtliche und technische Informationen", "image-number": "Bild {value1}",
"legal-ui.zuruck.zum.informationsmenu": "Zurück zum Informationsmenü", "new-image": "Neues Bild {value1}",
"legal-ui.informationsfenster.schliessen": "Informationsfenster schließen", "remove-media-hint": "{value1}. Tippe auf ein Bild oder eine Tonaufnahme, um sie zu entfernen.",
"legal-ui.schliessen": "Schließen", "no-media": "Noch keine Bilder oder Tonaufnahmen vorhanden.",
"local-routes.deine.strecke.kann.auf.diesem.gerat.gerade": "Deine Strecke kann auf diesem Gerät gerade nicht gespeichert werden.", "audio-section": "Tonaufnahmen",
"local-routes.bilder.und.ton.konnen.auf.diesem.gerat": "Bilder und Ton können auf diesem Gerät gerade nicht gespeichert werden.", "image-section": "Bilder",
"local-routes.gespeicherte.bilder.und.ton.konnten.nicht.geoffnet": "Gespeicherte Bilder und Ton konnten nicht geöffnet werden.", "draft-status": "Deine neue Strecke bleibt zuerst auf diesem Gerät.",
"local-routes.schliesse.andere.fenster.mit.wegwichtel.und.versuche": "Schließe andere Fenster mit Wegwichtel und versuche es noch einmal.", "saved": "Gespeichert.",
"local-routes.bilder.und.ton.konnten.nicht.geoffnet.oder": "Bilder und Ton konnten nicht geöffnet oder gespeichert werden.", "submitting": "Deine Strecke wird gesendet …",
"local-routes.das.speichern.von.bildern.und.ton.wurde": "Das Speichern von Bildern und Ton wurde abgebrochen.", "submit-success": "Zur Prüfung gesendet.",
"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.", "station-saved-with-media": "Station mit Bildern und Ton auf diesem Gerät gespeichert.",
"local-routes.der.platz.fur.bilder.und.ton.ist.2": "Der Platz für Bilder und Ton ist voll.", "station-saved": "Station gespeichert.",
"local-routes.das.bild.oder.die.tonaufnahme.konnte.nicht": "Das Bild oder die Tonaufnahme konnte nicht gespeichert werden.", "image-removed": "Bild entfernt.",
"local-routes.entferne.bilder.tonaufnahmen.oder.eigene.strecken.die": "{value1} Entferne Bilder, Tonaufnahmen oder eigene Strecken, die du nicht mehr brauchst.", "station-removed-and-saved": "Station entfernt. Deine Strecke wurde gespeichert.",
"local-routes.erlaube.noch.einmal.den.zugriff.auf.das": "Erlaube noch einmal den Zugriff auf das ausgewählte Bild oder die Tonaufnahme.", "station-removed": "Station entfernt.",
"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.", "station-order-saved": "Reihenfolge der Stationen gespeichert.",
"local-routes.auf.diesem.gerat.kann.gerade.keine.neue": "Auf diesem Gerät kann gerade keine neue Strecke angelegt werden.", "route-deleted": "Strecke gelöscht.",
"local-routes.diese.datei.enthalt.keinen.lesbaren.weg.wahle": "Diese Datei enthält keinen lesbaren Weg. Wähle eine andere Datei.", "open": "Strecken bearbeiten",
"local-routes.der.ort.von.fehlt": "Der Ort von {value1} fehlt.", "close": "Fenster zum Bearbeiten schließen",
"local-routes.der.platz.fur.deine.strecken.ist.voll": "Der Platz für deine Strecken ist voll.", "routes-title": "Deine Strecken",
"local-routes.deine.strecke.konnte.nicht.gespeichert.werden": "Deine Strecke konnte nicht gespeichert werden.", "create-route": "Neue Strecke anlegen",
"local-routes.entferne.eigene.strecken.die.du.nicht.mehr": "{value1} Entferne eigene Strecken, die du nicht mehr brauchst.", "routes-hint": "Hier findest du Strecken für alle und deine eigenen Entwürfe.",
"local-routes.der.weg.deiner.strecke.fehlt": "Der Weg deiner Strecke fehlt.", "route-title": "Deine Strecke",
"local-routes.deine.strecke.hat.noch.keinen.aufgezeichneten.weg": "Deine Strecke hat noch keinen aufgezeichneten Weg.", "route-name": "Name der Strecke",
"local-routes.deine.strecke.wurde.nicht.gefunden": "Deine Strecke wurde nicht gefunden.", "description": "Beschreibung",
"local-routes.diese.datei.kann.auf.deinem.gerat.nicht": "Diese Datei kann auf deinem Gerät nicht gelesen werden.", "import-path": "Weg aus einer Datei übernehmen",
"local-routes.diese.datei.konnte.nicht.geoffnet.werden.wahle": "Diese Datei konnte nicht geöffnet werden. Wähle sie noch einmal aus.", "new-draft-hint": "Deine neue Strecke bleibt zuerst auf diesem Gerät. Mit „Zur Prüfung senden“ kannst du sie zur Freigabe einreichen.",
"offline-cache.das.speichern.fur.unterwegs.hat.nicht.geklappt": "Das Speichern für unterwegs hat nicht geklappt. Bitte versuche es noch einmal.", "save-route": "Strecke speichern",
"offline-cache.wegwichtel.wird.fur.unterwegs.vorbereitet": "Wegwichtel wird für unterwegs vorbereitet …", "station-name": "Name",
"offline-cache.wegwichtel.konnte.nicht.fur.unterwegs.geladen.werden": "Wegwichtel konnte nicht für unterwegs geladen werden. Prüfe deine Internetverbindung.", "station-text": "Text",
"offline-cache.wegwichtel.konnte.nicht.fur.unterwegs.geladen.werden.2": "Wegwichtel konnte nicht für unterwegs geladen werden. Bitte versuche es noch einmal.", "media-actions-label": "Bilder und Ton zur Station hinzufügen",
"offline-cache.fur.unterwegs.speichern.von": "Für unterwegs speichern: {value1} von {value2}", "add-audio": "Ton hinzufügen",
"offline-cache.wegwichtel.ist.fur.unterwegs.bereit": "Wegwichtel ist für unterwegs bereit.", "add-image": "Bild hinzufügen",
"offline-cache.speichern.abgebrochen": "Speichern abgebrochen.", "media-preview-label": "Bilder und Ton ansehen",
"offline-cache.das.speichern.hat.nicht.geklappt": "Das Speichern hat nicht geklappt.", "save-station": "Station speichern",
"offline-cache.wegwichtel.ist.noch.nicht.ohne.internet.bereit": "Wegwichtel ist noch nicht ohne Internet bereit.", "remove-station": "Station entfernen",
"offline-cache.die.gespeicherte.app.wurde.entfernt.deine.strecken": "Die gespeicherte App wurde entfernt. Deine Strecken bleiben erhalten.", "station": {
"offline-cache.gespeicherte.strecke.erneuern": "Gespeicherte Strecke erneuern", "up": "{value1} nach oben schieben",
"offline-cache.fur.unterwegs.speichern": "Für unterwegs speichern", "down": "{value1} nach unten schieben"
"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.", "route": {
"offline-cache.bitte.warte.bis.die.andere.strecke.gespeichert": "Bitte warte, bis die andere Strecke gespeichert ist.", "loading": "Deine Strecke wird geöffnet …"
"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.", "audio": {
"offline-cache.die.strecke.konnte.nicht.geladen.werden": "Die Strecke konnte nicht geladen werden.", "removed": "Tonaufnahme entfernt."
"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.", "order": {
"offline-cache.strecke.speichern.von": "Strecke speichern: {value1} von {value2}", "saved": "Reihenfolge gespeichert."
"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.", "location": {
"offline-storage.inhalte.von.anderen.seiten.konnen.hier.nicht": "Inhalte von anderen Seiten können hier nicht gespeichert werden.", "error": {
"offline-storage.ein.teil.konnte.nicht.gespeichert.werden.prufe": "Ein Teil konnte nicht gespeichert werden. Prüfe deine Internetverbindung und versuche es noch einmal.", "unsupported": "Dieses Gerät kann deinen Standort nicht finden."
"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.", "symbols": {
"storage-usage.fast.voll.entferne.inhalte.die.du.nicht": " Fast voll: Entferne Inhalte, die du nicht mehr brauchst.", "wood": "Holz",
"storage-usage.app.bilder.und.ton.des.verfugbaren.platzes": "App, Bilder und Ton: {value1} % des verfügbaren Platzes belegt. Ungefähr {value2} frei.{value3}", "rock": "Felsen",
"storage-usage.texte.und.einstellungen.konnen.hier.gerade.nicht": "Texte und Einstellungen können hier gerade nicht gespeichert werden.", "lantern": "Laterne",
"storage-usage.der.balken.ist.fast.voll.grosse.bilder": " Der Balken ist fast voll. Große Bilder können das Speichern verhindern.", "flower": "Blume",
"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}", "pipe": "Pfeife",
"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.", "bird": "Vogel",
"user-messages.diese.strecke.kann.nur.die.person.loschen": "Diese Strecke kann nur die Person löschen, die Wegwichtel betreut.", "mushroom": "Pilz",
"user-messages.diese.strecke.ist.fur.alle.freigegeben.du": "Diese Strecke ist für alle freigegeben. Du kannst sie hier nicht mehr verändern.", "strawberry": "Erdbeere",
"user-messages.die.datei.enthalt.keinen.lesbaren.weg.wahle": "Die Datei enthält keinen lesbaren Weg. Wähle eine andere Datei.", "pumpkin": "Kürbis"
"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.", "info": {
"user-messages.dieses.bild.kann.nicht.geoffnet.werden.wahle": "Dieses Bild kann nicht geöffnet werden. Wähle ein anderes Bild.", "privacy": "Datenschutzerklärung",
"user-messages.diese.tonaufnahme.kann.nicht.geoffnet.werden.wahle": "Diese Tonaufnahme kann nicht geöffnet werden. Wähle eine andere Aufnahme.", "imprint": "Impressum",
"user-messages.diese.datei.ist.zu.gross.wahle.eine": "Diese Datei ist zu groß. Wähle eine kleinere Datei.", "storage": "Speichern und Berechtigungen",
"user-messages.du.darfst.diese.aktion.hier.nicht.ausfuhren": "Du darfst diese Aktion hier nicht ausführen.", "open": "Hilfe und Informationen öffnen",
"user-messages.der.platz.ist.voll.entferne.gespeicherte.strecken": "Der Platz ist voll. Entferne gespeicherte Strecken, Bilder oder Tonaufnahmen, die du nicht mehr brauchst.", "title": "Informationen",
"user-messages.wegwichtel.ist.gerade.nicht.erreichbar.bitte.versuche": "Wegwichtel ist gerade nicht erreichbar. Bitte versuche es später noch einmal.", "menu-label": "Rechtliche und technische Informationen",
"vibration.das.vibrationsmuster.muss.aus.nichtnegativen.millisekunden.bestehen": "Das Vibrationsmuster muss aus nichtnegativen Millisekunden bestehen.", "back": "Zurück zum Informationsmenü",
"vibration.die.vibrationsdauer.muss.eine.nichtnegative.millisekundenangabe.sein": "Die Vibrationsdauer muss eine nichtnegative Millisekundenangabe sein.", "close-label": "Informationsfenster schließen",
"html.title.wegwichtel": "Wegwichtel", "close": "Schließen",
"html.span.i": "i", "icon": "i",
"html.info-dropdown.hilfe.und.informationen": "Hilfe und Informationen", "dropdown-label": "Hilfe und Informationen",
"html.p.gleich.kannst.du.mit.dem.wegwichtel.losziehen": "Gleich kannst du mit dem Wegwichtel losziehen.", "missing": "{value1}: Diese Information fehlt noch."
"html.bootstrap-status-text.wegwichtel.wird.vorbereitet": "Wegwichtel wird vorbereitet …", },
"html.span.los.gehts": "Los geht’s", "local": {
"html.bootstrap-retry.erneut.versuchen": "Erneut versuchen", "error": {
"html.bootstrap-ready.alles.ist.bereit": "Alles ist bereit.", "route-storage": "Deine Strecke kann auf diesem Gerät gerade nicht gespeichert werden.",
"html.div.wegwichtel.funktionen": "Wegwichtel Funktionen", "media-storage": "Bilder und Ton können auf diesem Gerät gerade nicht gespeichert werden.",
"html.span.strecken": "Strecken", "media-read": "Gespeicherte Bilder und Ton konnten nicht geöffnet werden.",
"html.span.entdecken": "entdecken", "other-tabs": "Schließe andere Fenster mit Wegwichtel und versuche es noch einmal.",
"html.span.spannende": "Spannende", "media-access": "Bilder und Ton konnten nicht geöffnet oder gespeichert werden.",
"html.span.geschichten": "Geschichten", "media-cancelled": "Das Speichern von Bildern und Ton wurde abgebrochen.",
"html.span.kindgerechte": "Kindgerechte", "media-quota-hint": "Der Platz für Bilder und Ton ist voll. Entferne Bilder, Tonaufnahmen oder eigene Strecken, die du nicht mehr brauchst.",
"html.span.erlebnisse": "Erlebnisse", "media-quota": "Der Platz für Bilder und Ton ist voll.",
"html.p.hier.gibt.es.keine.werbung.wir.verfolgen": "Hier gibt es keine Werbung. Wir verfolgen nicht, welche anderen Seiten du besuchst.", "media-save": "Das Bild oder die Tonaufnahme konnte nicht gespeichert werden.",
"html.strong.optionale.cookies": "Optionale Cookies", "media-cleanup-hint": "{value1} Entferne Bilder, Tonaufnahmen oder eigene Strecken, die du nicht mehr brauchst.",
"html.small.diese.funktion.wird.nicht.verwendet": "Diese Funktion wird nicht verwendet.", "file-permission": "Erlaube noch einmal den Zugriff auf das ausgewählte Bild oder die Tonaufnahme.",
"html.strong.meinen.fortschritt.merken": "Meinen Fortschritt merken", "file-missing": "Ein ausgewähltes Bild oder eine Tonaufnahme wurde verschoben oder gelöscht. Wähle die Datei noch einmal aus.",
"html.small.merkt.sich.auf.diesem.gerat.welche.stationen": "Merkt sich auf diesem Gerät, welche Stationen du schon besucht hast.", "create-route": "Auf diesem Gerät kann gerade keine neue Strecke angelegt werden.",
"html.strong.app.ohne.internet.offnen": "App ohne Internet öffnen", "invalid-track": "Diese Datei enthält keinen lesbaren Weg. Wähle eine andere Datei.",
"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.", "station-location": "Der Ort von {value1} fehlt.",
"html.offline-cache-help-button.was.wird.fur.unterwegs.gespeichert": "Was wird für unterwegs gespeichert?", "route-quota": "Der Platz für deine Strecken ist voll.",
"html.offline-cache-help-button.hilfe.zum.speichern.fur.unterwegs": "Hilfe zum Speichern für unterwegs", "save-route": "Deine Strecke konnte nicht gespeichert werden.",
"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.", "route-cleanup-hint": "{value1} Entferne eigene Strecken, die du nicht mehr brauchst.",
"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.", "path-missing": "Der Weg deiner Strecke fehlt.",
"html.p.warte.vor.dem.losgehen.bis.alles.gespeichert": "Warte vor dem Losgehen, bis alles gespeichert ist. Zum Speichern und Erneuern brauchst du Internet.", "path-empty": "Deine Strecke hat noch keinen aufgezeichneten Weg.",
"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.", "route-missing": "Deine Strecke wurde nicht gefunden.",
"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.", "file-unsupported": "Diese Datei kann auf deinem Gerät nicht gelesen werden.",
"html.offline-cache-refresh.gespeicherte.app.erneuern": "Gespeicherte App erneuern", "file-read": "Diese Datei konnte nicht geöffnet werden. Wähle sie noch einmal aus."
"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", "device": "Auf diesem Gerät"
"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", "offline": {
"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.", "error": {
"html.button.platz.prufen": "Platz prüfen", "download": "Das Speichern für unterwegs hat nicht geklappt. Bitte versuche es noch einmal.",
"html.li.gespeicherte.inhalte.prufen": "Gespeicherte Inhalte prüfen", "connection": "Wegwichtel konnte nicht für unterwegs geladen werden. Prüfe deine Internetverbindung.",
"html.li.wegwichtel.vorbereiten": "Wegwichtel vorbereiten", "prepare": "Wegwichtel konnte nicht für unterwegs geladen werden. Bitte versuche es noch einmal.",
"html.li.bedienung.vorbereiten": "Bedienung vorbereiten", "save": "Das Speichern hat nicht geklappt.",
"html.li.app.vorbereiten": "App vorbereiten", "busy": "Bitte warte, bis die andere Strecke gespeichert ist.",
"html.li.verbindung.prufen": "Verbindung prüfen", "route-unsupported": "Auf diesem Gerät kannst du Strecken gerade nicht für unterwegs speichern.",
"html.li.strecken.laden": "Strecken laden", "select-route": "Wähle zuerst eine Strecke, die für alle freigegeben ist.",
"html.li.deinen.standort.suchen": "Deinen Standort suchen", "load-route": "Die Strecke konnte nicht geladen werden.",
"html.route-control-button.strecke.auswahlen.oder.wechseln": "Strecke auswählen oder wechseln", "published-only": "Du kannst nur freigegebene Strecken für unterwegs speichern.",
"html.route-control-dropdown.strecken.auswahlen.und.bearbeiten": "Strecken auswählen und bearbeiten", "path-missing": "Der Weg dieser Strecke fehlt. Sie kann noch nicht für unterwegs gespeichert werden.",
"html.editor-button.strecken.bearbeiten": "Strecken bearbeiten", "wait": "Bitte warte, bis das Speichern fertig ist.",
"html.location-hint.wir.suchen.strecken.in.deiner.nahe.dein": "Wir suchen Strecken in deiner Nähe. Dein Standort bleibt auf diesem Gerät.", "unsupported": "Auf diesem Gerät lässt sich Wegwichtel gerade nicht für unterwegs speichern. Du kannst es mit Internet weiter nutzen.",
"html.h2.strecken.in.deiner.nahe": "Strecken in deiner Nähe", "save-app-first": "Speichere zuerst die App. Danach kannst du die Strecken auswählen, die du mitnehmen möchtest.",
"html.h2.alle.strecken": "Alle Strecken", "previous": {
"html.label.strecken.durchsuchen": "Strecken durchsuchen", "kept": "{value1} Was du vorher gespeichert hast, bleibt erhalten."
"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.", "preparing": "Wegwichtel wird für unterwegs vorbereitet …",
"html.p.meine.entwurfe.werden.geladen": "Meine Entwürfe werden geladen …", "download-progress": "Für unterwegs speichern: {value1} von {value2}",
"html.button.zuruck.zur.streckenauswahl": "Zurück zur Streckenauswahl", "ready": "Wegwichtel ist für unterwegs bereit.",
"html.route-offline-controls.strecke.auf.dem.gerat.speichern": "Strecke auf dem Gerät speichern", "cancelled": "Speichern abgebrochen.",
"html.route-offline-remove.gespeicherte.strecke.entfernen": "Gespeicherte Strecke entfernen", "not-ready": "Wegwichtel ist noch nicht ohne Internet bereit.",
"html.route-gpx-download.weg.als.datei.herunterladen": "Weg als Datei herunterladen", "app-removed": "Die gespeicherte App wurde entfernt. Deine Strecken bleiben erhalten.",
"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.", "renew-route": "Gespeicherte Strecke erneuern",
"html.dt.lange": "Länge", "save-route": "Für unterwegs speichern",
"html.dt.hohenmeter": "Höhenmeter", "route-ready": "Diese Strecke ist für unterwegs bereit.",
"html.dt.stationen": "Stationen", "route-hint": "Nimm diese Strecke mit Bildern und Tonaufnahmen mit.",
"html.legend.unterwegs": "Unterwegs", "route-progress": "Strecke speichern: {value1} von {value2}",
"html.pause-route.strecke.pausieren": "Strecke pausieren", "open-app": "App ohne Internet öffnen",
"html.stop-route.strecke.beenden": "Strecke beenden", "open-app-hint": "Damit kannst du Wegwichtel auch ohne Internet öffnen. Wähle danach die Strecken aus, die du mitnehmen möchtest.",
"html.button.zuruck.zur.strecke": "Zurück zur Strecke", "help-title": "Was wird für unterwegs gespeichert?",
"html.poi-title.station": "Station", "help-label": "Hilfe zum Speichern für unterwegs",
"html.legend.bildnavigation": "Bildnavigation", "help": {
"html.slide-prev.vorheriges.bild": "Vorheriges Bild", "app-only": "Mit diesem Schalter kannst du Wegwichtel auch ohne Internet öffnen. Strecken werden dabei noch nicht gespeichert.",
"html.slide-next.nachstes.bild": "Nächstes Bild", "save-route": "Öffne eine Strecke und tippe auf „Für unterwegs speichern“. So nimmst du den Weg, die Stationen, Bilder und Tonaufnahmen mit.",
"html.poi-audio.ton.zur.station": "Ton zur Station", "connection": "Warte vor dem Losgehen, bis alles gespeichert ist. Zum Speichern und Erneuern brauchst du Internet.",
"html.poi-audio.dieses.gerat.kann.den.ton.leider.nicht": "Dieses Gerät kann den Ton leider nicht abspielen.", "keep-routes": "Alles bleibt auf diesem Gerät. Der Schalter zeichnet deinen Weg nicht auf. Beim Ausschalten bleiben deine gespeicherten Strecken und Entwürfe erhalten.",
"html.navigation-mode.richtung.zum.nachsten.station": "Richtung zum nächsten Station", "storage-limits": "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.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:", "renew-app": "Gespeicherte App erneuern",
"html.h3.1.verantwortliche.stelle.und.datenschutzkontakt": "1. Verantwortliche Stelle und Datenschutzkontakt", "route-controls-label": "Strecke auf dem Gerät speichern",
"html.p.zum.eichig.3": "Zum Eichig 3", "remove-route": "Gespeicherte Strecke entfernen",
"html.p.98617.meiningen.ot.herpf": "98617 Meiningen / OT Herpf", "export-path": "Weg als Datei herunterladen",
"html.p.telefon.036943.63722": "Telefon: (036943) 63722", "route-actions-help": "„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.p.e.mail.info.grundschule.herpf.de": "E-Mail: info@grundschule-herpf.de", "previous": {
"html.strong.staatliche.grundschule.herpf": "Staatliche Grundschule Herpf", "kept": "Was du vorher gespeichert hast, bleibt erhalten."
"html.p.datenschutzangaben.der.schule": "Datenschutzangaben der Schule:", }
"html.p.datenschutzbeauftragter": "Datenschutzbeauftragter", },
"html.p.holderlinstrae.1": "Hölderlinstraße 1", "offline-storage": {
"html.p.98527.suhl": "98527 Suhl", "error": {
"html.p.telefon.03681.7341.25": "Telefon: 03681 / 7341 25", "foreign-origin": "Inhalte von anderen Seiten können hier nicht gespeichert werden.",
"html.p.fax.03681.7341.09": "Fax: 03681 / 7341 09", "response": "Ein Teil konnte nicht gespeichert werden. Prüfe deine Internetverbindung und versuche es noch einmal."
"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.", "slideshow": {
"html.h3.2.wenn.du.wegwichtel.offnest": "2. Wenn du Wegwichtel öffnest", "image-label": "Bild zur Station",
"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.", "error": {
"html.p.wer.die.website.bereitstellt.und.welche.verbindungsdaten": "Wer die Website bereitstellt und welche Verbindungsdaten wie lange gespeichert werden:", "load-image": "Bild konnte nicht geladen werden"
"html.strong.anbieter.anschrift.gespeicherte.daten.und.speicherdauer.erganzen": "[Anbieter, Anschrift, gespeicherte Daten und Speicherdauer ergänzen]", },
"html.h3.3.standortdaten": "3. Standortdaten", "controls-label": "Bildnavigation",
"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.", "previous": "Vorheriges Bild",
"html.h3.4.deinen.fortschritt.merken": "4. Deinen Fortschritt merken", "next": "Nächstes Bild"
"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.", "storage": {
"html.small.du.entscheidest.ob.sich.wegwichtel.deinen.fortschritt": "Du entscheidest, ob sich Wegwichtel deinen Fortschritt merkt.", "error": {
"html.h3.5.deine.eigenen.strecken": "5. Deine eigenen Strecken", "estimate": "Der freie Platz konnte nicht gemessen werden.",
"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.", "local-unavailable": "Texte und Einstellungen können hier gerade nicht gespeichert werden.",
"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.", "quota": "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."
"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.", "site-warning": " Fast voll: Entferne Inhalte, die du nicht mehr brauchst.",
"html.h3.7.inhalte.von.schulerinnen.und.schulern": "7. Inhalte von Schülerinnen und Schülern", "site-usage": "App, Bilder und Ton: {value1} % des verfügbaren Platzes belegt. Ungefähr {value2} frei.{value3}",
"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.", "local-warning": " Der Balken ist fast voll. Große Bilder können das Speichern verhindern.",
"html.h3.8.deine.rechte": "8. Deine Rechte", "local-usage": "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}",
"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.", "title": "Platz für Wegwichtel",
"html.p.zustandige.datenschutzaufsicht": "Zuständige Datenschutzaufsicht:", "site-meter-label": "Belegter Platz für App, Bilder und Ton",
"html.p.postfach.90.04.55.99107.erfurt": "Postfach 90 04 55, 99107 Erfurt", "loading": "Wir schauen, wie viel Platz belegt ist …",
"html.p.halerstrae.8.99096.erfurt": "Häßlerstraße 8, 99096 Erfurt", "local-meter-label": "Belegter Platz für Texte und Einstellungen; der Balken zeigt einen ungefähren Richtwert",
"html.p.telefon.0361.57.311.29.00": "Telefon: 0361 / 57 311 29 00", "help": "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.p.telefax.0361.57.311.29.04": "Telefax: 0361 / 57 311 29 04", "refresh": "Platz prüfen"
"html.p.e.mail.poststelle.datenschutz.thueringen.de": "E-Mail: poststelle@datenschutz.thueringen.de", },
"html.p.website.www.tlfdi.de": "Website: www.tlfdi.de", "errors": {
"html.strong.thuringer.landesbeauftragter.fur.den.datenschutz.und.die": "Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)", "delete-published": "Diese Strecke kann nur die Person löschen, die Wegwichtel betreut.",
"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.", "edit-published": "Diese Strecke ist für alle freigegeben. Du kannst sie hier nicht mehr verändern.",
"html.h3.wer.fur.wegwichtel.verantwortlich.ist": "Wer für Wegwichtel verantwortlich ist", "invalid-track": "Die Datei enthält keinen lesbaren Weg. Wähle eine andere Datei.",
"html.h3.kontakt": "Kontakt", "station-missing": "Diese Station wurde nicht gefunden. Öffne die Strecke noch einmal.",
"html.p.telefax.036943.63843": "Telefax: (036943) 63843", "station-location": "Der Ort dieser Station fehlt oder konnte nicht gelesen werden.",
"html.h3.leitung.der.schule": "Leitung der Schule", "image-format": "Dieses Bild kann nicht geöffnet werden. Wähle ein anderes Bild.",
"html.p.frau.wichert.schulleiterin": "Frau Wichert, Schulleiterin", "audio-format": "Diese Tonaufnahme kann nicht geöffnet werden. Wähle eine andere Aufnahme.",
"html.h3.schultrager": "Schulträger", "file-size": "Diese Datei ist zu groß. Wähle eine kleinere Datei.",
"html.p.landratsamt": "Landratsamt", "permission": "Du darfst diese Aktion hier nicht ausführen.",
"html.p.obertshauser.platz.1": "Obertshäuser Platz 1", "quota": "Der Platz ist voll. Entferne gespeicherte Strecken, Bilder oder Tonaufnahmen, die du nicht mehr brauchst.",
"html.p.98617.meiningen": "98617 Meiningen", "unavailable": "Wegwichtel ist gerade nicht erreichbar. Bitte versuche es später noch einmal."
"html.strong.landkreis.schmalkalden.meiningen": "Landkreis Schmalkalden-Meiningen", },
"html.h3.inhaltlich.verantwortlich": "Inhaltlich verantwortlich", "vibration": {
"html.p.frau.wichert": "Frau Wichert", "error": {
"html.h3.weitere.pflichtangaben": "Weitere Pflichtangaben", "pattern": "Das Vibrationsmuster muss aus nichtnegativen Millisekunden bestehen.",
"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.", "duration": "Die Vibrationsdauer muss eine nichtnegative Millisekundenangabe sein."
"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", "common": {
"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.", "app-name": "Wegwichtel",
"html.h3.was.merkt.sich.wegwichtel": "Was merkt sich Wegwichtel?", "start": "Los geht’s",
"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.", "retry": "Erneut versuchen",
"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.", "notice": "Hinweis:",
"html.small.die.anderung.gilt.sofort.auf.diesem.gerat": "Die Änderung gilt sofort auf diesem Gerät.", "back": "Zurück"
"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.", "start": {
"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.", "intro": "Gleich kannst du mit dem Wegwichtel losziehen.",
"html.h3.deine.eigenen.strecken": "Deine eigenen Strecken", "preparing": "Wegwichtel wird vorbereitet …",
"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.", "features-label": "Wegwichtel Funktionen",
"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.", "features": {
"html.h3.welche.erlaubnisse.braucht.die.app": "Welche Erlaubnisse braucht die App?", "routes": "Strecken",
"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.", "explore": "entdecken",
"html.li.die.richtung.deines.gerats.damit.sich.der": "Die Richtung deines Geräts, damit sich der Pfeil mitdrehen kann.", "exciting": "Spannende",
"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.", "stories": "Geschichten",
"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.", "child-friendly": "Kindgerechte",
"html.h3.werbung.und.andere.anbieter": "Werbung und andere Anbieter", "experiences": "Erlebnisse"
"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", "privacy": {
"html.editor-new-route.neue.strecke.anlegen": "Neue Strecke anlegen", "no-tracking": "Hier gibt es keine Werbung. Wir verfolgen nicht, welche anderen Seiten du besuchst.",
"html.p.hier.findest.du.strecken.fur.alle.und": "Hier findest du Strecken für alle und deine eigenen Entwürfe.", "incomplete-notice": "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.editor-route-back.zuruck": "Zurück", "controller-title": "1. Verantwortliche Stelle und Datenschutzkontakt",
"html.editor-route-detail-title.deine.strecke": "Deine Strecke", "connection-title": "2. Wenn du Wegwichtel öffnest",
"html.label.name.der.strecke": "Name der Strecke", "connection-data": "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.label.beschreibung": "Beschreibung", "provider-label": "Wer die Website bereitstellt und welche Verbindungsdaten wie lange gespeichert werden:",
"html.label.weg.aus.einer.datei.ubernehmen": "Weg aus einer Datei übernehmen", "provider-placeholder": "[Anbieter, Anschrift, gespeicherte Daten und Speicherdauer ergänzen]",
"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.", "location-title": "3. Standortdaten",
"html.editor-route-save.strecke.speichern": "Strecke speichern", "location-data": "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.label.name": "Name", "progress-title": "4. Deinen Fortschritt merken",
"html.label.text": "Text", "progress-data": "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.div.bilder.und.ton.zur.station.hinzufugen": "Bilder und Ton zur Station hinzufügen", "progress-deletion": "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.editor-poi-audio-add.ton.hinzufugen": "Ton hinzufügen", "drafts-title": "5. Deine eigenen Strecken",
"html.editor-poi-images-add.bild.hinzufugen": "Bild hinzufügen", "draft-data": "Neue Strecken bleiben zuerst auf deinem Gerät. Gespeichert werden der Weg, die Orte der Stationen, Texte sowie ausgewählte Bilder und Tonaufnahmen.",
"html.editor-poi-media-preview.bilder.und.ton.ansehen": "Bilder und Ton ansehen", "draft-deletion": "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.editor-poi-save.station.speichern": "Station speichern", "settings-title": "6. Werbung und gespeicherte Einstellungen",
"html.span.station.entfernen": "Station entfernen", "settings-data": "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.",
"media.audio.count": { "children-title": "7. Inhalte von Schülerinnen und Schülern",
"one": "{count} Tonaufnahme", "children-permission": "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.",
"other": "{count} Tonaufnahmen" "rights-title": "8. Deine Rechte",
}, "rights-data": "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."
"media.image.count": { },
"one": "{count} Bild", "progress": {
"other": "{count} Bilder" "cookies-label": "Optionale Cookies",
}, "cookies-unused": "Diese Funktion wird nicht verwendet.",
"app.page.missing": "Diese Seite konnte nicht geöffnet werden.", "remember": "Meinen Fortschritt merken",
"app.location.searching": "Deinen Standort suchen", "remember-hint": "Merkt sich auf diesem Gerät, welche Stationen du schon besucht hast.",
"app.distance.spoken": "{value1} Meter entfernt", "choice-hint": "Du entscheidest, ob sich Wegwichtel deinen Fortschritt merkt.",
"app.distance.short": "{value1} m entfernt", "change-hint": "Die Änderung gilt sofort auf diesem Gerät."
"app.path.missing": "Der Weg dieser Strecke fehlt.", },
"app.location.start": "Los geht’s! Wir suchen deinen Standort.", "routes": {
"bootstrap.location": "Deinen Standort suchen …", "open-menu": "Strecke auswählen oder wechseln",
"bootstrap.step.failed": "Das hat nicht geklappt: {value1}", "menu-label": "Strecken auswählen und bearbeiten",
"bootstrap.ready": "Alles ist bereit.", "location-hint": "Wir suchen Strecken in deiner Nähe. Dein Standort bleibt auf diesem Gerät.",
"editor.station.up": "{value1} nach oben schieben", "nearby": "Strecken in deiner Nähe",
"editor.station.down": "{value1} nach unten schieben", "all": "Alle Strecken",
"editor.route.loading": "Deine Strecke wird geöffnet …", "search-label": "Strecken durchsuchen",
"editor.audio.removed": "Tonaufnahme entfernt.", "search-placeholder": "Name, Schule oder Beschreibung",
"editor.order.saved": "Reihenfolge gespeichert.", "clear-search": "Suche leeren",
"info.missing": "{value1}: Diese Information fehlt noch.", "drafts-hint": "Diese Strecken hast du auf diesem Gerät angelegt. Andere können sie noch nicht sehen.",
"local.device": "Auf diesem Gerät", "loading-drafts": "Meine Entwürfe werden geladen …",
"file.read.failed": "Die Datei konnte nicht geöffnet werden. Wähle sie noch einmal aus.", "back": "Zurück zur Streckenauswahl"
"offline.previous.kept": "Was du vorher gespeichert hast, bleibt erhalten.", },
"offline.error.previous.kept": "{value1} Was du vorher gespeichert hast, bleibt erhalten." "route": {
}); "length": "Länge",
"elevation": "Höhenmeter",
"station-count": "Stationen",
"controls-label": "Unterwegs",
"pause": "Strecke pausieren",
"stop": "Strecke beenden",
"back": "Zurück zur Strecke"
},
"station": {
"title": "Station"
},
"audio": {
"station-label": "Ton zur Station",
"unsupported": "Dieses Gerät kann den Ton leider nicht abspielen."
},
"navigation": {
"station-direction": "Richtung zum nächsten Station"
},
"contacts": {
"school": {
"address": "Zum Eichig 3",
"city": "98617 Meiningen / OT Herpf",
"phone": "Telefon: (036943) 63722",
"email": "E-Mail: info@grundschule-herpf.de",
"name": "Staatliche Grundschule Herpf",
"privacy-label": "Datenschutzangaben der Schule:",
"fax": "Telefax: (036943) 63843",
"head": "Frau Wichert, Schulleiterin",
"content-owner": "Frau Wichert"
},
"privacy": {
"role": "Datenschutzbeauftragter",
"address": "Hölderlinstraße 1",
"city": "98527 Suhl",
"phone": "Telefon: 03681 / 7341 25",
"fax": "Fax: 03681 / 7341 09",
"email": "E-Mail: datenschutz.suedthueriungen@schulamt.thueringen.de",
"name": "Staatliches Schulamt Südthüringen",
"postal-hint": "Der Datenschutzbeauftragte kann außerdem über die Postadresse der Schule mit dem Zusatz „der Datenschutzbeauftragte“ erreicht werden."
},
"authority": {
"label": "Zuständige Datenschutzaufsicht:",
"postal-address": "Postfach 90 04 55, 99107 Erfurt",
"address": "Häßlerstraße 8, 99096 Erfurt",
"phone": "Telefon: 0361 / 57 311 29 00",
"fax": "Telefax: 0361 / 57 311 29 04",
"email": "E-Mail: poststelle@datenschutz.thueringen.de",
"website": "Website: www.tlfdi.de",
"name": "Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)"
},
"sponsor": {
"role": "Landratsamt",
"address": "Obertshäuser Platz 1",
"city": "98617 Meiningen",
"name": "Landkreis Schmalkalden-Meiningen"
}
},
"imprint": {
"incomplete-notice": "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.",
"controller-title": "Wer für Wegwichtel verantwortlich ist",
"contact-title": "Kontakt",
"head-title": "Leitung der Schule",
"sponsor-title": "Schulträger",
"content-title": "Inhaltlich verantwortlich",
"required-title": "Weitere Pflichtangaben",
"required-data": "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.",
"liability-title": "Haftung für Inhalte und Verweise",
"liability-data": "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.",
"copyright-title": "Urheber- und Nutzungsrechte",
"copyright-data": "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."
},
"storage-help": {
"progress-title": "Was merkt sich Wegwichtel?",
"progress-data": "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.",
"progress-deletion": "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.",
"offline-title": "Ohne Internet unterwegs",
"offline-data": "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.",
"export-data": "Mit „Weg als Datei herunterladen“ kannst du den Weg in einer anderen Wander-App öffnen. Bilder und Tonaufnahmen sind in dieser Datei nicht enthalten.",
"drafts-title": "Deine eigenen Strecken",
"drafts-data": "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.",
"drafts-deletion": "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.",
"permissions-title": "Welche Erlaubnisse braucht die App?",
"permissions": {
"location": "Deinen Standort, um Strecken in deiner Nähe und den Weg zur nächsten Station zu zeigen.",
"direction": "Die Richtung deines Geräts, damit sich der Pfeil mitdrehen kann.",
"files": "Zugriff auf ausgewählte Bilder und Tonaufnahmen, wenn du sie zu einer Strecke hinzufügen möchtest.",
"optional": "Du kannst diese Erlaubnisse ablehnen. Dann fehlen die jeweiligen Funktionen. Strecken und Stationen kannst du weiterhin selbst auswählen."
},
"providers-title": "Werbung und andere Anbieter",
"providers-data": "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."
},
"media": {
"audio": {
"count": {
"one": "{count} Tonaufnahme",
"other": "{count} Tonaufnahmen"
}
},
"image": {
"count": {
"one": "{count} Bild",
"other": "{count} Bilder"
}
}
},
"file": {
"read": {
"failed": "Die Datei konnte nicht geöffnet werden. Wähle sie noch einmal aus."
}
}
}
);
} }
ns.TextsDe = WegwichtelTextsDe; ns.Texts = WegwichtelTexts;
ns.TextCatalogs ??= new Map();
ns.TextCatalogs.set(WegwichtelTexts.language, WegwichtelTexts.messages);
}(globalThis.Wegwichtel)); }(globalThis.Wegwichtel));
+532
View File
@@ -0,0 +1,532 @@
globalThis.Wegwichtel ??= {};
(function (ns) {
'use strict';
class WegwichtelTexts {
static language = 'en';
static messages = Object.freeze(
{
"app": {
"resume-hint": "You can carry on where you left off.",
"draft": "My draft",
"choose-route": "Choose a trail",
"route-menu": "Public trails and my drafts",
"route": "Trail",
"stations-complete": "All stops reached",
"no-stations": "No stops yet",
"next-station-summary": "Next stop: {value1}{value2}",
"published": "Published",
"public-route-count": "Public trails ({value1})",
"draft-count": "My drafts ({value1})",
"no-public-route": "There are no public trails yet.",
"no-route": "You have not created a trail of your own yet.",
"no-nearby-route": "No trail found within {value1} km. Choose any trail below.",
"location-permission-hint": "Allow location access to find trails near you. You can choose from all trails below.",
"no-search-result": "No matching trail found. Try a different word.",
"search-result-count": "{value1} of {value2} trails found.",
"route-count": "{value1} trails to choose from.",
"resume-route": "Continue trail",
"start-route": "Start trail",
"route-finished": "Trail finished.",
"pause-hint": "You are taking a break. The app is not checking your location right now.",
"route-paused": "Trail paused",
"empty-route": "This trail has no stops yet.",
"next-station-north": "North at the top · next stop: {value1}",
"next-station": "Next stop: {value1}",
"next-station-distance": "Next stop {value1}, {value2} metres away",
"destination-reached": "All stops and the finish reached",
"next-path-point-north": "North at the top · next point on the path",
"next-path-point": "Next point on the path",
"next-path-point-distance": "Next point on the path, {value1} metres away",
"location-accuracy": "Finding your location · accurate to about {value1} m",
"nearest-station-distance": " · next stop {value1} m away",
"stations-complete-suffix": " · all stops reached",
"resuming": "Continuing your trail.",
"resuming-title": "Continuing your trail",
"error": {
"location": "We could not find your location. Check that location access is allowed on your device.",
"load-routes": "The trails could not be loaded. Please try again.",
"load-drafts": "Your drafts could not be opened. Please try again.",
"open-route": "The trail could not be opened. Check your connection and try again.",
"open-draft": "The trail could not be opened. Please try again."
},
"navigation-north": "North at the top · directions",
"location-found": "Your location was found with an accuracy of about {value1} m. ",
"search-radius": "We show trails within {value1} km.",
"location-optional": "We could not find your location. You can still choose from all trails.",
"page": {
"missing": "This page could not be opened."
},
"location": {
"searching": "Finding your location",
"start": "Let’s go! We are finding your location."
},
"distance": {
"spoken": "{value1} metres away",
"short": "{value1} m away"
},
"path": {
"missing": "This trail has no path."
}
},
"bootstrap-loader": {
"steps": {
"storage": "Checking saved content …",
"jquery": "Getting Wegwichtel ready …",
"jquery-ui": "Getting the controls ready …",
"modules": "Getting the app ready …",
"server": "Checking the connection …",
"routes": "Loading trails …",
"location": "Finding your location …"
},
"hints": {
"chrome": "You can choose pictures and recordings directly on this device.",
"android": "Tip for Android: Chrome lets you choose pictures and recordings without saving them again."
},
"error": {
"start-error": "Part of the app could not start. Please try again.",
"load-error": "Part of the app could not be loaded. Check your internet connection and try again.",
"connection-error": "Wegwichtel could not start. Check your connection and try again.",
"init-error": "Wegwichtel could not start. Please try again.",
"step-error": "That did not work: {value1}"
},
"cleanup-progress": "Removing old progress ({value1} / {value2})",
"ready": "Everything is ready."
},
"editor": {
"error": {
"add-station": "Another stop cannot be added to this trail right now.",
"action": "This action could not be completed. Please try again.",
"draft-missing": "Your draft was not found.",
"create-locally": "Create a new trail on this device first.",
"route-name": "Give your trail a name.",
"send-file": "The file {value1} could not be sent. Please choose it again.",
"send-route": "The trail could not be sent. Please try again.",
"send-station": "The stop {value1} could not be sent. Please try again.",
"submit-own-draft": "You can only send your own drafts for review.",
"already-submitted": "This trail has already been sent for review.",
"location-required": "We need your location to add a new stop. Allow location access and try again.",
"save-route-first": "Save the trail first.",
"station-location": "This stop has no location. Allow location access and try again.",
"station-name": "Give your stop a name.",
"station-unsaved": "This stop has not been saved yet.",
"remove-image": "The picture could not be removed. Open the trail again.",
"generic": "That did not work. Please try again."
},
"untitled-route": "Untitled trail",
"untitled-station": "Untitled stop",
"edit-draft": "Edit my draft",
"new-route": "New trail",
"view-route": "View trail",
"edit-station": "Edit stop",
"add-station": "Add stop",
"published-route": "Published trail",
"edit-route": "Edit trail",
"delete-route": "Delete trail",
"station-number": "Stop {value1}",
"delete-item": "Delete {value1}",
"public-routes": "Public trails",
"drafts": "My drafts",
"no-draft": "There are no drafts yet.",
"no-stations": "There are no stops yet.",
"loading-routes": "Loading trails …",
"submitted": "Already sent for review",
"submit": "Send for review",
"submitted-hint": "This trail has been sent for review. Others can see it once it has been approved.",
"draft-hint": "This draft stays on your device. Choose “Send for review” to submit it for approval.",
"read-only-hint": "This trail is available to everyone and can no longer be changed here.",
"unpublished-hint": "This trail has not been made available to everyone yet.",
"remove-media-label": "{value1}: remove button {value2}",
"new-audio": "New recording",
"image-number": "Picture {value1}",
"new-image": "New picture {value1}",
"remove-media-hint": "{value1}. Tap a picture or recording to remove it.",
"no-media": "There are no pictures or recordings yet.",
"audio-section": "Recordings",
"image-section": "Pictures",
"draft-status": "Your new trail stays on this device for now.",
"saved": "Saved.",
"submitting": "Sending your trail …",
"submit-success": "Sent for review.",
"station-saved-with-media": "Stop saved on this device with pictures and recordings.",
"station-saved": "Stop saved.",
"image-removed": "Picture removed.",
"station-removed-and-saved": "Stop removed. Your trail has been saved.",
"station-removed": "Stop removed.",
"station-order-saved": "Stop order saved.",
"route-deleted": "Trail deleted.",
"open": "Edit trails",
"close": "Close the editing window",
"routes-title": "Your trails",
"create-route": "Create a new trail",
"routes-hint": "Here you can find public trails and your own drafts.",
"route-title": "Your trail",
"route-name": "Trail name",
"description": "Description",
"import-path": "Import a path from a file",
"new-draft-hint": "Your new trail stays on this device for now. Choose “Send for review” to submit it for approval.",
"save-route": "Save trail",
"station-name": "Name",
"station-text": "Text",
"media-actions-label": "Add pictures and recordings to this stop",
"add-audio": "Add recording",
"add-image": "Add picture",
"media-preview-label": "View pictures and recordings",
"save-station": "Save stop",
"remove-station": "Remove stop",
"station": {
"up": "Move {value1} up",
"down": "Move {value1} down"
},
"route": {
"loading": "Opening your trail …"
},
"audio": {
"removed": "Recording removed."
},
"order": {
"saved": "Order saved."
}
},
"location": {
"error": {
"unsupported": "This device cannot find your location."
}
},
"symbols": {
"wood": "Wood",
"rock": "Rock",
"lantern": "Lantern",
"flower": "Flower",
"pipe": "Pipe",
"bird": "Bird",
"mushroom": "Mushroom",
"strawberry": "Strawberry",
"pumpkin": "Pumpkin"
},
"info": {
"privacy": "Privacy notice",
"imprint": "Legal notice",
"storage": "Saving and permissions",
"open": "Open help and information",
"title": "Information",
"menu-label": "Legal information and app details",
"back": "Back to the information menu",
"close-label": "Close the information window",
"close": "Close",
"icon": "i",
"dropdown-label": "Help and information",
"missing": "{value1}: This information still needs to be added."
},
"local": {
"error": {
"route-storage": "Your trail cannot be saved on this device right now.",
"media-storage": "Pictures and recordings cannot be saved on this device right now.",
"media-read": "Saved pictures and recordings could not be opened.",
"other-tabs": "Close other Wegwichtel windows and try again.",
"media-access": "Pictures and recordings could not be opened or saved.",
"media-cancelled": "Saving pictures and recordings was cancelled.",
"media-quota-hint": "There is no room for more pictures or recordings. Remove pictures, recordings or your own trails that you no longer need.",
"media-quota": "There is no room for more pictures or recordings.",
"media-save": "The picture or recording could not be saved.",
"media-cleanup-hint": "{value1} Remove pictures, recordings or your own trails that you no longer need.",
"file-permission": "Allow access to the selected picture or recording again.",
"file-missing": "A selected picture or recording was moved or deleted. Choose the file again.",
"create-route": "A new trail cannot be created on this device right now.",
"invalid-track": "This file does not contain a readable path. Choose another file.",
"station-location": "The location of {value1} is missing.",
"route-quota": "There is no room for more trails.",
"save-route": "Your trail could not be saved.",
"route-cleanup-hint": "{value1} Remove your own trails that you no longer need.",
"path-missing": "Your trail has no path.",
"path-empty": "Your trail has no recorded path yet.",
"route-missing": "Your trail was not found.",
"file-unsupported": "This file cannot be read on your device.",
"file-read": "This file could not be opened. Choose it again."
},
"device": "On this device"
},
"offline": {
"error": {
"download": "Saving for your trip did not work. Please try again.",
"connection": "Wegwichtel could not be saved for your trip. Check your internet connection.",
"prepare": "Wegwichtel could not be saved for your trip. Please try again.",
"save": "Saving did not work.",
"busy": "Please wait until the other trail has been saved.",
"route-unsupported": "Trails cannot be saved for your trip on this device right now.",
"select-route": "Choose a trail that is available to everyone first.",
"load-route": "The trail could not be loaded.",
"published-only": "You can only save approved trails for your trip.",
"path-missing": "This trail has no path. It cannot be saved for your trip yet.",
"wait": "Please wait until saving is finished.",
"unsupported": "Wegwichtel cannot be saved for your trip on this device right now. You can still use it with internet.",
"save-app-first": "Save the app first. Then choose the trails you want to take with you.",
"previous": {
"kept": "{value1} Your previously saved content is kept."
}
},
"preparing": "Getting Wegwichtel ready for your trip …",
"download-progress": "Saving for your trip: {value1} of {value2}",
"ready": "Wegwichtel is ready for your trip.",
"cancelled": "Saving cancelled.",
"not-ready": "Wegwichtel is not ready to use without internet yet.",
"app-removed": "The saved app has been removed. Your trails are still saved.",
"renew-route": "Update saved trail",
"save-route": "Save for my trip",
"route-ready": "This trail is ready for your trip.",
"route-hint": "Take this trail with its pictures and recordings on your trip.",
"route-progress": "Saving trail: {value1} of {value2}",
"open-app": "Open the app without internet",
"open-app-hint": "Open Wegwichtel without internet. Then choose the trails you want to take with you.",
"help-title": "What is saved for my trip?",
"help-label": "Help with saving for your trip",
"help": {
"app-only": "This switch lets you open Wegwichtel without internet. It does not save any trails yet.",
"save-route": "Open a trail and tap “Save for my trip”. This saves the path, stops, pictures and recordings for your trip.",
"connection": "Wait until everything is saved before you set off. You need internet to save and update content.",
"keep-routes": "Everything stays on this device. This switch does not record your path. Turning it off keeps your saved trails and drafts.",
"storage-limits": "If you clear Wegwichtel’s saved data in your internet app, your trails will be lost too. Your device may remove saved content when space is low. Maps from other providers are not saved for your trip."
},
"renew-app": "Update saved app",
"route-controls-label": "Save this trail on your device",
"remove-route": "Remove saved trail",
"export-path": "Download the path as a file",
"route-actions-help": "“Save for my trip” saves this trail with pictures and recordings. “Download the path as a file” saves only the path for another walking app.",
"previous": {
"kept": "Your previously saved content is kept."
}
},
"offline-storage": {
"error": {
"foreign-origin": "Content from other websites cannot be saved here.",
"response": "Some content could not be saved. Check your internet connection and try again."
}
},
"slideshow": {
"image-label": "Picture for this stop",
"error": {
"load-image": "Picture could not be loaded"
},
"controls-label": "Picture controls",
"previous": "Previous picture",
"next": "Next picture"
},
"storage": {
"error": {
"estimate": "The free space could not be measured.",
"local-unavailable": "Text and settings cannot be saved here right now.",
"quota": "There is no space left. Remove saved trails, drafts, pictures or recordings that you no longer need. Even when the first bar is nearly empty, the space for text and settings may be full."
},
"site-warning": " Almost full: Remove content that you no longer need.",
"site-usage": "App, pictures and recordings: {value1}% of the available space used. About {value2} free.{value3}",
"local-warning": " The bar is almost full. Large pictures may prevent saving.",
"local-usage": "Text and settings: about {value1} used. The bar shows a guide of about 5 MB. How much it can actually hold depends on your internet app.{value2}",
"title": "Space for Wegwichtel",
"site-meter-label": "Space used by the app, pictures and recordings",
"loading": "Checking how much space is used …",
"local-meter-label": "Space used by text and settings; the bar shows a rough guide",
"help": "The first bar shows the space for the app, pictures and recordings. The second shows the space for text and settings. The bars only show space used by Wegwichtel.",
"refresh": "Check space"
},
"errors": {
"delete-published": "Only the person who looks after Wegwichtel can delete this trail.",
"edit-published": "This trail is available to everyone. You can no longer change it here.",
"invalid-track": "The file does not contain a readable path. Choose another file.",
"station-missing": "This stop was not found. Open the trail again.",
"station-location": "This stop has no location, or its location could not be read.",
"image-format": "This picture cannot be opened. Choose another picture.",
"audio-format": "This recording cannot be opened. Choose another recording.",
"file-size": "This file is too large. Choose a smaller file.",
"permission": "You are not allowed to do this here.",
"quota": "There is no space left. Remove saved trails, pictures or recordings that you no longer need.",
"unavailable": "Wegwichtel cannot be reached right now. Please try again later."
},
"vibration": {
"error": {
"pattern": "The vibration pattern must contain durations of zero milliseconds or more.",
"duration": "The vibration duration must be zero milliseconds or more."
}
},
"common": {
"app-name": "Wegwichtel",
"start": "Let’s go",
"retry": "Try again",
"notice": "Please note:",
"back": "Back"
},
"start": {
"intro": "You will soon be ready to explore with Wegwichtel.",
"preparing": "Getting Wegwichtel ready …",
"features-label": "Wegwichtel features",
"features": {
"routes": "Trails",
"explore": "Explore",
"exciting": "Exciting",
"stories": "Stories",
"child-friendly": "Child-friendly",
"experiences": "Adventures"
}
},
"privacy": {
"no-tracking": "There are no adverts here. We do not track which other websites you visit.",
"incomplete-notice": "This information is not complete yet. The contact details are from Grundschule Herpf. The website operator, the connection data stored and how long it is kept still need to be added.",
"controller-title": "1. Who is responsible and the privacy contact",
"connection-title": "2. When you open Wegwichtel",
"connection-data": "When you open Wegwichtel, connection data may be processed. This may include your device’s internet address, the date, time and time zone, the page you opened, data transferred and whether the request worked. It may also include the previous page and details of your device, internet app and its language. This data helps to provide the website, find problems and keep it safe.",
"provider-label": "Who provides the website, which connection data is stored and how long it is kept:",
"provider-placeholder": "[Add provider, address, stored data and retention period]",
"location-title": "3. Location data",
"location-data": "You decide whether Wegwichtel may use your location. This helps you find trails nearby and see the way to the next stop. Your current location is processed on your device. Wegwichtel does not save a history of your movements on the website.",
"progress-title": "4. Remembering your progress",
"progress-data": "When you turn on “Remember my progress”, Wegwichtel saves your selected trail, stops you have reached or viewed and recordings you have listened to. It also saves whether you are on the trail, taking a break or finished, and the time. This information stays on your device and is not sent to the website.",
"progress-deletion": "If your progress has not changed for at least one day, it is deleted the next time you open the app. Turning the switch off deletes it straight away. Your own trails and trails saved for your trip are kept.",
"drafts-title": "5. Your own trails",
"draft-data": "New trails stay on your device for now. The path, stop locations, text and selected pictures and recordings are saved.",
"draft-deletion": "Your drafts are kept when old progress is deleted or you turn off “Remember my progress”. They stay on this device until you delete them or clear Wegwichtel’s saved data in your internet app. Your trail is only sent to the website when you choose “Send for review”.",
"settings-title": "6. Adverts and saved settings",
"settings-data": "Wegwichtel does not use cookies. It does not count visits for advertising or track which other websites you visit. Your device saves whether you have turned on “Remember my progress”.",
"children-title": "7. Content from schoolchildren",
"children-permission": "Pictures, text and recordings may only be shared when permission has been given. If children can be recognised in pictures or recordings, the school’s rules must be followed. Parents or other people with parental responsibility must also give the necessary permission.",
"rights-title": "8. Your rights",
"rights-data": "You can ask what data about you is stored. Where the law allows, you can ask for it to be corrected, deleted, used only in a limited way or provided to you so you can pass it on. You can object to its use and withdraw permission for future use. If you want to complain about how your data is handled, you can contact the data protection authority below."
},
"progress": {
"cookies-label": "Optional cookies",
"cookies-unused": "This feature is not used.",
"remember": "Remember my progress",
"remember-hint": "Remembers which stops you have already visited on this device.",
"choice-hint": "You choose whether Wegwichtel remembers your progress.",
"change-hint": "The change takes effect straight away on this device."
},
"routes": {
"open-menu": "Choose or switch trails",
"menu-label": "Choose and edit trails",
"location-hint": "Finding trails near you. Your location stays on this device.",
"nearby": "Trails near you",
"all": "All trails",
"search-label": "Search trails",
"search-placeholder": "Name, school or description",
"clear-search": "Clear search",
"drafts-hint": "You created these trails on this device. Others cannot see them yet.",
"loading-drafts": "Loading my drafts …",
"back": "Back to trail selection"
},
"route": {
"length": "Length",
"elevation": "Elevation gain",
"station-count": "Stops",
"controls-label": "On your trail",
"pause": "Pause trail",
"stop": "Finish trail",
"back": "Back to the trail"
},
"station": {
"title": "Stop"
},
"audio": {
"station-label": "Recording for this stop",
"unsupported": "This device cannot play the recording."
},
"navigation": {
"station-direction": "Direction to the next stop"
},
"contacts": {
"school": {
"address": "Zum Eichig 3",
"city": "98617 Meiningen / OT Herpf",
"phone": "Phone: (036943) 63722",
"email": "Email: info@grundschule-herpf.de",
"name": "Staatliche Grundschule Herpf",
"privacy-label": "The school’s privacy contact:",
"fax": "Fax: (036943) 63843",
"head": "Ms Wichert, headteacher",
"content-owner": "Ms Wichert"
},
"privacy": {
"role": "Data protection officer",
"address": "Hölderlinstraße 1",
"city": "98527 Suhl",
"phone": "Phone: 03681 / 7341 25",
"fax": "Fax: 03681 / 7341 09",
"email": "Email: datenschutz.suedthueriungen@schulamt.thueringen.de",
"name": "Staatliches Schulamt Südthüringen",
"postal-hint": "You can also contact the data protection officer by writing to the school’s postal address and adding “der Datenschutzbeauftragte”."
},
"authority": {
"label": "Responsible data protection authority:",
"postal-address": "Postfach 90 04 55, 99107 Erfurt",
"address": "Häßlerstraße 8, 99096 Erfurt",
"phone": "Phone: 0361 / 57 311 29 00",
"fax": "Fax: 0361 / 57 311 29 04",
"email": "Email: poststelle@datenschutz.thueringen.de",
"website": "Website: www.tlfdi.de",
"name": "Thüringer Landesbeauftragter für den Datenschutz und die Informationsfreiheit (TlfDI)"
},
"sponsor": {
"role": "District administration",
"address": "Obertshäuser Platz 1",
"city": "98617 Meiningen",
"name": "Landkreis Schmalkalden-Meiningen"
}
},
"imprint": {
"incomplete-notice": "This information comes from Grundschule Herpf. Before publication, check whether Wegwichtel needs any additional or different information.",
"controller-title": "Who is responsible for Wegwichtel",
"contact-title": "Contact",
"head-title": "School leadership",
"sponsor-title": "School authority",
"content-title": "Responsible for content",
"required-title": "Further required information",
"required-data": "Depending on who operates Wegwichtel, further information may be required. This may include the people responsible for operating the website. These details still need to be checked.",
"liability-title": "Responsibility for content and links",
"liability-data": "The content is prepared carefully and checked regularly. If something is wrong, out of date or not allowed, please use the contact details above to let us know. Other website operators are responsible for their own linked pages.",
"copyright-title": "Copyright and rights of use",
"copyright-data": "Unless stated otherwise, the rights belong to Grundschule Herpf. You may only use text, pictures, recordings and paths in the ways permitted for each item. The rights of the children, families, teachers and other people involved remain in place."
},
"storage-help": {
"progress-title": "What does Wegwichtel remember?",
"progress-data": "You can save your progress on this device. Next time you open Wegwichtel, it will know which stops you have visited or viewed and which recordings you have listened to.",
"progress-deletion": "If nothing has changed for one day, your progress is deleted the next time you open the app. Turning the switch off deletes it straight away. Your own trails and trails saved for your trip are kept.",
"offline-title": "Out and about without internet",
"offline-data": "“Open the app without internet” saves Wegwichtel on this device. You can then take individual trails with you using “Save for my trip”. Wait until everything is saved before you set off.",
"export-data": "“Download the path as a file” lets you open the path in another walking app. Pictures and recordings are not included in this file.",
"drafts-title": "Your own trails",
"drafts-data": "New trails stay on this device for now. Choose “Send for review” to submit a trail for approval. Others can see it once it has been approved.",
"drafts-deletion": "Your drafts are saved until you delete them or clear Wegwichtel’s saved data in your internet app. Lots of large pictures take up space. The “Space for Wegwichtel” display helps you keep an eye on the space used.",
"permissions-title": "Which permissions does the app need?",
"permissions": {
"location": "Your location, to show trails near you and the way to the next stop.",
"direction": "The direction your device is facing, so the arrow can turn with it.",
"files": "Access to selected pictures and recordings when you want to add them to a trail.",
"optional": "You can refuse these permissions. The matching features will then be unavailable. You can still choose trails and stops yourself."
},
"providers-title": "Adverts and other providers",
"providers-data": "Wegwichtel does not show adverts or track which other websites you visit. The app loads its own programs and pictures from Wegwichtel. No cookies are used."
},
"media": {
"audio": {
"count": {
"one": "{count} recording",
"other": "{count} recordings"
}
},
"image": {
"count": {
"one": "{count} picture",
"other": "{count} pictures"
}
}
},
"file": {
"read": {
"failed": "The file could not be opened. Choose it again."
}
}
}
);
}
ns.Texts = WegwichtelTexts;
ns.TextCatalogs ??= new Map();
ns.TextCatalogs.set(WegwichtelTexts.language, WegwichtelTexts.messages);
}(globalThis.Wegwichtel));
+21 -14
View File
@@ -14,35 +14,42 @@ globalThis.Wegwichtel ??= {};
} }
const explanations = [ const explanations = [
[/Freigegebene Strecken dürfen nur/, "user-messages.diese.strecke.kann.nur.die.person.loschen"], [/Freigegebene Strecken dürfen nur/, "errors.delete-published"],
[/Freigegebene Strecken können/, "user-messages.diese.strecke.ist.fur.alle.freigegeben.du"], [/Freigegebene Strecken können/, "errors.edit-published"],
[/GPX|XML|Trackpunkte/, "user-messages.die.datei.enthalt.keinen.lesbaren.weg.wahle"], [/GPX|XML|Trackpunkte/, "errors.invalid-track"],
[/POI.*nicht gefunden/, "user-messages.diese.station.wurde.nicht.gefunden.offne.die"], [/POI.*nicht gefunden/, "errors.station-missing"],
[/POI-Koordinaten/, "user-messages.der.ort.dieser.station.fehlt.oder.konnte"], [/POI-Koordinaten/, "errors.station-location"],
[/Bildformat|Bilddatei/, "user-messages.dieses.bild.kann.nicht.geoffnet.werden.wahle"], [/Bildformat|Bilddatei/, "errors.image-format"],
[/Audioformat|Audiodatei/, "user-messages.diese.tonaufnahme.kann.nicht.geoffnet.werden.wahle"] [/Audioformat|Audiodatei/, "errors.audio-format"]
]; ];
const statusExplanations = new Map([ const statusExplanations = new Map([
[413, "user-messages.diese.datei.ist.zu.gross.wahle.eine"], [413, "errors.file-size"],
[401, "user-messages.du.darfst.diese.aktion.hier.nicht.ausfuhren"], [401, "errors.permission"],
[403, "user-messages.du.darfst.diese.aktion.hier.nicht.ausfuhren"] [403, "errors.permission"]
]); ]);
function readableMessage(message, fallback) { 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(?: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 (/\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; if (message && readablePatterns.some(pattern => pattern.test(message))) return message;
return fallback; return fallback;
} }
function errorMessage(error, fallback = ns.I18n.t("editor-ui.das.hat.leider.nicht.geklappt.bitte.versuche")) { const readablePatterns = [
/[äöüÄÖÜß]/,
/\b(?:The|This|Your|Please|Choose|Allow|Give|Save|Remove|Trail|Stop|There)\b/,
/\b(?:Die|Der|Das|Dein|Deine|Diese|Dieser|Dieses|Bitte|Wähle|Erlaube)\b/,
/\b(?:Ein|Eine|Kein|Keine|Speicher|Strecke|Station|Wegwichtel|Gespeichert|Entferne)\b/
];
function errorMessage(error, fallback = ns.I18n.t("editor.error.generic")) {
const message = messageText(error); 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"); if (error?.name === 'QuotaExceededError' || error?.cause?.name === 'QuotaExceededError') return ns.I18n.t("errors.quota");
const explanation = explanations.find(([pattern]) => pattern.test(message)); const explanation = explanations.find(([pattern]) => pattern.test(message));
if (explanation) return ns.I18n.t(explanation[1]); if (explanation) return ns.I18n.t(explanation[1]);
const statusExplanation = statusExplanations.get(error?.status); const statusExplanation = statusExplanations.get(error?.status);
if (statusExplanation) return ns.I18n.t(statusExplanation); if (statusExplanation) return ns.I18n.t(statusExplanation);
if (error?.status >= 500) return ns.I18n.t("user-messages.wegwichtel.ist.gerade.nicht.erreichbar.bitte.versuche"); if (error?.status >= 500) return ns.I18n.t("errors.unavailable");
return readableMessage(message, fallback); return readableMessage(message, fallback);
} }
+2 -2
View File
@@ -21,14 +21,14 @@
if (Array.isArray(selected)) { if (Array.isArray(selected)) {
const values = selected.map(Number); const values = selected.map(Number);
if (!values.length || values.some(value => !Number.isFinite(value) || value < 0)) { if (!values.length || values.some(value => !Number.isFinite(value) || value < 0)) {
throw new TypeError(ns.I18n.t("vibration.das.vibrationsmuster.muss.aus.nichtnegativen.millisekunden.bestehen")); throw new TypeError(ns.I18n.t("vibration.error.pattern"));
} }
return values; return values;
} }
const duration = Number(selected); const duration = Number(selected);
if (!Number.isFinite(duration) || duration < 0) { if (!Number.isFinite(duration) || duration < 0) {
throw new TypeError(ns.I18n.t("vibration.die.vibrationsdauer.muss.eine.nichtnegative.millisekundenangabe.sein")); throw new TypeError(ns.I18n.t("vibration.error.duration"));
} }
return [duration]; return [duration];
} }
+1 -1
View File
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
// Serve app files and individually selected route snapshots independently (v2). // Serve app files and individually selected route snapshots independently (v2).
importScripts('/js/texts-de.js', '/js/i18n.js', '/js/offline-storage.js'); importScripts('/js/texts-de.js', '/js/texts-en.js', '/js/i18n.js', '/js/offline-storage.js');
globalThis.addEventListener('install', event => event.waitUntil(globalThis.skipWaiting())); globalThis.addEventListener('install', event => event.waitUntil(globalThis.skipWaiting()));
globalThis.addEventListener('activate', event => event.waitUntil(globalThis.clients.claim())); globalThis.addEventListener('activate', event => event.waitUntil(globalThis.clients.claim()));
+30
View File
@@ -0,0 +1,30 @@
import fs from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
const escapeText = value => value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
// Populate the checked-in German fallback while retaining all runtime i18n markers.
export function localizeHtml(source, translate) {
const result = source.replaceAll(/(<[^>]+\bdata-i18n="([^"]+)"[^>]*>)([^<]*)(<\/)/g,
(_match, opening, key, _text, closing) => opening + escapeText(translate(key)) + closing);
return result.replaceAll(/<[^>]+>/g, opening => {
let translated = opening;
for (const attribute of ['title', 'aria-label', 'placeholder', 'alt']) {
const key = new RegExp(`data-i18n-${attribute}="([^"]+)"`).exec(opening)?.[1];
if (!key) continue;
const value = escapeText(translate(key)).replaceAll('"', '&quot;');
// Match an actual attribute, never the suffix of its data-i18n marker.
translated = translated.replaceAll(new RegExp(`(\\s)${attribute}="[^"]*"`, 'g'), (_match, space) => `${space}${attribute}="${value}"`);
}
return translated;
});
}
if (process.argv[1] === fileURLToPath(import.meta.url)) {
await import('../public/js/texts-de.js');
await import('../public/js/i18n.js');
const file = new URL('../public/index.html', import.meta.url);
const source = await fs.readFile(file, 'utf8');
const translated = localizeHtml(source, key => globalThis.Wegwichtel.I18n.t(key));
await fs.writeFile(file, translated);
}
+2
View File
@@ -1,2 +1,4 @@
sonar.javascript.lcov.reportPaths=coverage/js/lcov.info sonar.javascript.lcov.reportPaths=coverage/js/lcov.info
sonar.python.coverage.reportPaths=coverage/python/coverage.xml sonar.python.coverage.reportPaths=coverage/python/coverage.xml
# Language catalogs intentionally share keys and structure; keep code/coverage analysis.
sonar.cpd.exclusions=public/js/texts-*.js
+3 -13
View File
@@ -1,31 +1,21 @@
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import vm from 'node:vm'; import vm from 'node:vm';
import { localizeHtml } from '../scripts/localize-html.js';
const root = path.resolve(import.meta.dirname, '..'); const root = path.resolve(import.meta.dirname, '..');
export async function loadI18n(context) { export async function loadI18n(context) {
for (const name of ['texts-de', 'i18n']) { for (const name of ['texts-de', 'texts-en', 'i18n']) {
const filename = path.join(root, `public/js/${name}.js`); const filename = path.join(root, `public/js/${name}.js`);
vm.runInNewContext(await fs.readFile(filename, 'utf8'), context, { filename }); vm.runInNewContext(await fs.readFile(filename, 'utf8'), context, { filename });
} }
return context.Wegwichtel.I18n; return context.Wegwichtel.I18n;
} }
const escapeText = value => value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;');
export async function localizedHtml(source) { export async function localizedHtml(source) {
const i18n = await loadI18n({}); const i18n = await loadI18n({});
let result = source.replace(/(<[^>]+\bdata-i18n="([^"]+)"[^>]*>)([^<]*)(<\/)/g, const result = localizeHtml(source, key => i18n.t(key));
(_match, opening, key, _text, closing) => opening + escapeText(i18n.t(key)) + closing);
result = result.replace(/<[^>]+>/g, opening => {
let translated = opening;
for (const attribute of ['title', 'aria-label', 'placeholder', 'alt']) {
const key = new RegExp(`data-i18n-${attribute}="([^"]+)"`).exec(opening)?.[1];
if (key) translated = translated.replace(new RegExp(`\\b${attribute}="[^"]*"`), `${attribute}="${escapeText(i18n.t(key)).replaceAll('"', '&quot;')}"`);
}
return translated;
});
return result.replace(/\sdata-i18n(?:-[\w-]+)?="[^"]*"/g, ''); return result.replace(/\sdata-i18n(?:-[\w-]+)?="[^"]*"/g, '');
} }
+13 -1
View File
@@ -224,7 +224,7 @@ function route(id, name, source = 'server') {
}; };
} }
async function loadApp() { async function loadApp({ language = 'de-DE' } = {}) {
const sourcePath = path.join(root, 'public/js/app.js'); const sourcePath = path.join(root, 'public/js/app.js');
const source = await fs.readFile(sourcePath, 'utf8'); const source = await fs.readFile(sourcePath, 'utf8');
const harness = createHarness(); const harness = createHarness();
@@ -299,6 +299,7 @@ async function loadApp() {
context.addEventListener = (name, handler) => harness.globalListeners.set(name, handler); context.addEventListener = (name, handler) => harness.globalListeners.set(name, handler);
context.globalThis = context; context.globalThis = context;
context.navigator = { language };
await loadI18n(context); await loadI18n(context);
@@ -318,6 +319,17 @@ async function loadApp() {
}; };
} }
test('English browser settings localize app controls and retain user-authored trail names', async () => {
const { context, harness } = await loadApp({ language: 'en-GB' });
await context.Wegwichtel.App.initialize({ markStep() {} });
assert.equal(harness.node('#route-control-title').textValue, 'Choose a trail');
assert.equal(harness.node('#route-control-server-title').textValue, 'Public trails (1)');
assert.equal(harness.node('#route-control-local-title').textValue, 'My drafts (1)');
assert.match(harness.node('#location-hint').textValue, /You can still choose from all trails/);
assert.ok(harness.node('#all-route-list').appended.join('').includes('Serverweg'));
assert.ok(harness.node('#local-route-list').appended.join('').includes('Lokaler Weg'));
});
test('route control runtime loads server/local routes and supports switching without geolocation', async () => { test('route control runtime loads server/local routes and supports switching without geolocation', async () => {
const setup = await loadApp(); const setup = await loadApp();
const { context, harness, apiCalls, editor } = setup; const { context, harness, apiCalls, editor } = setup;
+4 -6
View File
@@ -36,10 +36,8 @@ test('bootstrap status text fades between loading steps and is replaced by the s
assert.match(fadingRule, /opacity:\s*0/); assert.match(fadingRule, /opacity:\s*0/);
assert.match(loader, /statusText\.classList\.add\('is-fading'\)/); assert.match(loader, /statusText\.classList\.add\('is-fading'\)/);
assert.match(loader, /statusText\.classList\.remove\('is-fading'\)/); assert.match(loader, /statusText\.classList\.remove\('is-fading'\)/);
assert.match(loader, /get storage\(\) \{ return globalThis\.Wegwichtel\.I18n\.t\(/); assert.match(loader, /const stepLabel = step => globalThis\.Wegwichtel\.I18n\.t\(`bootstrap-loader\.steps\.\$\{step\}`\)/);
assert.match(loader, /get server\(\) \{ return globalThis\.Wegwichtel\.I18n\.t\(/); assert.match(loader, /steps\.includes\(step\)/);
assert.match(loader, /get routes\(\) \{ return globalThis\.Wegwichtel\.I18n\.t\(/);
assert.match(loader, /get location\(\) \{ return globalThis\.Wegwichtel\.I18n\.t\(/);
assert.match(loader, /actionStatus\?\.classList\.add\('is-leaving'\)/); assert.match(loader, /actionStatus\?\.classList\.add\('is-leaving'\)/);
assert.match(loader, /enter\.classList\.add\('is-visible'\)/); assert.match(loader, /enter\.classList\.add\('is-visible'\)/);
assert.match(loader, /const completedSteps = new Set\(\)/); assert.match(loader, /const completedSteps = new Set\(\)/);
@@ -59,6 +57,6 @@ test('Android start hint distinguishes Chrome from other Android browsers', asyn
assert.match(loader, /getElementById\('android-browser-hint'\)/); assert.match(loader, /getElementById\('android-browser-hint'\)/);
assert.match(loader, /\/Android\/i/); assert.match(loader, /\/Android\/i/);
assert.match(loader, /\/Chrome\\\//i); assert.match(loader, /\/Chrome\\\//i);
assert.match(loader, /I18n\.t\("bootstrap-loader\.auf\.diesem\.gerat\.kannst\.du\.bilder\.und"\)/); assert.match(loader, /I18n\.t\("bootstrap-loader\.hints\.chrome"\)/);
assert.match(loader, /I18n\.t\("bootstrap-loader\.tipp\.fur\.android\.mit\.chrome\.lassen\.sich"\)/); assert.match(loader, /I18n\.t\("bootstrap-loader\.hints\.android"\)/);
}); });
+97 -9
View File
@@ -3,19 +3,44 @@ import assert from 'node:assert/strict';
import fs from 'node:fs/promises'; import fs from 'node:fs/promises';
import path from 'node:path'; import path from 'node:path';
import test from 'node:test'; import test from 'node:test';
import vm from 'node:vm';
import { localizeHtml } from '../scripts/localize-html.js';
const root = path.resolve(import.meta.dirname, '..'); const root = path.resolve(import.meta.dirname, '..');
test('checked-in HTML has synchronized German defaults and accessible headings, labels and links before scripts load', async () => {
const i18n = await loadI18n({});
const html = await fs.readFile(path.join(root, 'public/index.html'), 'utf8');
assert.equal(localizeHtml(html, key => i18n.t(key)), html, 'Run node scripts/localize-html.js after editing German texts');
for (const match of html.matchAll(/<h[1-6]\b[^>]*>([\s\S]*?)<\/h[1-6]>/g)) assert.ok(match[1].replaceAll(/<[^>]*>/g, '').trim(), match[0]);
for (const match of html.matchAll(/\baria-label="([^"]*)"/g)) assert.ok(match[1].trim(), 'Empty accessible label');
for (const match of html.matchAll(/<a\b[^>]*>([\s\S]*?)<\/a>/g)) assert.ok(match[1].replaceAll(/<[^>]*>/g, '').trim(), match[0]);
for (const match of html.matchAll(/<label\b([^>]*)>/g)) {
const control = /\bfor="([^"]+)"/.exec(match[1])?.[1];
assert.ok(control, `Missing label association: ${match[0]}`);
assert.ok(html.includes(`id="${control}"`), `Missing control: ${control}`);
}
});
test('HTML generation escapes text and attributes, preserves i18n markers and updates existing defaults', () => {
const source = '<h1 data-i18n="heading">Old title</h1><button aria-label="Old label" data-i18n-aria-label="label">?</button>';
const messages = { heading: '<New & title>', label: 'A "label" & $&' };
const translate = key => messages[key];
const result = localizeHtml(source, translate);
assert.equal(result, '<h1 data-i18n="heading">&lt;New &amp; title&gt;</h1><button aria-label="A &quot;label&quot; &amp; $&amp;" data-i18n-aria-label="label">?</button>');
assert.equal(localizeHtml(result, translate), result);
});
test('translations support placeholders, plurals and German fallback without treating text as HTML', async () => { test('translations support placeholders, plurals and German fallback without treating text as HTML', async () => {
const context = {}; const context = {};
const i18n = await loadI18n(context); const i18n = await loadI18n(context);
assert.equal(i18n.t('app.strecke.wahlen'), 'Strecke wählen'); assert.equal(i18n.t('app.choose-route'), 'Strecke wählen');
assert.equal(i18n.t('media.image.count', { count: 1 }), '1 Bild'); assert.equal(i18n.t('media.image.count', { count: 1 }), '1 Bild');
assert.equal(i18n.t('media.image.count', { count: 2 }), '2 Bilder'); assert.equal(i18n.t('media.image.count', { count: 2 }), '2 Bilder');
assert.equal(i18n.t('app.nachste.station', { value1: '<script>', value2: ' · 3 m' }), 'Nächste Station: <script> · 3 m'); assert.equal(i18n.t('app.next-station-summary', { value1: '<script>', value2: ' · 3 m' }), 'Nächste Station: <script> · 3 m');
i18n.register('en', { 'app.strecke.wahlen': 'Choose a trail', 'media.image.count': { one: '{count} picture', other: '{count} pictures' } }); i18n.register('en', { 'app.choose-route': 'Choose a trail', 'media.image.count': { one: '{count} picture', other: '{count} pictures' } });
assert.equal(i18n.setLanguage('en-GB', null), 'en'); assert.equal(i18n.setLanguage('en-GB', null), 'en');
assert.equal(i18n.t('app.strecke.wahlen'), 'Choose a trail'); assert.equal(i18n.t('app.choose-route'), 'Choose a trail');
assert.equal(i18n.t('media.image.count', { count: 0 }), '0 pictures'); assert.equal(i18n.t('media.image.count', { count: 0 }), '0 pictures');
assert.equal(i18n.t('media.audio.count', { count: 1 }), '1 Tonaufnahme'); assert.equal(i18n.t('media.audio.count', { count: 1 }), '1 Tonaufnahme');
assert.equal(i18n.setLanguage('fr', null), 'de'); assert.equal(i18n.setLanguage('fr', null), 'de');
@@ -24,10 +49,15 @@ test('translations support placeholders, plurals and German fallback without tre
test('language changes translate visible and spoken labels through textContent and retain text markers', async () => { test('language changes translate visible and spoken labels through textContent and retain text markers', async () => {
const i18n = await loadI18n({}); const i18n = await loadI18n({});
const make = key => ({ textContent: '', attributes: new Map(key), getAttribute(name) { return this.attributes.get(name); }, setAttribute(name, value) { this.attributes.set(name, value); } }); const make = entries => ({
const text = make([['data-i18n', 'app.strecke.wahlen']]); textContent: '',
attributes: new Map(entries),
dataset: Object.fromEntries(entries.map(([name, value]) => [name.slice(5).replaceAll(/-([a-z])/g, (_match, letter) => letter.toUpperCase()), value])),
setAttribute(name, value) { this.attributes.set(name, value); }
});
const text = make([['data-i18n', 'app.choose-route']]);
const attributes = ['aria-label', 'title', 'placeholder', 'alt']; const attributes = ['aria-label', 'title', 'placeholder', 'alt'];
const labels = new Map(attributes.map(attribute => [attribute, make([[`data-i18n-${attribute}`, 'app.strecke.wahlen']])])); const labels = new Map(attributes.map(attribute => [attribute, make([[`data-i18n-${attribute}`, 'app.choose-route']])]));
const root = { documentElement: {}, querySelectorAll(selector) { const root = { documentElement: {}, querySelectorAll(selector) {
if (selector === '[data-i18n]') return [text]; if (selector === '[data-i18n]') return [text];
for (const [attribute, label] of labels) { for (const [attribute, label] of labels) {
@@ -38,12 +68,12 @@ test('language changes translate visible and spoken labels through textContent a
i18n.apply(root); i18n.apply(root);
assert.equal(text.textContent, 'Strecke wählen'); assert.equal(text.textContent, 'Strecke wählen');
for (const [attribute, label] of labels) assert.equal(label.attributes.get(attribute), 'Strecke wählen'); for (const [attribute, label] of labels) assert.equal(label.attributes.get(attribute), 'Strecke wählen');
i18n.register('en', { 'app.strecke.wahlen': '<Choose a trail>' }); i18n.register('en', { 'app.choose-route': '<Choose a trail>' });
i18n.setLanguage('en', root); i18n.setLanguage('en', root);
assert.equal(text.textContent, '<Choose a trail>'); assert.equal(text.textContent, '<Choose a trail>');
for (const [attribute, label] of labels) assert.equal(label.attributes.get(attribute), '<Choose a trail>'); for (const [attribute, label] of labels) assert.equal(label.attributes.get(attribute), '<Choose a trail>');
assert.equal(root.documentElement.lang, 'en'); assert.equal(root.documentElement.lang, 'en');
assert.equal(text.attributes.get('data-i18n'), 'app.strecke.wahlen'); assert.equal(text.attributes.get('data-i18n'), 'app.choose-route');
}); });
test('every static HTML and JavaScript text key exists in the central catalog and translations load before app modules', async () => { test('every static HTML and JavaScript text key exists in the central catalog and translations load before app modules', async () => {
@@ -56,5 +86,63 @@ test('every static HTML and JavaScript text key exists in the central catalog an
for (const match of source.matchAll(/I18n\.t\(["']([^"']+)["']/g)) assert.equal(typeof i18n.t(match[1], { count: 2 }), 'string', match[1]); for (const match of source.matchAll(/I18n\.t\(["']([^"']+)["']/g)) assert.equal(typeof i18n.t(match[1], { count: 2 }), 'string', match[1]);
} }
assert.ok(html.indexOf('js/texts-de.js') < html.indexOf('js/i18n.js')); assert.ok(html.indexOf('js/texts-de.js') < html.indexOf('js/i18n.js'));
assert.ok(html.indexOf('js/texts-en.js') > html.indexOf('js/texts-de.js'));
assert.ok(html.indexOf('js/texts-en.js') < html.indexOf('js/i18n.js'));
assert.ok(html.indexOf('js/i18n.js') < html.indexOf('js/local-routes.js')); assert.ok(html.indexOf('js/i18n.js') < html.indexOf('js/local-routes.js'));
}); });
test('browser language preferences select German or English, including regional variants and unavailable languages', async () => {
const cases = [
[{ languages: ['en-GB', 'de-DE'], language: 'de-DE' }, 'en', 'Choose a trail'],
[{ languages: ['de-DE', 'en-US'] }, 'de', 'Strecke wählen'],
[{ languages: ['fr-FR', 'en-US'] }, 'en', 'Choose a trail'],
[{ language: 'EN-us' }, 'en', 'Choose a trail'],
[{ languages: ['fr-FR'] }, 'de', 'Strecke wählen'],
[{}, 'de', 'Strecke wählen']
];
for (const [navigator, language, title] of cases) {
const i18n = await loadI18n({ navigator });
assert.equal(i18n.language, language);
assert.equal(i18n.t('app.choose-route'), title);
assert.equal(i18n.t('bootstrap-loader.steps.routes'), language === 'en' ? 'Loading trails …' : 'Strecken laden …');
}
});
function leafMessages(node, prefix = '') {
return Object.entries(node).flatMap(([key, value]) => {
const path = prefix + key;
return typeof value === 'string' ? [[path, value]] : leafMessages(value, `${path}.`);
});
}
test('German and English catalogs have the same semantic keys, placeholders and plural forms', async () => {
const context = {};
const i18n = await loadI18n(context);
const de = new Map(leafMessages(context.Wegwichtel.TextCatalogs.get('de')));
const en = new Map(leafMessages(context.Wegwichtel.TextCatalogs.get('en')));
assert.deepEqual([...en.keys()].sort(), [...de.keys()].sort());
for (const [key, text] of de) {
assert.ok(en.get(key).trim(), key);
const placeholders = value => [...value.matchAll(/\{(\w+)\}/g)].map(match => match[1]).sort();
assert.deepEqual(placeholders(en.get(key)), placeholders(text), key);
assert.match(key, /^[a-z][a-z0-9.-]*$/, key);
assert.ok(key.split('.').length <= 4, key);
}
i18n.setLanguage('en', null);
assert.equal(i18n.t('app.no-route'), 'You have not created a trail of your own yet.');
assert.equal(i18n.t('media.audio.count', { count: 1 }), '1 recording');
assert.equal(i18n.t('media.audio.count', { count: 2 }), '2 recordings');
for (const step of ['storage', 'jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location']) assert.ok(i18n.t(`bootstrap-loader.steps.${step}`));
});
test('the generic text class and i18n can start with only the English language file', async () => {
const context = { navigator: { language: 'en-GB' } };
for (const name of ['texts-en', 'i18n']) {
const filename = path.join(root, `public/js/${name}.js`);
vm.runInNewContext(await fs.readFile(filename, 'utf8'), context, { filename });
}
assert.equal(context.Wegwichtel.Texts.name, 'WegwichtelTexts');
assert.equal(context.Wegwichtel.I18n.language, 'en');
assert.equal(context.Wegwichtel.I18n.t('app.choose-route'), 'Choose a trail');
assert.equal(context.Wegwichtel.I18n.setLanguage('fr', null), 'en');
});
+1 -1
View File
@@ -23,7 +23,7 @@ test('start screen exposes cookie status, optional local progress and deliberate
assert.match(html, /role="switch"[^>]*aria-checked="false"/); assert.match(html, /role="switch"[^>]*aria-checked="false"/);
assert.match(html, /id="storage-cleanup-status"/); assert.match(html, /id="storage-cleanup-status"/);
assert.match(html, /id="bootstrap-enter"[^>]*hidden/); assert.match(html, /id="bootstrap-enter"[^>]*hidden/);
assert.match(loader, /I18n\.t\("bootstrap-loader\.alten\.fortschritt\.loschen", \{ value1: removed, value2: total \}\)/); assert.match(loader, /I18n\.t\("bootstrap-loader\.cleanup-progress", \{ value1: removed, value2: total \}\)/);
assert.match(loader, /showReadyState\(\)/); assert.match(loader, /showReadyState\(\)/);
assert.doesNotMatch(loader, /getElementById\('bootstrap-screen'\)\.hidden = true/); assert.doesNotMatch(loader, /getElementById\('bootstrap-screen'\)\.hidden = true/);
assert.match(loader, /event\.target\.closest\('button, input, label, a, dialog'\)/); assert.match(loader, /event\.target\.closest\('button, input, label, a, dialog'\)/);
+1 -1
View File
@@ -153,7 +153,7 @@ test('Stationen have live direction and metre indicators and take navigation pri
assert.match(app, /function updatePoiIndicators\(position\)/); assert.match(app, /function updatePoiIndicators\(position\)/);
assert.match(app, /function nearestPendingPoi\(metrics\)/); assert.match(app, /function nearestPendingPoi\(metrics\)/);
assert.match(app, /if \(nextPoi\) showPoiNavigation\(position, nextPoi\)/); assert.match(app, /if \(nextPoi\) showPoiNavigation\(position, nextPoi\)/);
assert.match(app, /I18n\.t\("app\.nachste\.station\.2", \{ value1: metric\.poi\.title \}\)/); assert.match(app, /I18n\.t\("app\.next-station", \{ value1: metric\.poi\.title \}\)/);
assert.match(app, /find\('small'\)\.text\(`\$\{distance\} m`\)/); assert.match(app, /find\('small'\)\.text\(`\$\{distance\} m`\)/);
assert.match(app, /find\('svg'\)\.css\('transform'/); assert.match(app, /find\('svg'\)\.css\('transform'/);
assert.match(css, /\.poi-proximity[\s\S]*grid-column:\s*2/); assert.match(css, /\.poi-proximity[\s\S]*grid-column:\s*2/);
+1 -1
View File
@@ -56,7 +56,7 @@ test('persistent route control displays the next unvisited Station and preserves
const app = await read('public/js/app.js'); const app = await read('public/js/app.js');
assert.match(app, /function nextPoiMetric\(/); assert.match(app, /function nextPoiMetric\(/);
assert.match(app, /I18n\.t\("app\.nachste\.station", \{ value1: metric\.poi\.title, value2: distance \}\)/); assert.match(app, /I18n\.t\("app\.next-station-summary", \{ value1: metric\.poi\.title, value2: distance \}\)/);
assert.match(app, /state\.triggered\.has\(poi\.id\)/); assert.match(app, /state\.triggered\.has\(poi\.id\)/);
assert.match(app, /function suspendCurrentRoute\(/); assert.match(app, /function suspendCurrentRoute\(/);
assert.match(app, /persistRouteProgress\(\)/); assert.match(app, /persistRouteProgress\(\)/);
+11
View File
@@ -47,3 +47,14 @@ test('public help and spoken labels omit implementation terms and preserve meani
assert.match(text, /Fortschritt.*gelöscht/s); assert.match(text, /Fortschritt.*gelöscht/s);
assert.match(text, /Meine Entwürfe/); assert.match(text, /Meine Entwürfe/);
}); });
test('English errors keep translated validation hints and hide technical replies', async () => {
const context = { navigator: { language: 'en-GB' } };
await loadI18n(context);
vm.runInNewContext(await fs.readFile(path.join(root, 'public/js/user-messages.js'), 'utf8'), context);
const { I18n, UserMessages } = context.Wegwichtel;
assert.equal(UserMessages.error(new Error(I18n.t('editor.error.route-name'))), 'Give your trail a name.');
assert.equal(UserMessages.error({ status: 413 }), 'This file is too large. Choose a smaller file.');
assert.match(UserMessages.error({ message: 'Die GPX-Datei ist kein gültiges XML.' }), /readable path/);
assert.equal(UserMessages.error(new Error('Failed to fetch')), 'That did not work. Please try again.');
});