// UI strings for both languages. Keys are grouped by the view they belong to; // {placeholders} are filled by t(). const STRINGS = { de: { "common.cancel": "Abbrechen", "common.save": "Speichern", "common.create": "Anlegen", "common.request": "Anfragen", "common.search": "Suchen", "common.all": "Alle", "common.none": "Keine", "common.close": "Schließen", "common.edit": "Bearbeiten", "common.delete": "Löschen", "common.link": "Verbinden", "common.createAndLink": "Anlegen & verbinden", "common.selectAll": "Alle auswählen", "common.removeSelected": "Ausgewählte entfernen", "common.removeSelectedN": "Ausgewählte entfernen ({n})", "common.library": "Library", "common.type": "Typ", "common.title": "Titel", "common.authors": "Autor(en)", "common.author": "Autor", "common.narrator": "Sprecher", "common.series": "Serie", "common.volume": "Band", "common.year": "Jahr", "common.name": "Name", "common.path": "Pfad", "common.language": "Sprache", "common.score": "Score", "common.status": "Status", "common.noLibrary": "— keine Library —", "common.nothingHere": "Nichts hier.", "common.optionalForAll": "optional, für alle", "common.optional": "optional", "common.needLibraryFirst": "Erst eine Library für diesen Typ anlegen (Tab Libraries)", "common.creating": "Lege an…", "lang.german": "Deutsch", "lang.english": "Englisch", "nav.search": "Suche", "nav.missing": "Missing", "nav.imported": "Importiert", "nav.import": "Import", "nav.settings": "Libraries", "nav.langToggle": "Switch to English", "search.languageTitle": "Sprache der Ausgabe — filtert Ebook- und Audiobook-Suche", "search.allLanguages": "Alle Sprachen", "search.placeholder": "Titel, Autor oder ISBN…", "search.manual": "Manuell anlegen", "search.running": "Suche…", "search.wholeSeries": "Ganze Serie…", "search.requested": "✓ Angefragt", "search.emptyEbook": "Keine Treffer. Tipp: ISBN ohne Bindestriche oder Titel + Autor versuchen — und ggf. „Alle Sprachen“ wählen.", "search.emptyAudiobook": "Keine Treffer. Tipp: EAN/ISBN funktioniert bei Audible nicht — nach Titel/Autor suchen.", "search.emptyComic": "Keine Treffer bei AniList (Manga). Westliche Comics per „Manuell anlegen“ erfassen.", "manual.heading": "Manuell anfragen", "manual.volumeTo": "bis Band (Bulk, optional)", "manual.bulkHint": "Legt pro Band/Folge eine Anfrage an, z.B. 1 bis 300", "manual.externalId": "ISBN/ID", "manual.noLibraryChosen": "Keine Library gewählt", "manual.needStartVolume": "Für Bulk bitte Start-Band angeben", "manual.created": "Anfrage angelegt", "manual.createdN": "{n} Anfragen angelegt", "series.fallbackTitle": "Serie", "series.from": "von Nr.", "series.to": "bis Nr.", "series.loading": "{unit} werden gesucht…", "series.unitVolumes": "Bände", "series.unitEpisodes": "Folgen", "series.found": "{n} {unit} gefunden", "series.range": " (Nr. {from}–{to})", "series.noteEbook": ". Bände ohne Ausgabe in der gewählten Sprache stehen mit ihrem Originaltitel da.", "series.noteAudiobook": ". Audible hat keine Serien-Abfrage — einzelne Folgen können fehlen.", "series.gapVolumes": "… {n} Bände nicht bei Hardcover gefunden ({from}–{to})", "series.gapVolume": "… {n} Band nicht bei Hardcover gefunden ({from}–{to})", "series.gapEpisodes": "… {n} Folgen nicht bei Audible gefunden ({from}–{to})", "series.gapEpisode": "… {n} Folge nicht bei Audible gefunden ({from}–{to})", "series.requestN": "{n} anfragen", "series.needLibrary": "Erst eine {type}-Library anlegen (Tab Libraries)", "series.createdN": "{n} Anfragen angelegt", "series.skippedN": ", {n} bereits vorhanden", "list.allTypes": "Alle Typen", "list.allLibraries": "Alle Libraries", "list.filterPlaceholder": "Filtern (Titel/Autor/Serie)…", "list.view": "Ansicht", "list.viewList": "Liste", "list.viewSmall": "Kleine Symbole", "list.viewLarge": "Große Symbole", "list.confirmDeleteRequests": "{n} Anfrage(n) entfernen?", "list.confirmDeleteImported": "{n} Eintrag/Einträge entfernen? Die importierten Dateien in der Library bleiben erhalten.", "list.deletedN": "{n} entfernt", "detail.statusMissing": "fehlt noch", "detail.statusImported": "importiert", "detail.externalId": "ISBN/ASIN", "detail.retag": "Speichern & neu taggen", "detail.relocate": "Speichern & neu ablegen", "detail.relocateTitle": "Ordner und Dateinamen nach dem Schema der Library neu anlegen", "detail.relocating": "Lege ab…", "detail.relocated": "Neu abgelegt: {dest}", "detail.alreadyPlaced": "Liegt bereits richtig", "detail.retagging": "Tagge…", "detail.retagged": "{n} Datei(en) neu getaggt", "detail.saved": "Anfrage gespeichert", "detail.duplicateConfirm": "Trotzdem anlegen?", "import.scan": "Download-Ordner scannen", "import.splitDirs": "Ordner als Einzeldateien behandeln (z.B. Folgen-Sammlungen)", "import.scanning": "Scanne…", "import.candidates": "{n} Kandidat(en) in {dir}", "import.remaining": "{n} Kandidat(en) übrig — für den aktuellen Stand erneut scannen", "import.filterPlaceholder": "Filtern (Name/Pfad)…", "import.modeSuggested": "Nur mit Vorschlag", "import.modeUnassigned": "Nur ohne Zuordnung", "import.modeConflict": "Nur Konflikte", "import.formatTitle": "Nur Einträge mit diesem Dateiformat zeigen", "import.allFormats": "Alle Formate", "import.sortScan": "Reihenfolge: Scan", "import.sortScore": "Score absteigend", "import.sortName": "Name A–Z", "import.selectSuggested": "Alle mit Vorschlag auswählen", "import.selectAllTitle": "Alle Einträge der aktuellen Ansicht auswählen, auch ohne Zuordnung", "import.selectPage": "Diese Seite auswählen", "import.selectPageTitle": "Nur die Einträge dieser Seite auswählen, alle anderen abwählen", "import.deselectAll": "Alle abwählen", "import.merge": "Ausgewählte zusammenfassen", "import.mergeTitle": "Mehrteiler: ausgewählte Einträge als ein Hörbuch importieren", "import.fromNames": "Anfragen aus Ordnernamen", "import.fromNamesTitle": "Für ausgewählte Einträge ohne Zuordnung Anfragen aus den Ordnernamen anlegen", "import.colFile": "Datei/Ordner", "import.colAssignment": "Zuordnung", "import.prev": "← Zurück", "import.next": "Weiter →", "import.run": "Ausgewählte importieren", "import.skip": "— überspringen —", "import.appendGroup": "Bereits importiert — Teile anhängen", "import.quickTitle": "Metadaten suchen und Anfrage direkt verbinden", "import.unmerge": "Zusammenfassung wieder auflösen", "import.splitDir": "Ordner in einzelne Titel aufteilen", "import.parts": "{parts} Teile · {files} Dateien", "import.maybeSeparate": "{n} Titel?", "import.maybeSeparateTitle": "Die {n} Dateien tragen verschiedene Titel und sind je groß genug für ein ganzes Buch — mit ✂️ aufteilen", "import.libraryCellTitle": "Ziel-Library dieser Anfrage ändern", "import.conflict": "⚠ Konflikt", "import.conflictTitle": "Mehrere Einträge zeigen auf dieselbe Anfrage", "import.running": "läuft…", "import.done": "✓ importiert", "import.failed": "✗ Fehler", "import.pageInfo": "Seite {page}/{pages} · {total} Einträge{filtered} · {selected} ausgewählt", "import.filtered": " (gefiltert: {n})", "import.movedToLibrary": "„{title}\" → {library}", "import.selectedN": "{n} Einträge {what} ausgewählt", "import.selectedWithout": ", davon {n} ohne Zuordnung", "import.thisPage": "dieser Seite", "import.inTotal": "insgesamt", "import.nothingSelected": "Nichts ausgewählt", "import.needTwo": "Mindestens zwei Einträge auswählen", "import.sameTypeOnly": "Nur Einträge desselben Typs lassen sich zusammenfassen", "import.differentFolders": "Die Einträge liegen in verschiedenen Ordnern", "import.mergedN": "{n} Einträge als „{name}\" zusammengefasst ({files} Dateien)", "import.splitN": "„{name}\" in {n} Einzeltitel aufgeteilt", "import.nothingImported": "Nichts importiert: alle ausgewählten Einträge waren mehreren Anfragen zugeordnet — Filter „Nur Konflikte\" zeigt sie.", "import.summary": "{n} importiert", "import.summaryFailed": ", {n} Fehler", "import.summarySkipped": "{n} Anfrage(n) waren mehreren Einträgen zugeordnet und wurden übersprungen — Filter „Nur Konflikte\" zeigt sie.", "import.finished": "Import fertig", "import.finishedFailed": "Import fertig, {n} Fehler", "import.progressDone": "{total}/{total} · fertig", "names.heading": "Anfragen aus Ordnernamen", "names.intro": "{n} Einträge ohne Zuordnung ({type}). Titel und Folgennummer kommen aus dem Ordnernamen; Serie und Autor gelten für alle.", "names.more": "… und {n} weitere", "names.noneWithout": "Keine ausgewählten Einträge ohne Zuordnung", "names.result": "{created} Anfragen angelegt{skipped}, {linked} Einträge verbunden", "names.existed": ", {n} gab es schon", "conflict.heading": "Mehrfach zugeordnete Anfragen", "conflict.intro": "{n} Anfrage(n) sind mehreren Einträgen zugeordnet. Gehören die Einträge zusammen (Mehrteiler), lassen sie sich als ein Hörbuch importieren — sonst überspringst du sie, korrigierst die Zuordnung und importierst sie später.", "conflict.merge": "Als je ein Hörbuch zusammenfassen", "conflict.skip": "Konflikte überspringen, Rest importieren", "quick.heading": "Suchen & Anfragen", "quick.placeholder": "Titel/Serie suchen…", "quick.targetLibrary": "Ziel-Library", "quick.openRequests": "Offene Anfragen", "quick.reqFilter": "nach Titel, Autor, Serie filtern…", "quick.manualSummary": "Nicht bei Audible? Manuell anlegen", "quick.editSummary": "Felder anpassen", "quick.episodeVolume": "Folge/Band", "quick.searching": "Suche läuft…", "quick.episodeNote": "Folge {n} — nicht mitgesucht", "quick.matchingOpen": "Passende offene Requests:", "quick.hits": "{shown} von {total} Treffern ({open} offen)", "quick.noHits": "Kein Treffer unter {open} offenen Anfragen", "quick.linked": "Mit bestehendem Request verbunden", "quick.editTitle": "Felder vor dem Anlegen anpassen (z.B. Serie)", "quick.pickTitle": "Anfrage anlegen und mit diesem Eintrag verbinden", "quick.basedOn": "Übernommen von Audible: „{title}\" — Sprecher, Cover, Jahr und ASIN bleiben erhalten.", "quick.requestedAndLinked": "„{title}\" angefragt und verbunden", "quick.needTitle": "Titel angeben", "lib.heading": "Libraries", "lib.folderTemplate": "Ordner-Schema", "lib.fileTemplate": "Datei-Schema", "lib.new": "Neue Library", "lib.edit": "Library „{name}“ bearbeiten", "lib.namePlaceholder": "Name (z.B. Kids)", "lib.folderPlaceholder": "Ordner-Schema (optional)", "lib.filePlaceholder": "Datei-Schema (optional)", "lib.languageTitle": "Sprache der Ausgaben in dieser Library — filtert die Metadaten-Suche", "lib.anyLanguage": "Sprache: egal", "lib.placeholders": "Platzhalter: {Author} {Authors} {Narrator} {Narrators} {Title} {Year} {Series} {Volume} — {Author}/{Narrator} nennen den ersten Namen, {Authors}/{Narrators} alle. Leere Werte fallen samt Trenner weg.", "lib.confirmDelete": "Library löschen?", "lib.saved": "Library gespeichert — gilt für künftige Importe", "lib.created": "Library angelegt", }, en: { "common.cancel": "Cancel", "common.save": "Save", "common.create": "Create", "common.request": "Request", "common.search": "Search", "common.all": "All", "common.none": "None", "common.close": "Close", "common.edit": "Edit", "common.delete": "Delete", "common.link": "Link", "common.createAndLink": "Create & link", "common.selectAll": "Select all", "common.removeSelected": "Remove selected", "common.removeSelectedN": "Remove selected ({n})", "common.library": "Library", "common.type": "Type", "common.title": "Title", "common.authors": "Author(s)", "common.author": "Author", "common.narrator": "Narrator", "common.series": "Series", "common.volume": "Volume", "common.year": "Year", "common.name": "Name", "common.path": "Path", "common.language": "Language", "common.score": "Score", "common.status": "Status", "common.noLibrary": "— no library —", "common.nothingHere": "Nothing here.", "common.optionalForAll": "optional, for all", "common.optional": "optional", "common.needLibraryFirst": "Create a library for this type first (Libraries tab)", "common.creating": "Creating…", "lang.german": "German", "lang.english": "English", "nav.search": "Search", "nav.missing": "Missing", "nav.imported": "Imported", "nav.import": "Import", "nav.settings": "Libraries", "nav.langToggle": "Auf Deutsch umschalten", "search.languageTitle": "Edition language — filters the ebook and audiobook search", "search.allLanguages": "All languages", "search.placeholder": "Title, author or ISBN…", "search.manual": "Add manually", "search.running": "Searching…", "search.wholeSeries": "Whole series…", "search.requested": "✓ Requested", "search.emptyEbook": "No hits. Try the ISBN without dashes, or title + author — and maybe pick “All languages”.", "search.emptyAudiobook": "No hits. EAN/ISBN does not work on Audible — search by title/author.", "search.emptyComic": "No hits on AniList (manga). Add western comics via “Add manually”.", "manual.heading": "Request manually", "manual.volumeTo": "to volume (bulk, optional)", "manual.bulkHint": "Creates one request per volume/episode, e.g. 1 to 300", "manual.externalId": "ISBN/ID", "manual.noLibraryChosen": "No library chosen", "manual.needStartVolume": "Bulk needs a starting volume", "manual.created": "Request created", "manual.createdN": "{n} requests created", "series.fallbackTitle": "Series", "series.from": "from no.", "series.to": "to no.", "series.loading": "Looking for {unit}…", "series.unitVolumes": "volumes", "series.unitEpisodes": "episodes", "series.found": "{n} {unit} found", "series.range": " (no. {from}–{to})", "series.noteEbook": ". Volumes without an edition in the chosen language keep their original title.", "series.noteAudiobook": ". Audible has no series query — single episodes can be missing.", "series.gapVolumes": "… {n} volumes not found on Hardcover ({from}–{to})", "series.gapVolume": "… {n} volume not found on Hardcover ({from}–{to})", "series.gapEpisodes": "… {n} episodes not found on Audible ({from}–{to})", "series.gapEpisode": "… {n} episode not found on Audible ({from}–{to})", "series.requestN": "Request {n}", "series.needLibrary": "Create an {type} library first (Libraries tab)", "series.createdN": "{n} requests created", "series.skippedN": ", {n} already there", "list.allTypes": "All types", "list.allLibraries": "All libraries", "list.filterPlaceholder": "Filter (title/author/series)…", "list.view": "View", "list.viewList": "List", "list.viewSmall": "Small icons", "list.viewLarge": "Large icons", "list.confirmDeleteRequests": "Remove {n} request(s)?", "list.confirmDeleteImported": "Remove {n} entr(y/ies)? The imported files stay in the library.", "list.deletedN": "{n} removed", "detail.statusMissing": "still missing", "detail.statusImported": "imported", "detail.externalId": "ISBN/ASIN", "detail.retag": "Save & re-tag", "detail.relocate": "Save & re-file", "detail.relocateTitle": "Rebuild folder and file names from the library's scheme", "detail.relocating": "Filing…", "detail.relocated": "Re-filed: {dest}", "detail.alreadyPlaced": "Already in the right place", "detail.retagging": "Tagging…", "detail.retagged": "{n} file(s) re-tagged", "detail.saved": "Request saved", "detail.duplicateConfirm": "Create anyway?", "import.scan": "Scan download folder", "import.splitDirs": "Treat folders as single files (e.g. episode collections)", "import.scanning": "Scanning…", "import.candidates": "{n} candidate(s) in {dir}", "import.remaining": "{n} candidate(s) left — scan again for the current state", "import.filterPlaceholder": "Filter (name/path)…", "import.modeSuggested": "With suggestion only", "import.modeUnassigned": "Unassigned only", "import.modeConflict": "Conflicts only", "import.formatTitle": "Show only entries with this file format", "import.allFormats": "All formats", "import.sortScan": "Order: scan", "import.sortScore": "Score descending", "import.sortName": "Name A–Z", "import.selectSuggested": "Select all with a suggestion", "import.selectAllTitle": "Select every entry of the current view, unassigned ones too", "import.selectPage": "Select this page", "import.selectPageTitle": "Select only this page's entries, deselect all others", "import.deselectAll": "Deselect all", "import.merge": "Merge selected", "import.mergeTitle": "Multi-part title: import the selected entries as one audiobook", "import.fromNames": "Requests from folder names", "import.fromNamesTitle": "Create requests from the folder names of the selected unassigned entries", "import.colFile": "File/folder", "import.colAssignment": "Assignment", "import.prev": "← Back", "import.next": "Next →", "import.run": "Import selected", "import.skip": "— skip —", "import.appendGroup": "Already imported — append parts", "import.quickTitle": "Search metadata and link the request right away", "import.unmerge": "Undo the merge", "import.splitDir": "Split the folder into single titles", "import.parts": "{parts} parts · {files} files", "import.maybeSeparate": "{n} titles?", "import.maybeSeparateTitle": "The {n} files carry different titles and are each big enough for a whole book — split with ✂️", "import.libraryCellTitle": "Change this request's target library", "import.conflict": "⚠ Conflict", "import.conflictTitle": "Several entries point at the same request", "import.running": "running…", "import.done": "✓ imported", "import.failed": "✗ Error", "import.pageInfo": "Page {page}/{pages} · {total} entries{filtered} · {selected} selected", "import.filtered": " (filtered: {n})", "import.movedToLibrary": "“{title}” → {library}", "import.selectedN": "{n} entries {what} selected", "import.selectedWithout": ", {n} of them unassigned", "import.thisPage": "on this page", "import.inTotal": "in total", "import.nothingSelected": "Nothing selected", "import.needTwo": "Select at least two entries", "import.sameTypeOnly": "Only entries of the same type can be merged", "import.differentFolders": "The entries live in different folders", "import.mergedN": "{n} entries merged as “{name}” ({files} files)", "import.splitN": "“{name}” split into {n} single titles", "import.nothingImported": "Nothing imported: every selected entry was assigned to several requests — the “Conflicts only” filter shows them.", "import.summary": "{n} imported", "import.summaryFailed": ", {n} failed", "import.summarySkipped": "{n} request(s) were assigned to several entries and were skipped — the “Conflicts only” filter shows them.", "import.finished": "Import finished", "import.finishedFailed": "Import finished, {n} failed", "import.progressDone": "{total}/{total} · done", "names.heading": "Requests from folder names", "names.intro": "{n} unassigned entries ({type}). Title and episode number come from the folder name; series and author apply to all of them.", "names.more": "… and {n} more", "names.noneWithout": "No selected entries without an assignment", "names.result": "{created} requests created{skipped}, {linked} entries linked", "names.existed": ", {n} already existed", "conflict.heading": "Requests assigned more than once", "conflict.intro": "{n} request(s) are assigned to several entries. If those entries belong together (a multi-part title), they can be imported as one audiobook — otherwise skip them, fix the assignment and import them later.", "conflict.merge": "Merge each into one audiobook", "conflict.skip": "Skip conflicts, import the rest", "quick.heading": "Search & request", "quick.placeholder": "Search title/series…", "quick.targetLibrary": "Target library", "quick.openRequests": "Open requests", "quick.reqFilter": "filter by title, author, series…", "quick.manualSummary": "Not on Audible? Add manually", "quick.editSummary": "Adjust the fields", "quick.episodeVolume": "Episode/volume", "quick.searching": "Searching…", "quick.episodeNote": "Episode {n} — not part of the search", "quick.matchingOpen": "Matching open requests:", "quick.hits": "{shown} of {total} hits ({open} open)", "quick.noHits": "No hit among {open} open requests", "quick.linked": "Linked to the existing request", "quick.editTitle": "Adjust the fields before creating (e.g. the series)", "quick.pickTitle": "Create the request and link it to this entry", "quick.basedOn": "Taken from Audible: “{title}” — narrator, cover, year and ASIN are kept.", "quick.requestedAndLinked": "“{title}” requested and linked", "quick.needTitle": "Enter a title", "lib.heading": "Libraries", "lib.folderTemplate": "Folder scheme", "lib.fileTemplate": "File scheme", "lib.new": "New library", "lib.edit": "Edit library “{name}”", "lib.namePlaceholder": "Name (e.g. Kids)", "lib.folderPlaceholder": "Folder scheme (optional)", "lib.filePlaceholder": "File scheme (optional)", "lib.languageTitle": "Language of this library's editions — filters the metadata search", "lib.anyLanguage": "Language: any", "lib.placeholders": "Placeholders: {Author} {Authors} {Narrator} {Narrators} {Title} {Year} {Series} {Volume} — {Author}/{Narrator} name the first one, {Authors}/{Narrators} all of them. Empty values drop out with their separator.", "lib.confirmDelete": "Delete library?", "lib.saved": "Library saved — applies to future imports", "lib.created": "Library created", }, }; const LANG_KEY = "wordarr.lang"; function initialLang() { try { const saved = localStorage.getItem(LANG_KEY); if (saved && STRINGS[saved]) return saved; } catch {} return (navigator.language || "en").startsWith("de") ? "de" : "en"; } let lang = initialLang(); // {Placeholders} of the naming scheme must survive, so only known vars are replaced function t(key, vars) { let s = STRINGS[lang][key] ?? STRINGS.en[key] ?? key; if (vars) { for (const [name, value] of Object.entries(vars)) { s = s.split("{" + name + "}").join(String(value)); } } return s; } const I18N_ATTRS = { "i18nPlaceholder": "placeholder", "i18nTitle": "title", "i18nAria": "aria-label" }; function applyI18n(root = document) { root.querySelectorAll("[data-i18n]").forEach((el) => (el.textContent = t(el.dataset.i18n))); for (const [data, attr] of Object.entries(I18N_ATTRS)) { root.querySelectorAll(`[data-${data.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase())}]`) .forEach((el) => el.setAttribute(attr, t(el.dataset[data]))); } document.documentElement.lang = lang; } function setLang(next) { lang = next; try { localStorage.setItem(LANG_KEY, next); } catch {} applyI18n(); if (typeof onLangChange === "function") onLangChange(); }