diff --git a/frontend/src/lbPhoneMigration.contract.test.ts b/frontend/src/lbPhoneMigration.contract.test.ts new file mode 100644 index 0000000..4e03602 --- /dev/null +++ b/frontend/src/lbPhoneMigration.contract.test.ts @@ -0,0 +1,53 @@ +import { readFileSync } from 'node:fs' + +import { describe, expect, it } from 'vitest' + +const migration = readFileSync( + new URL( + '../../sky_phone/source/server/lb_phone_migration.lua', + import.meta.url, + ), + 'utf8', +) +const config = readFileSync( + new URL('../../sky_phone/config/config.lua', import.meta.url), + 'utf8', +) + +describe('LB Phone social migration contract', () => { + it('reruns the repaired social domains and maps active LB profiles to Sky accounts', () => { + expect(migration).toMatch( + /local domain_versions = \{[\s\S]*?picstagram = 2,[\s\S]*?flipTok = 2,[\s\S]*?feather = 2,/, + ) + expect(migration).toContain( + 'local function prepare_social_profile_accounts', + ) + expect(migration).toContain('preferred_entry_by_owner') + expect(migration).toContain('owner.`account_id`') + expect(migration).toContain( + 'move_profile(existing, preferred_entry, owner_account_id)', + ) + }) + + it('refreshes device sessions when the active LB account changed', () => { + expect(migration).toMatch( + /INSERT INTO `sky_phone_picstagram_sessions`[\s\S]*?ON DUPLICATE KEY UPDATE `profile_id` = VALUES\(`profile_id`\)/, + ) + expect(migration).toMatch( + /INSERT INTO `sky_phone_fliptok_sessions`[\s\S]*?ON DUPLICATE KEY UPDATE `profile_id` = VALUES\(`profile_id`\)/, + ) + }) + + it('imports LB Tinder data into the Flare domain', () => { + expect(config).toMatch(/Domains = \{[\s\S]*?flare = true,/) + expect(migration).toContain('local function run_flare(dry_run)') + expect(migration).toContain('source_table("tinder_accounts")') + expect(migration).toContain('source_table("tinder_swipes")') + expect(migration).toContain('INSERT IGNORE INTO `sky_phone_flare_profiles`') + expect(migration).toContain( + 'INSERT IGNORE INTO `sky_phone_flare_profile_photos`', + ) + expect(migration).toContain('INSERT IGNORE INTO `sky_phone_flare_swipes`') + expect(migration).toContain('INSERT IGNORE INTO `sky_phone_flare_matches`') + }) +}) diff --git a/frontend/src/stores/phone-locale-contract.test.ts b/frontend/src/stores/phone-locale-contract.test.ts index e15f3b9..e42c6cb 100644 --- a/frontend/src/stores/phone-locale-contract.test.ts +++ b/frontend/src/stores/phone-locale-contract.test.ts @@ -73,20 +73,20 @@ function tokenizeLua(source: string): LuaToken[] { return tokens } -function collectLuaLocalePaths(source: string): Set { +function collectLuaLocaleValues(source: string): Map { const tokens = tokenizeLua(source) let position = tokens.findIndex( (token, index) => token.kind === '=' && tokens[index + 1]?.kind === '{', ) + 1 - const paths = new Set() + const values = new Map() function parseValue(path: string[]): void { if (tokens[position]?.kind === '{') { parseTable(path) return } - if (path.length) paths.add(path.join('.')) + if (path.length) values.set(path.join('.'), tokens[position]?.value ?? '') position += 1 } @@ -118,7 +118,11 @@ function collectLuaLocalePaths(source: string): Set { } parseTable([]) - return paths + return values +} + +function collectPlaceholders(value: string): string[] { + return [...new Set(value.match(/\{[A-Za-z0-9_]+\}/g) ?? [])].sort() } function collectDefaultLocalePaths(source: string): Set { @@ -196,7 +200,9 @@ function collectFrontendFiles(directory: string): string[] { } describe('phone locale contract', () => { - const englishPaths = collectLuaLocalePaths(englishLocaleSource) + const englishValues = collectLuaLocaleValues(englishLocaleSource) + const germanValues = collectLuaLocaleValues(germanLocaleSource) + const englishPaths = new Set(englishValues.keys()) it('keeps every bundled frontend fallback in en.lua', () => { const missing = [...collectDefaultLocalePaths(phoneStoreSource)].filter( @@ -243,8 +249,78 @@ describe('phone locale contract', () => { }) it('keeps German structurally aligned with English', () => { - const germanPaths = collectLuaLocalePaths(germanLocaleSource) + const germanPaths = new Set(germanValues.keys()) expect([...germanPaths].sort()).toEqual([...englishPaths].sort()) }) + + it('keeps German interpolation placeholders aligned with English', () => { + const mismatches = [...englishValues].flatMap(([path, englishValue]) => { + const germanValue = germanValues.get(path) + return germanValue !== undefined && + JSON.stringify(collectPlaceholders(germanValue)) !== + JSON.stringify(collectPlaceholders(englishValue)) + ? [ + `${path}: ${collectPlaceholders(englishValue).join(', ')} != ${collectPlaceholders(germanValue).join(', ')}`, + ] + : [] + }) + + expect(mismatches).toEqual([]) + }) + + it('keeps standard app names German and custom game names unchanged', () => { + const standardAppNames = { + health: 'Gesundheit', + messages: 'Nachrichten', + companies: 'Unternehmen', + phone: 'Telefon', + radio: 'Funk', + billing: 'Rechnungen', + house: 'Haus', + calculator: 'Rechner', + camera: 'Kamera', + calendar: 'Kalender', + clock: 'Uhr', + localPages: 'Lokale Seiten', + map: 'Karte', + weather: 'Wetter', + music: 'Musik', + notes: 'Notizen', + photos: 'Fotos', + settings: 'Einstellungen', + } + + for (const [app, name] of Object.entries(standardAppNames)) { + expect(germanValues.get(`Nui.Apps.${app}.name`), app).toBe(name) + } + + for (const app of [ + 'snake', + 'memory', + 'numberMerge', + 'minesweeper', + 'towerStack', + 'skyFlappy', + 'neonDrop', + ]) { + const path = `Nui.Apps.${app}.name` + expect(germanValues.get(path), app).toBe(englishValues.get(path)) + } + }) + + it('keeps common German UI terms semantically correct', () => { + expect(germanValues.get('Nui.Apps.phone.cancelled')).toBe('Abgebrochen') + expect(germanValues.get('Nui.Apps.flare.interests')).toBe('Interessen') + expect(germanValues.get('Nui.Apps.picstagram.emptyFeed')).toBe( + 'Dein Feed ist ruhig', + ) + expect(germanValues.get('Nui.Apps.clock.lap')).toBe('Runde') + expect(germanValues.get('Nui.Apps.easyShare.incoming')).toBe( + 'Eingehende Freigabe', + ) + expect(germanValues.get('Nui.Apps.settings.phoneScale')).toBe( + 'Telefongröße', + ) + }) }) diff --git a/frontend/src/views/apps/FeatherApp.contract.test.ts b/frontend/src/views/apps/FeatherApp.contract.test.ts index a607df7..22aec0e 100644 --- a/frontend/src/views/apps/FeatherApp.contract.test.ts +++ b/frontend/src/views/apps/FeatherApp.contract.test.ts @@ -142,6 +142,19 @@ describe('FeatherApp Sky UI contract', () => { expect(source).not.toContain('feather-feed-add') }) + it('keeps profile suggestion content styles off the follow button', () => { + expect(source).toContain('class="feather-profile-suggestion__profile"') + expect(source).toMatch( + /\.feather-app\.feather-app--active \.feather-profile-suggestion__profile\s*\{[^}]*width:\s*100%/s, + ) + expect(source).not.toContain( + '.feather-app.feather-app--active .feather-profile-suggestion > button', + ) + expect(source).not.toContain( + '.dark.feather-app .feather-profile-suggestion > button', + ) + }) + it('keeps owner actions in the navbar and removes the profile media tab', () => { const profileStart = source.indexOf('class="feather-profile"') const tabsStart = source.indexOf( diff --git a/sky_phone/README.md b/sky_phone/README.md index ae2df30..e7a3932 100644 --- a/sky_phone/README.md +++ b/sky_phone/README.md @@ -414,7 +414,9 @@ The importer: - Can remove migration-created Sky Phone data with `remove` - Reports unsupported source records instead of forcing them into incompatible Sky Phone structures -Supported domains include devices, settings, alarms, contacts, blocked numbers, calls, messages, photos, notes, wallet, voice memos, Picstagram, Mail, map markers, compatible DarkChat data, FlipTok, and Feather. +For Picstagram, FlipTok, and Feather, the active LB Phone login is attached to the migrated player's Sky Cloud account. If LB has no active-login row, the oldest mapped profile is used. Additional social profiles remain separate, and a normal import automatically runs versioned ownership repairs for older Sky Phone migrations. + +Supported domains include devices, settings, alarms, contacts, blocked numbers, calls, messages, photos, notes, wallet, voice memos, Picstagram, Mail, map markers, compatible DarkChat data, FlipTok, Feather, and Flare (LB Tinder profiles, photos, swipes, and mutual matches). The migration command is server-console only. diff --git a/sky_phone/config/config.lua b/sky_phone/config/config.lua index a6e1175..96a0dcc 100644 --- a/sky_phone/config/config.lua +++ b/sky_phone/config/config.lua @@ -1439,6 +1439,7 @@ if IsDuplicityVersion() then darkChat = true, flipTok = true, feather = true, + flare = true, }, } end diff --git a/sky_phone/config/locales/de.lua b/sky_phone/config/locales/de.lua index 8fca349..5e12560 100644 --- a/sky_phone/config/locales/de.lua +++ b/sky_phone/config/locales/de.lua @@ -176,26 +176,26 @@ Locales["de"] = { weather = { city = "Los Santos", condition = "Teilweise bewölkt" }, calendar = { event = "Keine Veranstaltungen mehr heute" }, battery = { label = "Telefon" }, - media = { title = "Nachtantrieb", artist = "Funkgerät für den Himmel", play = "Wiedergabe", pause = "Pause" }, + media = { title = "Night Drive", artist = "Sky Radio", play = "Wiedergabe", pause = "Pause" }, }, widgetSystem = { galleryTitle = "Widgets", search = "Widgets suchen", size = "Größe", add = "Widget hinzufügen", addWidget = "Widget hinzufügen", editWidget = "Bearbeiten", removeWidget = "Widget entfernen", remove = "Widget entfernen", configure = "Widget einrichten", noResults = "Keine Widgets gefunden", sizes = { small = "Klein", medium = "Mittel", large = "Groß" }, - categories = { essentials = "Wesentliche Aspekte", information = "Unterrichtung", media = "Medien", finance = "Finanzen", people = "Menschen" }, + categories = { essentials = "Grundlagen", information = "Informationen", media = "Medien", finance = "Finanzen", people = "Menschen" }, clock = { name = "Uhr", description = "Die aktuelle Zeit, mit einem optionalen Datum.", showDate = "Datum anzeigen" }, - date = { name = "Zeitpunkt", description = "Wochentag, Monat und Datum auf einen Blick." }, - weather = { name = "Wetter", description = "Aktuelle Bedingungen, Lage und hoch und niedrig.", range = "H: {high}° L: {low}°" }, - music = { name = "Jetzt spielen", description = "Musiksteuerung und die aktuelle Spur.", empty = "Keine vor kurzem gespielte Musik" }, - wallet = { name = "Brieftasche", description = "Deine aktuelle Bank oder dein Guthaben.", balance = "Ausgeblendete Balance", bank = "Bank", cash = "Bargeld" }, + date = { name = "Datum", description = "Wochentag, Monat und Datum auf einen Blick." }, + weather = { name = "Wetter", description = "Aktuelles Wetter, Standort sowie Höchst- und Tiefstwerte.", range = "H: {high}° T: {low}°" }, + music = { name = "Jetzt läuft", description = "Musiksteuerung und der aktuelle Titel.", empty = "Keine kürzlich gespielte Musik" }, + wallet = { name = "Wallet", description = "Dein aktuelles Bank- und Bargeldguthaben.", balance = "Verfügbares Guthaben", bank = "Bank", cash = "Bargeld" }, transactions = { name = "Transaktionen", description = "Deine letzten eingehenden und ausgehenden Zahlungen." }, - contacts = { name = "Favoriten", description = "Ruf deine Lieblingskontakte an oder sag sie.", choose = "Lieblingskontakte" }, + contacts = { name = "Favoriten", description = "Rufe deine Lieblingskontakte an oder schreibe ihnen.", choose = "Lieblingskontakte" }, }, }, Apps = { health = { name = "Gesundheit", - navigation = "Gesundheitsschutz", + navigation = "Gesundheitsnavigation", tabs = { today = "Heute", trends = "Trends", medicalId = "Notfallpass" }, loading = "Gesundheitsdaten werden geladen...", errorTitle = "Gesundheit ist nicht verfügbar", @@ -220,15 +220,15 @@ Locales["de"] = { medicalId = { title = "Notfallpass", edit = "Bearbeiten", done = "Fertig", resident = "Los Santos Einwohner", emergencyInformation = "Notfallinformationen", bloodType = "Bluttyp", - allergies = "Allergien", conditions = "Bedingungen", medication = "Arzneimittel", - noneRecorded = "Keine erfasst", emergencyContact = "Kontakt mit Notfällen", - contactName = "Name des Ansprechpartners", relation = "Verhältnis", phoneNumber = "Telefonnummer", - emergencyCall = "Notruf", callContact = "Notruf", - saveFailed = "Der medizinische Ausweis konnte nicht gespeichert werden.", + allergies = "Allergien", conditions = "Erkrankungen", medication = "Medikamente", + noneRecorded = "Keine erfasst", emergencyContact = "Notfallkontakt", + contactName = "Kontaktname", relation = "Beziehung", phoneNumber = "Telefonnummer", + emergencyCall = "Notruf", callContact = "Kontakt anrufen", + saveFailed = "Der Notfallpass konnte nicht gespeichert werden.", }, errors = { invalid_phone_number = "Gib eine gültige Nummer ein.", - invalid_request = "Prüf die Krankenausweise.", + invalid_request = "Prüfe die Angaben im Notfallpass.", rate_limited = "Bitte warte, bevor du es erneut versuchst.", request_failed = "Gesundheit konnte die Anfrage nicht erfüllen.", }, @@ -253,75 +253,75 @@ Locales["de"] = { retry = "Erneut versuchen", loadMore = "Mehr laden", home = { - eyebrow = "Los Santos, lebend", + eyebrow = "Los Santos, live", latest = "Neueste Nachrichten", }, categories = { title = "Kategorien", - subtitle = "Such jede Geschichte vom Weazel News Schreibtisch.", + subtitle = "Durchsuche alle Geschichten der Weazel-News-Redaktion.", all = "Alle Geschichten", official = "Offizielle Bekanntmachungen", events = "Veranstaltungen & Freizeit", - jobs = "Arbeitsplätze", - news = "Nachrichten & Veranstaltungen", + jobs = "Jobs", + news = "Nachrichten & Ereignisse", business = "Unternehmen", }, search = { placeholder = "Artikel durchsuchen...", title = "Suche nach Weazel News", emptyTitle = "Keine Artikel gefunden", - emptyBody = "Versuch's noch mal mit Schlagzeilen, Themen oder Kategorien.", + emptyBody = "Versuche es mit anderen Schlagzeilen, Themen oder Kategorien.", }, states = { loading = "Weazel News wird geladen...", emptyTitle = "Noch keine Nachrichten", - emptyBody = "Der Schreibtisch Weazel News hat hier noch keine Artikel veröffentlicht.", + emptyBody = "Die Weazel-News-Redaktion hat hier noch keine Artikel veröffentlicht.", errorTitle = "Weazel News ist nicht verfügbar", readOnlyTitle = "Nur-Lesezugriff", readOnlyBody = "Dein aktueller Job kann Weazel News lesen, aber Artikel nicht verwalten.", noManagedTitle = "Keine redaktionellen Artikel", - noManagedBody = "Erstell einen Artikel oder ändere den aktuellen Editorial-Filter.", + noManagedBody = "Erstelle einen Artikel oder ändere den aktuellen Redaktionsfilter.", }, article = { readMore = "Artikel lesen", byline = "Von {author}", - updated = "Aktualisierte {date}", + updated = "Aktualisiert {date}", published = "Veröffentlicht {date}", coverAlt = "Cover-Bild für {title}", }, editorial = { title = "Redaktion", - subtitle = "Erstellen, veröffentlichen und pflegen Weazel News Artikel.", + subtitle = "Weazel-News-Artikel erstellen, veröffentlichen und verwalten.", newArticle = "Neuer Artikel", published = "Veröffentlicht", drafts = "Entwürfe", all = "Alle", - jobAccess = "Unterzeichnet als {job}", + jobAccess = "Angemeldet als {job}", edit = "Artikel bearbeiten", - delete = "Artikel streichen", + delete = "Artikel löschen", }, composer = { newTitle = "Neuer Artikel", editTitle = "Artikel bearbeiten", - title = "Kopfzeile", + title = "Überschrift", titlePlaceholder = "Schreibe eine klare Überschrift", body = "Artikel", bodyPlaceholder = "Schreib die ganze Geschichte...", category = "Kategorie", status = "Veröffentlichungsstatus", - cover = "Bild abdecken", - photos = "Artikel Fotos", + cover = "Titelbild", + photos = "Artikelfotos", photoCount = "{count} von {maximum} Fotos", addPhotos = "Fotos hinzufügen", choosePhotos = "Wähle aus Fotos", takePhoto = "Foto machen", photoSourceHint = "Wähle mehrere Fotos aus deiner Bibliothek oder nimm ein neues.", - primaryPhoto = "Abdeckung", - makePrimary = "Als Abdeckung verwenden", + primaryPhoto = "Titelbild", + makePrimary = "Als Titelbild verwenden", chooseCover = "Wähle aus Fotos", - changeCover = "Abdeckung ändern", - removeCover = "Cover entfernen", - coverAlt = "Ausgewählte Artikel-Cover", + changeCover = "Titelbild ändern", + removeCover = "Titelbild entfernen", + coverAlt = "Ausgewähltes Artikel-Titelbild", publish = "Artikel veröffentlichen", saveDraft = "Entwurf speichern", saveChanges = "Änderungen speichern", @@ -329,7 +329,7 @@ Locales["de"] = { statusDraft = "Entwurf", }, delete = { - title = "Artikel streichen?", + title = "Artikel löschen?", body = "Dieser Artikel wird aus Weazel News entfernt.", cancel = "Abbrechen", confirm = "Löschen", @@ -337,7 +337,7 @@ Locales["de"] = { feedback = { created = "Artikel erstellt.", updated = "Artikel aktualisiert.", - deleted = "Artikel gestrichen.", + deleted = "Artikel gelöscht.", }, accessibility = { articleList = "Weazel News Artikel", @@ -353,11 +353,11 @@ Locales["de"] = { clearSearch = "Artikelsuche löschen", articleImages = "Artikelfotos", previousPhoto = "Zeige das vorherige Artikelfoto", - nextPhoto = "Den nächsten Artikel anzeigen Foto", + nextPhoto = "Nächstes Artikelfoto anzeigen", showPhoto = "Artikelfoto anzeigen {current} von {total}", coverPreview = "Vorschau auf Artikel-Cover", - removeCover = "Entferne das ausgewählte Coverbild", - removePhoto = "Diesen Artikel entfernen Foto", + removeCover = "Ausgewähltes Titelbild entfernen", + removePhoto = "Dieses Artikelfoto entfernen", status = "Artikelstatus: {status}", }, errors = { @@ -378,33 +378,33 @@ Locales["de"] = { }, }, crewlink = { - name = "CrewLink", connecting = "Crew wird verbunden …", privateNetwork = "Privates Standortnetz", - signInTitle = "Schließ dein Sky Cloud-Konto an", signInBody = "CrewLink verwendet deine private Sky-Cloud-Identität, damit Gruppen und Rollen auf deinen Handys verfügbar bleiben.", openSettings = "Sky Cloud-Einstellungen öffnen", - authEyebrow = "Privates Besatzungsnetz", authTitle = "Willkommen bei CrewLink", + name = "CrewLink", connecting = "CrewLink wird verbunden …", privateNetwork = "Privates Standortnetz", + signInTitle = "Sky-Cloud-Konto verbinden", signInBody = "CrewLink verwendet deine private Sky-Cloud-Identität, damit Gruppen und Rollen auf deinen Handys verfügbar bleiben.", openSettings = "Sky-Cloud-Einstellungen öffnen", + authEyebrow = "Privates Crew-Netzwerk", authTitle = "Willkommen bei CrewLink", authBody = "Deine iFruit-E-Mail wird automatisch verknüpft. Gib dein Passwort ein, um fortzufahren.", - login = "Anmelden", register = "Registrieren", ifruitEmail = "iFruit-Adresse", password = "Passwort", passwordPlaceholder = "8-72 Zeichen", gallery = "Fotos", camera = "Kamera", backToLogin = "Zurück zu CrewLink Anmeldung", + login = "Anmelden", register = "Registrieren", ifruitEmail = "iFruit-Adresse", password = "Passwort", passwordPlaceholder = "8–72 Zeichen", gallery = "Fotos", camera = "Kamera", backToLogin = "Zurück zur CrewLink-Anmeldung", authErrors = { - no_ifruit_account = "Melde dich zuerst in deinem Sky Cloud-Konto in Einstellungen an.", invalid_username = "Benutze 3-20 Buchstaben, Zahlen, Punkte oder Unterstriche.", invalid_password = "Passwort muss 8-72 Zeichen sein.", invalid_credentials = "Das Passwort für diese iFruit E-Mail ist falsch.", - profile_not_found = "Für diese iFruit E-Mail existiert kein CrewLink Profil.", profile_exists = "Diese iFruit E-Mail hat bereits ein CrewLink Profil.", - username_taken = "Der CrewLink Benutzername ist bereits vergeben.", invalid_profile_image = "Wähle ein gültiges Foto von diesem Handy.", + no_ifruit_account = "Melde dich zuerst in den Einstellungen bei deinem Sky-Cloud-Konto an.", invalid_username = "Verwende 3–20 Buchstaben, Zahlen, Punkte oder Unterstriche.", invalid_password = "Das Passwort muss 8–72 Zeichen lang sein.", invalid_credentials = "Das Passwort für diese iFruit-E-Mail ist falsch.", + profile_not_found = "Für diese iFruit-E-Mail existiert kein CrewLink-Profil.", profile_exists = "Diese iFruit-E-Mail besitzt bereits ein CrewLink-Profil.", + username_taken = "Der CrewLink-Benutzername ist bereits vergeben.", invalid_profile_image = "Wähle ein gültiges Foto von diesem Handy.", rate_limited = "Zu viele Versuche. Versuch es gleich erneut.", request_failed = "CrewLink konnte die Anfrage nicht abschließen.", }, welcomeEyebrow = "Deine Crew, ein Signal.", welcomeTitle = "Finde deine Leute", welcomeBody = "Wähle einen privaten CrewLink-Benutzernamen. Nur bestätigte Gruppenmitglieder können deine Anwesenheit und deinen gemeinsamen Standort sehen.", - username = "CrewLink Benutzername", usernamePlaceholder = "z.B. Skyline", createProfile = "CrewLink Profil erstellen", profileCreated = "Dein CrewLink Profil ist fertig.", privacyNote = "Privates Design. Keine öffentliche Spielersuche.", - noGroupEyebrow = "Keine aktive Besatzung", noGroupTitle = "Bau dein privates Netzwerk auf", noGroupBody = "Erstell eine Crew oder freunde dich mit einem privaten Einladungscode an.", + username = "CrewLink-Benutzername", usernamePlaceholder = "z. B. Skyline", createProfile = "CrewLink-Profil erstellen", profileCreated = "Dein CrewLink-Profil ist fertig.", privacyNote = "Privat entwickelt. Keine öffentliche Spielersuche.", + noGroupEyebrow = "Keine aktive Crew", noGroupTitle = "Baue dein privates Netzwerk auf", noGroupBody = "Erstelle eine Crew oder tritt mit einem privaten Einladungscode bei.", createGroup = "Gruppe erstellen", createGroupBody = "Gib deiner Crew einen erkennbaren Namen und Signalfarbe.", joinGroup = "Gruppe beitreten", joinWithCode = "Mit Code verbinden", joinWithCodeBody = "Gib den privaten 8-Zeichen-Code ein, den ein Koordinator teilt.", - groupName = "Bezeichnung der Gruppe", groupNamePlaceholder = "z.B. Nachtschicht", groupColour = "Signalfarbe", createCrew = "Crew erstellen", joinCrew = "Komm zur Crew.", inviteCode = "Einladungscode", inviteCodePlaceholder = "AB12CD34", + groupName = "Gruppenname", groupNamePlaceholder = "z. B. Nachtschicht", groupColour = "Signalfarbe", createCrew = "Crew erstellen", joinCrew = "Crew beitreten", inviteCode = "Einladungscode", inviteCodePlaceholder = "AB12CD34", groupCreated = "Crew erstellt.", joinedGroup = "Du bist der Crew beigetreten.", activeGroupChanged = "Die aktive Crew hat sich verändert.", pendingInvitations = "Einladungen", invitedBy = "Eingeladen von @{username}", inviteAccepted = "Einladung angenommen.", inviteDeclined = "Die Einladung ging zurück.", - navigation = "CrewLink Navigation", map = "Karte", crew = "Besatzung", pings = "Ping", profile = "Profil", onlineNow = "jetzt online", zoomIn = "Vergrößern", zoomOut = "Auszoomen", myLocation = "Zentrum meiner Position", - member = "Mitglied", members = "Mitglieder", openCrew = "Team ansehen", newPing = "Neuer Ping", shareLocation = "Einen Platz markieren", locationUnavailable = "Dein Live-Standort ist momentan versteckt.", activeCrew = "Aktive Privatbesatzung", groupSummarySingle = "{online} von {total} Mitglied online", groupSummary = "{online} von {total} Mitgliedern online", private = "Privat", + navigation = "CrewLink-Navigation", map = "Karte", crew = "Crew", pings = "Pings", profile = "Profil", onlineNow = "jetzt online", zoomIn = "Vergrößern", zoomOut = "Verkleinern", myLocation = "Auf meine Position zentrieren", + member = "Mitglied", members = "Mitglieder", openCrew = "Crew ansehen", newPing = "Neuer Ping", shareLocation = "Ort markieren", locationUnavailable = "Dein Live-Standort ist momentan verborgen.", activeCrew = "Aktive private Crew", groupSummarySingle = "{online} von {total} Mitglied online", groupSummary = "{online} von {total} Mitgliedern online", private = "Privat", nearby = "In der Nähe", copyCode = "Code kopieren", shareInvite = "Per Nachricht einladen", shareInviteTitle = "{group} auf CrewLink beitreten", shareInviteBody = "Benutzt Einladungscode {code}, um {group} anzuschließen.", manage = "Verwaltung", refresh = "Aktualisieren", codeCopied = "Einladungscode kopiert.", codeRotated = "Ein neuer Einladungscode ist aktiv.", - peopleNearby = "Menschen in der Nähe", peopleNearbyBody = "Hier erscheinen nur CrewLink-Benutzer innerhalb von {distance}-Metern.", metersAway = "{distance} m entfernt", invite = "Einladen", inviteSent = "Einladung an @{username} gesendet.", nobodyNearby = "Niemand in Reichweite", nobodyNearbyBody = "Zieh dich näher zu einem anderen CrewLink Benutzer und scanne wieder.", scanAgain = "Noch einmal scannen", - liveCoordination = "Koordinierung im Leben", noPings = "Keine aktiven Pings", noPingsBody = "Teile einen Treffpunkt, eine Warnung oder ein Ziel mit deiner Crew.", expiresMinutes = "{count} min links", expiresSeconds = "{count} sec links", sharedBy = "Von @{username} geteilt", setRoute = "Route festlegen", routeSet = "GPS-Routeneinstellung.", pingCreated = "Ping mit deiner Crew geteilt.", pingRemoved = "Ping entfernt.", + peopleNearby = "Personen in der Nähe", peopleNearbyBody = "Hier erscheinen nur CrewLink-Benutzer im Umkreis von {distance} Metern.", metersAway = "{distance} m entfernt", invite = "Einladen", inviteSent = "Einladung an @{username} gesendet.", nobodyNearby = "Niemand in Reichweite", nobodyNearbyBody = "Gehe näher an einen anderen CrewLink-Benutzer heran und scanne erneut.", scanAgain = "Erneut scannen", + liveCoordination = "Live-Koordination", noPings = "Keine aktiven Pings", noPingsBody = "Teile einen Treffpunkt, eine Warnung oder ein Ziel mit deiner Crew.", expiresMinutes = "noch {count} Min.", expiresSeconds = "noch {count} Sek.", sharedBy = "Von @{username} geteilt", setRoute = "Route festlegen", routeSet = "GPS-Route festgelegt.", pingCreated = "Ping mit deiner Crew geteilt.", pingRemoved = "Ping entfernt.", newPingBody = "Wähle einen Punkt auf der Karte, dann füge ein Label und einen Ping-Typ hinzu.", pingLabel = "Ping-Kennzeichnung", pingLabelPlaceholder = "z.B. Treffen in der Garage", placeOnMap = "Ping auf der Karte platzieren", placeOnMapBody = "Beweg die Karte, bis das Fadenkreuz genau den Punkt markiert.", positionSelected = "Position ausgewählt · Tippen zum Ändern", addHere = "Hierhin", sharePing = "Teilen Ping", pingTypes = { meeting = "Treffen", danger = "Gefahren", help = "Hilfe", target = "Ziel", info = "Info" }, - status = { live = "Lebend", hidden = "Standort ausgeblendet", offline = "Offline" }, + status = { live = "Live", hidden = "Standort ausgeblendet", offline = "Offline" }, roles = { owner = "Eigentümer", coordinator = "Koordinator", moderator = "Moderator", member = "Mitglied", guest = "Gast" }, - roleDescriptions = { coordinator = "Manage Einladungen, Einstellungen, Rollen und Pings.", moderator = "Lade die Nutzer ein und mische Gruppenpings.", member = "Präsenz teilen und Pings erstellen, wenn aktiviert.", guest = "Schau dir die aktive Crew ohne Koordinationsberechtigung an." }, + roleDescriptions = { coordinator = "Verwaltet Einladungen, Einstellungen, Rollen und Pings.", moderator = "Lädt Benutzer ein und verwaltet Gruppen-Pings.", member = "Teilt die Anwesenheit und erstellt Pings, wenn dies aktiviert ist.", guest = "Kann die aktive Crew ohne Koordinationsrechte ansehen." }, manageCrew = "Crew verwalten", manageCrewBody = "Aktualisiere Identität, Erlaubnisse und Einladungen.", memberPings = "Mitglieder", memberPingsBody = "Erlaubt Mitgliedern und Gästen, Pings zu erstellen.", allowOverhead = "Überkopfetiketten", allowOverheadBody = "Erlaube in der Nähe Opt-in-Mitgliedslabels der Welt.", groupSaved = "Crew-Einstellungen gespeichert.", assignRole = "Rolle zuweisen", roleUpdated = "Die Rolle der Mitglieder wird aktualisiert.", transferOwnership = "Übertragungseigentum", removeMember = "Entfernen von Crew", yourCrewLinkId = "Dein CrewLink-ID", privacyVisibility = "Datenschutz & Sichtbarkeit", shareOnMap = "Auf der Gruppenkarte teilen", shareOnMapBody = "Aktiven Mitgliedern kann man deine Live-Position zeigen.", overheadLabels = "In der Nähe befindliches Overhead-Label", overheadLabelsBody = "Zeig deinen CrewLink-Namen den nahe gelegenen, gewählten Mitgliedern.", yourGroups = "Deine Gruppen", createAnotherGroup = "Eine andere Gruppe erstellen", account = "CrewLink Konto", externalApi = "Verbundene Ressourcen", externalApiBody = "Zugelassene Skripte können temporäre Gruppenpings hinzufügen.", editUsername = "Benutzername bearbeiten", editUsernameBody = "Dein Benutzername ist in CrewLink einzigartig.", @@ -418,7 +418,7 @@ Locales["de"] = { ["delete-groupDone"] = "Die Crew wurde gelöscht.", ["leave-groupDone"] = "Du hast die Crew verlassen.", ["remove-memberDone"] = "Mitglied entfernt.", ["transfer-ownerDone"] = "Eigentum übertragen.", notifications = { invite = "{actor} hat dich zu {group} eingeladen.", member_joined = "{actor} schloss sich {group} an.", ping = "{actor} teilten '{ping}' mit {group}.", role = "Deine CrewLink Rolle hat sich in {group} verändert.", removed = "Du wurdest aus {group} entfernt.", default = "Deine CrewLink Gruppe hat ein Update." }, errors = { - not_authenticated = "Melde dich zuerst bei deinem Sky Cloud-Konto an.", profile_required = "Erstell zuerst dein CrewLink Profil.", invalid_username = "Benutze 3-20 Buchstaben, Zahlen, Punkte oder Unterstriche.", invalid_profile = "Sieh dir deine Profildetails an.", invalid_profile_image = "Wähle ein gültiges Foto von diesem Handy.", username_taken = "Der CrewLink Benutzername ist bereits vergeben.", + not_authenticated = "Melde dich zuerst bei deinem Sky-Cloud-Konto an.", profile_required = "Erstelle zuerst dein CrewLink-Profil.", invalid_username = "Verwende 3–20 Buchstaben, Zahlen, Punkte oder Unterstriche.", invalid_profile = "Prüfe deine Profildetails.", invalid_profile_image = "Wähle ein gültiges Foto von diesem Handy.", username_taken = "Der CrewLink-Benutzername ist bereits vergeben.", invalid_group = "Wähle einen gültigen Gruppennamen und Signalfarbe.", group_limit = "Du hast dein Gruppenlimit erreicht.", member_limit = "Diese Gruppe hat ihre Mitgliedergrenze erreicht.", group_not_found = "Diese Gruppe ist nicht mehr verfügbar.", invalid_code = "Dieser Einladungscode ist ungültig.", already_member = "Du bist schon ein Mitglied dieser Gruppe.", forbidden = "Deine aktuelle Rolle kann diese Aktion nicht durchführen.", player_not_nearby = "Der Spieler ist nicht mehr in der Nähe.", player_unavailable = "Dieser Spieler kann keine CrewLink Einladung erhalten.", invalid_invitation = "Diese Einladung ist ungültig.", invitation_expired = "Diese Einladung ist abgelaufen.", invalid_role = "Diese Rolle kann nicht zugewiesen werden.", owner_must_transfer = "Eigentumsübertragung vor dem Verlassen dieser Gruppe.", invalid_ping = "Wähle einen gültigen Ping-Typ, Etikett und Position.", ping_limit = "Diese Crew hat schon zu viele aktive Pings.", ping_not_found = "Dieser Ping ist nicht mehr aktiv.", rate_limited = "Zu viele CrewLink-Aktionen. Versuch es gleich erneut.", request_failed = "CrewLink ist vorübergehend nicht verfügbar.", @@ -426,42 +426,42 @@ Locales["de"] = { }, flare = { name = "Flare", signInTitle = "Melde dich bei Flare an", signInBody = "Flare-Profile und Matches sind mit deinem privaten Sky-Cloud-Konto verknüpft.", - createProfile = "Profil erstellen", welcome = "Finde deinen Funken", welcomeBody = "Stell ein Profil her und triffst Leute in Los Santos.", + createProfile = "Profil erstellen", welcome = "Finde deinen Funken", welcomeBody = "Erstelle ein Profil und lerne Leute in Los Santos kennen.", photo = "Profilfoto", profilePhotos = "Profilfotos", profilePhotosBody = "Füge ein bis sechs Fotos von Fotos oder Kamera hinzu. Dein erstes Foto wird zuerst angezeigt.", profilePhotoRequired = "Füge mindestens ein Profilfoto hinzu, um fortzufahren.", addPhotos = "Fotos hinzufügen", choosePhotos = "Wähle aus Fotos", primaryPhoto = "Haupt", removePhoto = "Foto {number} entfernen", - profilePhotoFallback = "Flare Profil Avatar", profilePhotoNumber = "Profilfoto {number} von {count}", viewProfilePhoto = "Profilfoto anzeigen {number} von {count}", + profilePhotoFallback = "Flare-Profilbild", profilePhotoNumber = "Profilfoto {number} von {count}", viewProfilePhoto = "Profilfoto {number} von {count} anzeigen", yourName = "Dein Name ist...", namePlaceholder = "Wie sollte man dich nennen?", age = "Alter", bio = "Über dich", bioPlaceholder = "Eine kurze Zeile, die ein Gespräch beginnt...", - gender = "Ich bin", showMe = "Zeig's mir.", woman = "Frau", man = "Mensch", nonbinary = "Nichtbinär", women = "Frauen", men = "Männer", nonbinaryPeople = "Nichtbinäre Personen", everyone = "Alle", - minimumAge = "Mindestalter", maximumAge = "Höchstalter", start = "Fang an zu erforschen", saveProfile = "Profil speichern", editProfileBody = "Halte dein Profil frisch und unverkennbar dich.", + gender = "Ich bin", showMe = "Zeig mir", woman = "Frau", man = "Mann", nonbinary = "Nichtbinär", women = "Frauen", men = "Männer", nonbinaryPeople = "Nichtbinäre Personen", everyone = "Alle", + minimumAge = "Mindestalter", maximumAge = "Höchstalter", start = "Entdecken starten", saveProfile = "Profil speichern", editProfileBody = "Halte dein Profil aktuell und ganz persönlich.", navigation = "Flare Navigation", discover = "Entdecken", matches = "Matches", profile = "Profil", tabs = { discover = "Swipe", explore = "Erkunden", likes = "Likes", matches = "Chatten", profile = "Profil" }, - filters = "Discovery-Einstellungen", nearby = "In der Nähe", rewind = "Rücklauf", superLike = "Super wie", explore = "Erkunden", clearExploreFilter = "Alle Profile anzeigen", - exploreTitle = "Erkunden", exploreBody = "Finde Leute, die in den gleichen Dingen sind.", + filters = "Discovery-Einstellungen", nearby = "In der Nähe", rewind = "Zurück", superLike = "Super-Like", explore = "Erkunden", clearExploreFilter = "Alle Profile anzeigen", + exploreTitle = "Erkunden", exploreBody = "Finde Leute mit denselben Interessen.", exploreModes = { forYou = "Für dich", dateNight = "Date-Night", freeTonight = "Heute Abend frei", longTerm = "Langfristige Beziehung", newFriends = "Neue Freunde", weekend = "Wochenendpläne" }, - likesTitle = "Mag dich", likesBody = "Sieh, wer dich schon mag, dann entscheide dich.", matched = "Entspricht", newMatches = "Neue Übereinstimmungen", messages = "Nachrichten", + likesTitle = "Mag dich", likesBody = "Sieh, wer dich bereits mag, und entscheide dann.", matched = "Matches", newMatches = "Neue Matches", messages = "Nachrichten", settings = "Einstellungen", editProfile = "Profil bearbeiten", editRelationshipGoal = "Beziehungsziel bearbeiten", profileGoalBody = "Auf deiner Entdeckungskarte.", like = "Gefällt mir", pass = "Überspringen", accountActions = "Account", signOut = "Abmelden", signOutTitle = "Von Sky Cloud abmelden?", signOutBody = "Damit meldest du das gesamte Handy von Sky Cloud ab. Dein Flare-Profil, deine Matches und Chats bleiben gespeichert.", signingOut = "Wird abgemeldet...", signOutHint = "Die Anmeldung betrifft jede App, die Sky Cloud auf diesem Handy verwendet.", - deleteAccount = "Flare-Konto löschen", deleteAccountTitle = "Löschst dein Flare-Konto?", deleteAccountBody = "Dein Flare-Profil, deine Swipes, Matches und Unterhaltungen werden dauerhaft gelöscht. Dein Sky-Cloud-Konto und deine Fotomediathek bleiben erhalten.", deletingAccount = "Löschen...", - relationshipGoal = "Beziehungsziel", interests = "Zinsen", interestsPlaceholder = "Musik, Reisen, Kaffee", interestsHint = "Trenne bis zu fünf Interessen mit Kommas.", + deleteAccount = "Flare-Konto löschen", deleteAccountTitle = "Flare-Konto löschen?", deleteAccountBody = "Dein Flare-Profil, deine Swipes, Matches und Unterhaltungen werden dauerhaft gelöscht. Dein Sky-Cloud-Konto und deine Fotomediathek bleiben erhalten.", deletingAccount = "Wird gelöscht...", + relationshipGoal = "Beziehungsziel", interests = "Interessen", interestsPlaceholder = "Musik, Reisen, Kaffee", interestsHint = "Trenne bis zu fünf Interessen mit Kommas.", discoverySettings = "Entdeckung", discoveryPreferences = "Wen du kennenlernen willst", showProfile = "Zeig mir Flare", showProfileBody = "Lass neue Leute dein Profil entdecken.", - discoveryPrivacyNote = "Discovery auszuschalten versteckt dich vor neuen Leuten. Deine vorhandenen Spiele und Chats bleiben verfügbar.", + discoveryPrivacyNote = "Wenn du Discovery ausschaltest, wirst du neuen Leuten nicht mehr angezeigt. Deine vorhandenen Matches und Chats bleiben verfügbar.", discoveryOffTitle = "Entdeckung ist aus", discoveryOffBody = "Dein Profil ist für neue Leute verborgen. Deine bestehenden Matches und Chats bleiben verfügbar.", discoveryOffShort = "Nur bestehende Matches können dich noch erreichen.", enableDiscovery = "Entdeckung aktivieren", saveSettings = "Einstellungen speichern", - likedYou = "Mag dich", superLikedYou = "Super gefallen dir", noLikes = "Keine neuen Likes", noLikesBody = "Leute, die dich mögen, bevor du dich entscheidest, werden hier auftauchen.", - matchActions = "Aktionen anpassen", unmatch = "Match aufheben", unmatchTitle = "Match aufheben?", unmatchBody = "Du und {name} verschwindet aus den Matchlisten des anderen. Dies kann nicht rückgängig gemacht werden.", - messagePhoto = "Foto", gif = "GIF", video = "Video", attachPhoto = "Foto anhängen", takePhoto = "Foto machen", emoji = "Emoji", attachGif = "GIF-Anlage", attachVideo = "Video anhängen", gifs = "GIF", searchGifs = "Suche nach GIPHY", loadMore = "Mehr laden", retryGifs = "Erneut versuchen", moreActions = "Weitere Maßnahmen", + likedYou = "Mag dich", superLikedYou = "Hat dir ein Super-Like gegeben", noLikes = "Keine neuen Likes", noLikesBody = "Profile, die dich bereits geliked haben, erscheinen hier.", + matchActions = "Match-Aktionen", unmatch = "Match aufheben", unmatchTitle = "Match aufheben?", unmatchBody = "Du und {name} verschwindet aus den Matchlisten des jeweils anderen. Dies kann nicht rückgängig gemacht werden.", + messagePhoto = "Foto", gif = "GIF", video = "Video", attachPhoto = "Foto anhängen", takePhoto = "Foto machen", emoji = "Emoji", attachGif = "GIF anhängen", attachVideo = "Video anhängen", gifs = "GIFs", searchGifs = "GIPHY durchsuchen", loadMore = "Mehr laden", retryGifs = "Erneut versuchen", moreActions = "Weitere Aktionen", noProfiles = "Du hast alle Profile gesehen", noProfilesBody = "Komm später wieder, wenn neue Leute Flare beitreten.", yourMatches = "Deine Matches", matchesBody = "Ein Funke geht in beide Richtungen.", newMatch = "Neues Match", noMatches = "Noch keine Matches", noMatchesBody = "Wenn dich jemand ebenfalls liked, erscheint das Match hier.", - messagePlaceholder = "Eine Nachricht schreiben", itsAMatch = "Es ist ein Funke!", matchBody = "Du und {name} habt euch geliked.", sayHello = "Sag Hallo", keepSwiping = "Weiter erforschen", newMatchNotification = "Du und {sender} habt ein Match!", newMessageNotification = "Neue Nachricht von {sender}", + messagePlaceholder = "Eine Nachricht schreiben", itsAMatch = "Es ist ein Funke!", matchBody = "Du und {name} habt euch geliked.", sayHello = "Sag Hallo", keepSwiping = "Weiter swipen", newMatchNotification = "Du und {sender} habt ein Match!", newMessageNotification = "Neue Nachricht von {sender}", lookingFor = { longTerm = "Langfristige Verbindung", dates = "Dates", friends = "Neue Freunde" }, errors = { - invalid_profile = "Prüfe deinen Namen, Alter und Profiltext.", profile_not_found = "Dieses Flare Konto ist nicht mehr verfügbar.", invalid_profile_photos = "Wähle mindestens ein und höchstens sechs Fotos aus deiner eigenen Fotomediathek aus oder nimm neue auf.", request_failed = "Flare konnte diese Änderungen nicht speichern. Versuch es erneut.", invalid_target = "Dieses Profil ist nicht mehr verfügbar.", invalid_choice = "Dieser Schlag konnte nicht gerettet werden.", invalid_discovery = "Diese Discovery-Einstellung ist ungültig.", discovery_disabled = "Schalte Discovery ein, bevor du swipst.", - nothing_to_rewind = "Es gibt keinen neuen Schlag zum Zurückspulen.", cannot_rewind_match = "Ein Streicher, der ein Match erzeugt hat, kann nicht neu gewickelt werden.", + invalid_profile = "Prüfe deinen Namen, dein Alter und deinen Profiltext.", profile_not_found = "Dieses Flare-Konto ist nicht mehr verfügbar.", invalid_profile_photos = "Wähle mindestens ein und höchstens sechs Fotos aus deiner eigenen Fotomediathek aus oder nimm neue auf.", request_failed = "Flare konnte diese Änderungen nicht speichern. Versuch es erneut.", invalid_target = "Dieses Profil ist nicht mehr verfügbar.", invalid_choice = "Dieser Swipe konnte nicht gespeichert werden.", invalid_discovery = "Diese Discovery-Einstellung ist ungültig.", discovery_disabled = "Schalte Discovery ein, bevor du swipst.", + nothing_to_rewind = "Es gibt keinen letzten Swipe zum Zurücknehmen.", cannot_rewind_match = "Ein Swipe, der zu einem Match geführt hat, kann nicht zurückgenommen werden.", match_not_found = "Dieses Match ist nicht mehr verfügbar.", invalid_message = "Schreib eine Nachricht, bevor du sie schickst.", invalid_attachment = "Dieser Anhang ist nicht verfügbar.", gif_provider_unconfigured = "GIF-Suche ist nicht konfiguriert.", gif_provider_unauthorized = "Der GIF-Anbieterschlüssel ist ungültig.", gif_provider_rate_limited = "Die GIF-Suche ist ausgelastet. Versuch es gleich erneut.", gif_provider_failed = "GIFs sind vorübergehend nicht verfügbar.", rate_limited = "Warte kurz und versuch es erneut.", default = "Flare konnte die Anfrage nicht abschließen.", }, }, picstagram = { - name = "Picstagram", loading = "Laden Picstagram", home = "Home", explore = "Erkunden", - create = "Erstellen", activity = "Aktivität", profile = "Profil", following = "Abonniert", + name = "Picstagram", loading = "Picstagram wird geladen...", home = "Home", explore = "Erkunden", + create = "Erstellen", activity = "Aktivität", profile = "Profil", following = "Folge ich", city = "Stadt", posts = "Beiträge", followers = "Follower", verified = "Verifiziertes Profil", - editProfile = "Profil bearbeiten", saved = "Gespeichert", photos = "Fotos", allPosts = "Alle Stellen", videos = "Videos", taggedPosts = "Tagged Beiträge", emptyBio = "Noch kein Bio.", + editProfile = "Profil bearbeiten", saved = "Gespeichert", photos = "Fotos", allPosts = "Alle Beiträge", videos = "Videos", taggedPosts = "Markierte Beiträge", emptyBio = "Noch keine Bio.", authTitle = "Dein Picstagram Profil", login = "Anmelden", register = "Registrieren", createAccount = "Profil erstellen", logout = "Abmelden", signingOut = "Wird abgemeldet...", loginBody = "Melde dich an, um mit deinen Fotos, Geschichten, Follows und gespeicherten Beiträgen fortzufahren.", @@ -473,38 +473,38 @@ Locales["de"] = { passwordPlaceholder = "Mindestens 8 Zeichen", confirmPasswordPlaceholder = "Passwort erneut eingeben", signOutTitle = "Von Picstagram abmelden?", signOutBody = "Dein Profil und deine Beiträge bleiben online. Auf diesem Handy erscheint wieder der Anmeldebildschirm.", - stories = "Geschichten", yourStory = "Deine Geschichte", noStories = "Keine aktiven Geschichten", - storyViews = "{count}-Ansichten", storyViewers = "Geschichtenerzähler", seenBy = "Von {count} gesehen", - nextStory = "Nächste Geschichte", closeStory = "Kurzbeschreibung", removeStory = "Geschichte entfernen", storyRemoved = "Geschichte entfernt.", - emptyFeed = "Dein Futter ist ruhig", emptyFeedBody = "Discover Profile oder teile das erste Foto.", - discoverPeople = "Menschen entdecken", searchPlaceholder = "Suchprofile, Bildunterschriften, Orte oder Tags", + stories = "Stories", yourStory = "Deine Story", noStories = "Keine aktiven Stories", + storyViews = "{count} Aufrufe", storyViewers = "Story-Aufrufe", seenBy = "Von {count} gesehen", + nextStory = "Nächste Story", closeStory = "Story schließen", removeStory = "Story entfernen", storyRemoved = "Story entfernt.", + emptyFeed = "Dein Feed ist ruhig", emptyFeedBody = "Entdecke Profile oder teile das erste Foto.", + discoverPeople = "Personen entdecken", searchPlaceholder = "Profile, Bildunterschriften, Orte oder Tags suchen", noResults = "Keine Ergebnisse", noResultsBody = "Versuch's noch mal mit einem anderen Benutzernamen, einer Beschriftung, einem Ort oder Hashtag.", - newPost = "Neuer Beitrag", newStory = "Neue Geschichte", post = "Posten", story = "Geschichte", + newPost = "Neuer Beitrag", newStory = "Neue Story", post = "Posten", story = "Story", choosePhotos = "Fotos auswählen", choosePhoto = "Foto auswählen", photo = "Foto", video = "Video", gallery = "Galerie", camera = "Kamera", choosePhotosHint = "Wähle bis zu fünf Fotos oder ein Video", chooseStoryHint = "Wähle ein Foto oder ein Video", selectedPhotos = "{count} ausgewählt", changePhotos = "Auswahl ändern", previousPhoto = "Vorheriges Foto", nextPhoto = "Nächstes Foto", caption = "Bildunterschrift", captionPlaceholder = "Schreib eine Beschriftung...", storyTextPlaceholder = "Einen Kurztext hinzufügen...", location = "Standort", locationPlaceholder = "Platz hinzufügen", allowComments = "Kommentare erlauben", - publishing = "Veröffentlichen...", published = "Dein Posten ist live.", storyPublished = "Deine Geschichte ist live.", + publishing = "Wird veröffentlicht...", published = "Dein Beitrag ist live.", storyPublished = "Deine Story ist live.", comments = "Kommentare", comment = "Kommentar", noComments = "Noch keine Kommentare", addComment = "Kommentar hinzufügen...", - reply = "Antwort", replyPlaceholder = "Schreib eine Antwort...", replyingTo = "Antwort auf {handle}", removeComment = "Kommentar entfernen", likes = "{count} mag", + reply = "Antworten", replyPlaceholder = "Schreib eine Antwort...", replyingTo = "Antwort an {handle}", removeComment = "Kommentar entfernen", likes = "{count} Likes", viewComments = "Alle Kommentare von {count} anzeigen", showReplies = "{count} Antworten anzeigen", hideReplies = "Antworten ausblenden", like = "Gefällt mir", unlike = "Gefällt mir nicht mehr", save = "Speichern", - unsave = "Aus dem Speichern entfernen", more = "Mehr", archive = "Archiv", restore = "Wiederherstellen", + unsave = "Nicht mehr speichern", more = "Mehr", archive = "Archiv", restore = "Wiederherstellen", deletePost = "Post löschen", deletePostTitle = "Diesen Beitrag löschen?", deletePostBody = "Der Beitrag und seine Reaktionen werden nicht mehr verfügbar sein.", privateProfile = "Dieses Profil ist privat", privateProfileBody = "Folge diesem Profil, um seine Beiträge und Geschichten zu sehen.", - follow = "Folgen", requested = "Angefragt", unfollow = "Abonniert", accept = "Akzeptieren", decline = "Zurückziehen", noConnections = "Noch keine Profile hier", shareProfile = "Profil teilen", share = "Teilen", - followRequests = "Anfragen befolgen", requestAccepted = "Folgst der Bitte.", requestDeclined = "Die Anfrage wurde abgelehnt.", noActivity = "Noch keine Tätigkeit", markRead = "Als gelesen markieren", + follow = "Folgen", requested = "Angefragt", unfollow = "Nicht mehr folgen", accept = "Akzeptieren", decline = "Ablehnen", noConnections = "Noch keine Profile hier", shareProfile = "Profil teilen", share = "Teilen", + followRequests = "Folgeanfragen", requestAccepted = "Folgeanfrage angenommen.", requestDeclined = "Folgeanfrage abgelehnt.", noActivity = "Noch keine Aktivität", markRead = "Als gelesen markieren", publicProfile = "Öffentliches Profil", privateProfileSetting = "Privatprofil", profileVisibility = "Profilsicht", - privacyHint = "Nur akzeptierte Anhänger können deine Beiträge und Geschichten sehen.", bio = "Bio", + privacyHint = "Nur akzeptierte Follower können deine Beiträge und Stories sehen.", bio = "Bio", avatar = "Profilfoto", chooseAvatar = "Profilfoto auswählen", removeAvatar = "Profilfoto entfernen", saveProfile = "Profil speichern", profileSaved = "Profil aktualisiert.", cancel = "Abbrechen", done = "Fertig", - report = "Melden", reportTarget = "Bericht {target}", reportReason = "Begründung", - reportDetails = "Zusätzliche Angaben (fakultativ)", submitReport = "Bericht einreichen", reported = "Bericht vorgelegt.", - block = "Profil blockieren", unblock = "Profil entsperren", blocked = "Profil blockiert.", + report = "Melden", reportTarget = "{target} melden", reportReason = "Begründung", + reportDetails = "Zusätzliche Angaben (optional)", submitReport = "Meldung einreichen", reported = "Meldung eingereicht.", + block = "Profil blockieren", unblock = "Blockierung aufheben", blocked = "Profil blockiert.", blockTitle = "@{handle} blockieren?", blockBody = "Du kannst die Inhalte dieses Profils nicht mehr sehen oder damit interagieren.", moderation = "Moderation", reports = "Offene Berichte", noReports = "Keine offenen Berichte", - noDetails = "Keine zusätzlichen Angaben", hide = "Ausblenden", remove = "Entfernen", restoreAction = "Wiederherstellen", dismiss = "Wegtreten", + noDetails = "Keine zusätzlichen Angaben", hide = "Ausblenden", remove = "Entfernen", restoreAction = "Wiederherstellen", dismiss = "Verwerfen", reportTargets = { post = "Beitrag", profile = "Profil", comment = "Kommentar", story = "Geschichte" }, reportReasons = { spam = "Spam oder irreführend", harassment = "Belästigung oder Mobbing", dangerous = "Gefährliche Tätigkeiten", illegal = "Illegaler Inhalt", other = "Etwas anderes" }, activityKinds = { follow_request = "möchte dir folgen", follow = "folgt dir jetzt", request_accepted = "hat deine Folgeanfrage angenommen", like = "gefällt dein Beitrag", comment = "hat deinen Beitrag kommentiert", comment_like = "gefällt dein Kommentar", reply = "hat auf deinen Kommentar geantwortet", verified = "Verifizierung geändert" }, @@ -530,72 +530,72 @@ Locales["de"] = { }, }, feather = { - name = "Feather", loading = "Laden Feather", home = "Home", explore = "Erkunden", activity = "Mitteilungen", activityNav = "Ausschreibungen", profile = "Profil", back = "Zurück", - settings = "Einstellungen", settingsEyebrow = "Feather-Einstellungen", compactMode = "Kompakte Zeitachse", compactModeBody = "Mehr Unterhaltung auf dem Bildschirm anzeigen.", showSuggestions = "Vorschläge für Profile", showSuggestionsBody = "Zeig den Leuten, dass sie deinem Profil folgen.", - forYou = "Für dich", following = "Abonniert", add = "Hinzufügen", network = "Netzwerk", networkTitle = "Finde deine Leute", networkBody = "Folge Stimmen aus ganz Los Santos und erstelle eine Timeline, die zu dir passt.", networkSearchPlaceholder = "Suchnamen oder @usernames", postSearchPlaceholder = "Suche nach Beiträgen oder #hashtags", searchResults = "Suchergebnisse", suggestedPeople = "Für dich empfohlen", noPeopleFound = "Keine Leute gefunden", noPeopleFoundBody = "Versuch einen anderen Namen oder Benutzernamen.", noSuggestions = "Noch keine Vorschläge", noSuggestionsBody = "Hier werden neue Feather-Konten erscheinen.", all = "Alle", mentions = "Erwähnungen", media = "Medien", trending = "Trendentwicklung", peopleTab = "Menschen", searchPlaceholder = "Suche nach Feather", search = "Suchen", cancel = "Abbrechen", done = "Fertig", + name = "Feather", loading = "Feather wird geladen...", home = "Home", explore = "Erkunden", activity = "Mitteilungen", activityNav = "Mitteilungen", profile = "Profil", back = "Zurück", + settings = "Einstellungen", settingsEyebrow = "Feather-Einstellungen", compactMode = "Kompakte Timeline", compactModeBody = "Mehr Beiträge auf dem Bildschirm anzeigen.", showSuggestions = "Profilvorschläge", showSuggestionsBody = "Zeige Profile an, denen du vielleicht folgen möchtest.", + forYou = "Für dich", following = "Folge ich", add = "Hinzufügen", network = "Netzwerk", networkTitle = "Finde deine Leute", networkBody = "Folge Stimmen aus ganz Los Santos und erstelle eine Timeline, die zu dir passt.", networkSearchPlaceholder = "Namen oder @Benutzernamen suchen", postSearchPlaceholder = "Beiträge oder #Hashtags suchen", searchResults = "Suchergebnisse", suggestedPeople = "Für dich empfohlen", noPeopleFound = "Keine Personen gefunden", noPeopleFoundBody = "Versuche es mit einem anderen Namen oder Benutzernamen.", noSuggestions = "Noch keine Vorschläge", noSuggestionsBody = "Hier werden neue Feather-Konten erscheinen.", all = "Alle", mentions = "Erwähnungen", media = "Medien", trending = "Trends", peopleTab = "Personen", searchPlaceholder = "Feather durchsuchen", search = "Suchen", cancel = "Abbrechen", done = "Fertig", authEyebrow = "Feather Netzwerk", authWelcome = "Willkommen bei Feather", authBody = "Deine iFruit-E-Mail wird automatisch verknüpft. Verwende deinen Feather-Benutzernamen, um fortzufahren.", - login = "Anmelden", register = "Registrieren", loginTitle = "Schön, dich wiederzusehen.", loginBody = "Benutzt deinen Feather Benutzername. Deine iFruit E-Mail wird automatisch verknüpft.", - registerTitle = "Erstell dein Feather Profil", registerBody = "Wähle einen Benutzernamen und ein optionales Profilfoto. Es wird kein Passwort benötigt.", + login = "Anmelden", register = "Registrieren", loginTitle = "Schön, dich wiederzusehen.", loginBody = "Verwende deinen Feather-Benutzernamen. Deine iFruit-E-Mail wird automatisch verknüpft.", + registerTitle = "Erstelle dein Feather-Profil", registerBody = "Wähle einen Benutzernamen und ein optionales Profilfoto. Es wird kein Passwort benötigt.", email = "iFruit-Adresse", emailPlaceholder = "deinname", password = "Passwort", passwordPlaceholder = "6–64 Zeichen", confirmPassword = "Passwort bestätigen", confirmPasswordPlaceholder = "Noch einmal eingeben", showPassword = "Passwort anzeigen", hidePassword = "Passwort ausblenden", loginAction = "Weiter zu Feather", registerAction = "Profil erstellen", noAccount = "Neu bei Feather?", haveAccount = "Schon registriert?", registerNow = "Profil erstellen", loginNow = "Anmelden", authTrust = "Dein Feather-Profil ist über deine iFruit-E-Mail mit deinem Sky-Cloud-Konto verknüpft.", profileStep = "Schritt 2 von 2", accountConnected = "Sky Cloud Konto verbunden", authErrors = { - no_ifruit_account = "Melde dich zuerst in deinem Sky Cloud-Konto in Einstellungen an.", invalid_handle = "Benutze 3-30 Buchstaben, Zahlen oder Unterstriche.", invalid_username = "Dieser Benutzername passt nicht zu deinem Feather Profil.", - profile_not_found = "Für diese iFruit E-Mail existiert kein Feather Profil.", already_registered = "Diese iFruit E-Mail hat bereits ein Feather Profil.", - handle_taken = "Der Feather Benutzername ist bereits vergeben.", invalid_media = "Wähle ein gültiges Foto von diesem Handy.", + no_ifruit_account = "Melde dich zuerst in den Einstellungen bei deinem Sky-Cloud-Konto an.", invalid_handle = "Verwende 3–30 Buchstaben, Zahlen oder Unterstriche.", invalid_username = "Dieser Benutzername passt nicht zu deinem Feather-Profil.", + profile_not_found = "Für diese iFruit-E-Mail existiert kein Feather-Profil.", already_registered = "Diese iFruit-E-Mail besitzt bereits ein Feather-Profil.", + handle_taken = "Der Feather-Benutzername ist bereits vergeben.", invalid_media = "Wähle ein gültiges Foto von diesem Handy.", rate_limited = "Zu viele Versuche. Versuch es in einer Minute erneut.", default = "Feather konnte die Anfrage nicht abschließen. Versuch es erneut.", }, - welcome = "Finde deine Stimme", welcomeBody = "Melde dich in Los Santos an. Dein Feather Profil bleibt mit deinem Sky Cloud Konto verknüpft.", - createProfile = "Feather Profil erstellen", profileDetailsHint = "Wähle, wie die Leute dich erkennen.", displayName = "Name der Anzeige", displayNamePlaceholder = "Dein Name ist...", handle = "Benutzername", handlePlaceholder = "dein_benutzername", handleHint = "3-30 Buchstaben, Zahlen oder Unterstriche", + welcome = "Finde deine Stimme", welcomeBody = "Melde dich in Los Santos an. Dein Feather-Profil bleibt mit deinem Sky-Cloud-Konto verknüpft.", + createProfile = "Feather-Profil erstellen", profileDetailsHint = "Wähle, wie andere dich erkennen.", displayName = "Anzeigename", displayNamePlaceholder = "Dein Name ist...", handle = "Benutzername", handlePlaceholder = "dein_benutzername", handleHint = "3–30 Buchstaben, Zahlen oder Unterstriche", bio = "Bio", bioPlaceholder = "Was sollten die Leute über dich wissen?", start = "Feather beitreten", chooseAvatar = "Profilfoto auswählen", changeAvatar = "Foto ändern", - newPost = "Neue feather", reply = "Antwort", replyingTo = "Antwort auf @{handle}", quote = "Zitat", post = "Posten", posting = "Wird gepostet …", - composerPlaceholder = "Was ist los?", replyPlaceholder = "Schick deine Antwort.", addPhotos = "Fotos hinzufügen", removePhoto = "Foto {number} entfernen", charactersLeft = "{count} links", composerAudience = "Jeder kann sich dem Gespräch anschließen", mediaTitle = "Füge zu deinem feather hinzu", mediaBody = "Teile bis zu vier Fotos von deinem Handy oder nimm ein neues.", chooseGallery = "Fotos", chooseGalleryBody = "Wähle ein oder mehrere gespeicherte Fotos", takePhoto = "Kamera", takePhotoBody = "Mach jetzt ein neues Foto", selectedPhotos = "Ausgewählte Fotos", photoLimit = "Du kannst bis zu vier Fotos hinzufügen.", - emptyFeed = "Deine Zeitlinie ist ruhig.", emptyFeedBody = "Folgst Leuten oder postest das erste feather.", emptyExplore = "Keine Ergebnisse gefunden", emptyExploreBody = "Probiere eine andere Suche oder erkunde ein Thema.", noTrendingHashtags = "Noch keine Hashtags", noTrendingHashtagsBody = "Hashtags aus veröffentlichten Beiträgen werden hier erscheinen.", - happeningNow = "Was passiert hier?", trends = "Trends für dich", trendingIn = "Trendentwicklung in Los Santos", openTopic = "Gespräch anzeigen", postsCount = "{count}-Posten", + newPost = "Neuer Beitrag", reply = "Antworten", replyingTo = "Antwort an @{handle}", quote = "Zitieren", post = "Posten", posting = "Wird gepostet …", + composerPlaceholder = "Was ist los?", replyPlaceholder = "Schreib deine Antwort.", addPhotos = "Fotos hinzufügen", removePhoto = "Foto {number} entfernen", charactersLeft = "{count} übrig", composerAudience = "Jeder kann sich an der Unterhaltung beteiligen", mediaTitle = "Medien hinzufügen", mediaBody = "Teile bis zu vier Fotos von deinem Handy oder nimm ein neues auf.", chooseGallery = "Fotos", chooseGalleryBody = "Wähle ein oder mehrere gespeicherte Fotos", takePhoto = "Kamera", takePhotoBody = "Nimm jetzt ein neues Foto auf", selectedPhotos = "Ausgewählte Fotos", photoLimit = "Du kannst bis zu vier Fotos hinzufügen.", + emptyFeed = "Deine Timeline ist ruhig.", emptyFeedBody = "Folge anderen oder veröffentliche den ersten Beitrag.", emptyExplore = "Keine Ergebnisse gefunden", emptyExploreBody = "Probiere eine andere Suche oder erkunde ein Thema.", noTrendingHashtags = "Noch keine Hashtags", noTrendingHashtagsBody = "Hashtags aus veröffentlichten Beiträgen werden hier erscheinen.", + happeningNow = "Was passiert gerade?", trends = "Trends für dich", trendingIn = "Trends in Los Santos", openTopic = "Unterhaltung anzeigen", postsCount = "{count} Beiträge", exploreTabs = { explore = "Erkunden", trending = "Trends", news = "Nachrichten" }, - trendKinds = { explore = "Trendentwicklung in Los Santos", trending = "Jetzt beliebt", news = "Nachrichten · Trend", sports = "Sport · Trend", entertainment = "Unterhaltung · Trendiv" }, - people = "Wer zu folgen hat", posts = "Beiträge", followers = "Follower", followingCount = "Abonniert", removeConnection = "Entfernen", followerRemoved = "Anhänger entfernt.", followingRemoved = "Konto unfolgbar.", noConnections = "Hier ist noch niemand.", noConnectionsBody = "Profile werden hier erscheinen, wenn Verbindungen hergestellt werden.", follow = "Folgen", unfollow = "Abonniert", - editProfile = "Profil bearbeiten", saveProfile = "Speichern", joined = "Verbunden mit Feather", joinedDate = "Seit August 2026", shareProfile = "Profil teilen", profileCopied = "Profilhandle kopiert.", postCopied = "Post kopiert.", verified = "Verifiziertes Profil", noBio = "Noch kein Bio.", showMore = "Mehr anzeigen", moreActions = "Weitere Maßnahmen", - now = "jetzt", minutesShort = "{count}m", hoursShort = "{count}h", daysShort = "{count}d", comments = "Kommentare", noComments = "Noch keine Kommentare", addComment = "Kommentar hinzufügen...", postComment = "Kommentar posten", likeComment = "Kommentar loben von {name}", likesCount = "{count} mag", - replies = "Antworten", noReplies = "Noch keine Antworten, beginne die Unterhaltung.", likes = "Gefällt mir", bookmarks = "Lesezeichen", noBookmarks = "Noch keine gespeicherten Beiträge", noBookmarksBody = "Posts, die du bookmark hier erscheinen.", imagePreview = "Vorschau", openImage = "Bild öffnen {number}", postImage = "Bild posten {number}", previousImage = "Vorheriges Bild", nextImage = "Nächstes Bild", delete = "Löschen", report = "Melden", block = "Blockieren @{handle}", - reportTitle = "Bericht feather", reportBody = "Erzähl uns, warum diese feather überprüft werden sollte.", reportSubmit = "Bericht einreichen", reported = "Bericht vorgelegt.", blocked = "Profil blockiert.", deleted = "Feather gelöscht.", + trendKinds = { explore = "Trends in Los Santos", trending = "Jetzt beliebt", news = "Nachrichten · Trend", sports = "Sport · Trend", entertainment = "Unterhaltung · Trend" }, + people = "Wem du folgen kannst", posts = "Beiträge", followers = "Follower", followingCount = "Folge ich", removeConnection = "Entfernen", followerRemoved = "Follower entfernt.", followingRemoved = "Du folgst dem Konto nicht mehr.", noConnections = "Hier ist noch niemand.", noConnectionsBody = "Profile erscheinen hier, sobald Verbindungen bestehen.", follow = "Folgen", unfollow = "Nicht mehr folgen", + editProfile = "Profil bearbeiten", saveProfile = "Speichern", joined = "Feather beigetreten", joinedDate = "Seit August 2026", shareProfile = "Profil teilen", profileCopied = "Profil-Benutzername kopiert.", postCopied = "Beitrag kopiert.", verified = "Verifiziertes Profil", noBio = "Noch keine Bio.", showMore = "Mehr anzeigen", moreActions = "Weitere Aktionen", + now = "jetzt", minutesShort = "{count}m", hoursShort = "{count}h", daysShort = "{count}d", comments = "Kommentare", noComments = "Noch keine Kommentare", addComment = "Kommentar hinzufügen...", postComment = "Kommentar posten", likeComment = "Kommentar von {name} liken", likesCount = "{count} Likes", + replies = "Antworten", noReplies = "Noch keine Antworten – beginne die Unterhaltung.", likes = "Gefällt mir", bookmarks = "Lesezeichen", noBookmarks = "Noch keine gespeicherten Beiträge", noBookmarksBody = "Beiträge, die du speicherst, erscheinen hier.", imagePreview = "Vorschau", openImage = "Bild {number} öffnen", postImage = "Beitragsbild {number}", previousImage = "Vorheriges Bild", nextImage = "Nächstes Bild", delete = "Löschen", report = "Melden", block = "@{handle} blockieren", + reportTitle = "Feather-Beitrag melden", reportBody = "Sag uns, warum dieser Beitrag überprüft werden sollte.", reportSubmit = "Meldung einreichen", reported = "Meldung eingereicht.", blocked = "Profil blockiert.", deleted = "Feather gelöscht.", reasons = { spam = "Spam", harassment = "Belästigung", dangerous = "Gefährlicher Inhalt", illegal = "Illegaler Inhalt", other = "Sonstige" }, - noActivity = "Noch keine Tätigkeit", activityBody = "Hier werden Likes, Antworten und neue Follower erscheinen.", - activityKinds = { like = "Gefiel dein feather", reply = "Hast auf deine feather geantwortet.", follow = "dir gefolgt", quote = "Hast deine feather zitiert." }, - errors = { accountRequired = "Melde dich zuerst bei Sky Cloud in Einstellungen an.", invalidHandle = "Benutze 3–30 Buchstaben, Zahlen oder Unterstriche.", handleTaken = "Der Griff ist schon genommen.", invalidPost = "Schreib etwas oder hänge ein Foto an.", generic = "Etwas ist schiefgelaufen. Versuch es erneut." }, + noActivity = "Noch keine Aktivität", activityBody = "Hier erscheinen Likes, Antworten und neue Follower.", + activityKinds = { like = "gefällt dein Feather-Beitrag", reply = "hat auf deinen Feather-Beitrag geantwortet", follow = "folgt dir jetzt", quote = "hat deinen Feather-Beitrag zitiert" }, + errors = { accountRequired = "Melde dich zuerst in den Einstellungen bei Sky Cloud an.", invalidHandle = "Verwende 3–30 Buchstaben, Zahlen oder Unterstriche.", handleTaken = "Der Benutzername ist bereits vergeben.", invalidPost = "Schreib etwas oder hänge ein Foto an.", generic = "Etwas ist schiefgelaufen. Versuch es erneut." }, notifications = { default = "Du hast neue Aktivitäten.", like = "{actor} gefällt dein Feather-Beitrag.", reply = "{actor} hat auf deinen Feather-Beitrag geantwortet.", follow = "{actor} folgt dir jetzt.", quote = "{actor} hat deinen Feather-Beitrag zitiert." }, }, fliptok = { - name = "FlipTok", loading = "Laden FlipTok", following = "Abonniert", forYou = "Für dich", verified = "Verifiziertes Konto", + name = "FlipTok", loading = "FlipTok wird geladen...", following = "Folge ich", forYou = "Für dich", verified = "Verifiziertes Konto", originalSound = "Originalton", save = "Speichern", home = "Home", discover = "Entdecken", create = "Erstellen", activity = "Aktivität", profile = "Profil", navigation = "FlipTok Navigation", - emptyFeed = "Noch keine Videos", emptyFeedBody = "Folge Schöpfern oder poste die erste FlipTok.", searchPlaceholder = "Suche Schöpfer und Videos", clearSearch = "Suche löschen", - trendLosSantos = "# Los Santos", trendRoleplay = "♪ Rollenspiele", trendTrending = "♪ Entwicklung", - noActivity = "Noch keine Tätigkeit", followers = "Follower", videos = "Videos", emptyBio = "Noch kein Bio.", editProfile = "Profil bearbeiten", more = "Mehr", - newVideo = "Neue FlipTok", createTitle = "Erstell deine nächste FlipTok", createBody = "Nimm einen Clip auf oder verwandle Fotos in einen FlipTok zum Durchwischen.", chooseMedia = "Medien auswählen", chooseMediaHint = "Nimm ein Video auf, wähle einen Clip oder bis zu 10 Fotos aus.", recordVideo = "Video aufnehmen", camera = "Kamera", chooseVideo = "Video auswählen", photoSlideshow = "Fotos als FlipTok", changeMedia = "Veränderung", previousPhoto = "Vorheriges Foto", nextPhoto = "Nächstes Foto", - caption = "Bildunterschrift", captionPlaceholder = "Schreib eine Beschriftung...", location = "Standort hinzufügen", whoCanWatch = "Wer kann zusehen", public = "Alle", + emptyFeed = "Noch keine Videos", emptyFeedBody = "Folge Creatorn oder poste dein erstes FlipTok.", searchPlaceholder = "Creator und Videos suchen", clearSearch = "Suche löschen", + trendLosSantos = "# Los Santos", trendRoleplay = "♪ Roleplay", trendTrending = "♪ Trends", + noActivity = "Noch keine Aktivität", followers = "Follower", videos = "Videos", emptyBio = "Noch keine Bio.", editProfile = "Profil bearbeiten", more = "Mehr", + newVideo = "Neues FlipTok", createTitle = "Erstelle dein nächstes FlipTok", createBody = "Nimm einen Clip auf oder verwandle Fotos in ein FlipTok zum Durchwischen.", chooseMedia = "Medien auswählen", chooseMediaHint = "Nimm ein Video auf, wähle einen Clip oder bis zu 10 Fotos aus.", recordVideo = "Video aufnehmen", camera = "Kamera", chooseVideo = "Video auswählen", photoSlideshow = "Fotos als FlipTok", changeMedia = "Ändern", previousPhoto = "Vorheriges Foto", nextPhoto = "Nächstes Foto", + caption = "Beschreibung", captionPlaceholder = "Schreib eine Beschreibung...", location = "Standort hinzufügen", whoCanWatch = "Wer kann es ansehen?", public = "Alle", followersOnly = "Follower", private = "Nur ich", allowComments = "Kommentare erlauben", saveDraft = "Entwurf speichern", publishing = "Wird gepostet …", post = "Posten", - draftSaved = "Der Entwurf wurde gerettet.", published = "Dein FlipTok ist live.", videoDeleted = "Video entfernt.", deleteVideoTitle = "Dieses Video entfernen?", deleteVideoBody = "Das Video verschwindet aus FlipTok und kann nicht wiederhergestellt werden.", deletingVideo = "Entfernen...", linkCopied = "Video-Link kopiert.", reported = "Bericht vorgelegt.", blocked = "Schöpfer blockiert.", + draftSaved = "Entwurf gespeichert.", published = "Dein FlipTok ist live.", videoDeleted = "Video entfernt.", deleteVideoTitle = "Dieses Video entfernen?", deleteVideoBody = "Das Video verschwindet aus FlipTok und kann nicht wiederhergestellt werden.", deletingVideo = "Wird entfernt...", linkCopied = "Video-Link kopiert.", reported = "Meldung eingereicht.", blocked = "Creator blockiert.", comments = "Kommentare", noComments = "Noch keine Kommentare", addComment = "Kommentar hinzufügen...", reply = "Antwort", showReplies = "{count} Antworten anzeigen", hideReplies = "{count} Antworten ausblenden", replyPlaceholder = "Schreib eine Antwort...", replyingTo = "Antwort auf {handle}", cancelReply = "Antwort abbrechen", report = "Video melden", reportReason = "Begründung", - reportDetails = "Zusätzliche Angaben (fakultativ)", submitReport = "Bericht einreichen", reportDiscordNote = "Dieser Bericht wird direkt an das Moderationsteam durch Discord geschickt.", + reportDetails = "Zusätzliche Angaben (optional)", submitReport = "Meldung einreichen", reportDiscordNote = "Diese Meldung wird direkt über Discord an das Moderationsteam gesendet.", reportReasons = { spam = "Spam oder irreführend", harassment = "Belästigung oder Mobbing", dangerous = "Gefährliche Tätigkeiten", illegal = "Illegaler Inhalt", other = "Etwas anderes" }, - block = "Ersteller der Blöcke", follow = "Folgen", unfollow = "Abonniert", backToProfile = "Zurück", cancel = "Abbrechen", - sounds = "Geräusch", chooseSound = "Musik auswählen", originalOnly = "Nur Original-Sound", noMusic = "Es sind keine Musikspuren konfiguriert.", + block = "Creator blockieren", follow = "Folgen", unfollow = "Nicht mehr folgen", backToProfile = "Zurück", cancel = "Abbrechen", + sounds = "Sounds", chooseSound = "Musik auswählen", originalOnly = "Nur Original-Sound", noMusic = "Es sind keine Musiktitel konfiguriert.", customSound = "Benutzerdefinierter Ton", customSoundLink = "YouTube oder Audio-Link", customSoundHint = "Füge einen YouTube Link oder einen direkten HTTPS Audio-Datei Link ein.", customSoundPlaceholder = "https://youtu.be/... oder https://example.com/song.mp3", - customSoundFormats = "YouTube, YouTube Musik, Shorts, MP3, M4A, AAC, OGG, OPUS, WAV oder WEBM", useCustomSound = "Ton verwenden", loadingSound = "Ladegeräusch", invalidCustomSoundLink = "Verwende einen YouTube Link oder einen direkten öffentlichen HTTPS Audio-Datei Link.", customSoundLoadFailed = "Dieser Klang konnte nicht geladen werden.", - trimAndCover = "Beschriftung", trimStart = "Starten", trimEnd = "Ende", coverFrame = "Abdeckung", originalVolume = "Ursprünglicher Ton", musicVolume = "Musik", - moderation = "Moderation", reports = "Offene Berichte", noReports = "Keine offenen Berichte", removeVideo = "Video entfernen", dismissReport = "Wegtreten", + customSoundFormats = "YouTube, YouTube Music, Shorts, MP3, M4A, AAC, OGG, OPUS, WAV oder WEBM", useCustomSound = "Sound verwenden", loadingSound = "Sound wird geladen...", invalidCustomSoundLink = "Verwende einen YouTube-Link oder einen direkten öffentlichen HTTPS-Audio-Link.", customSoundLoadFailed = "Dieser Sound konnte nicht geladen werden.", + trimAndCover = "Zuschneiden & Titelbild", trimStart = "Start", trimEnd = "Ende", coverFrame = "Titelbild", originalVolume = "Originalton", musicVolume = "Musik", + moderation = "Moderation", reports = "Offene Meldungen", noReports = "Keine offenen Meldungen", removeVideo = "Video entfernen", dismissReport = "Meldung verwerfen", done = "Fertig", savingProfile = "Speichern...", displayName = "Name", username = "Benutzername", bio = "Bio", accountType = "Kontotyp", profilePhoto = "Profilfoto", changePhoto = "Foto ändern", chooseFromGallery = "Fotos", takePhoto = "Kamera", removePhoto = "Foto entfernen", noConnections = "Keine Profile zu zeigen", authTitle = "Dein FlipTok Konto", login = "Anmelden", register = "Registrieren", createAccount = "Account erstellen", logout = "Abmelden", - loginBody = "Melde dich an, um mit deinen Videos fortzufahren, follows, und gespeicherte Beiträge.", registerBody = "Erstell ein privates FlipTok Login für dieses Profil.", + loginBody = "Melde dich an, um mit deinen Videos, Follows und gespeicherten Beiträgen fortzufahren.", registerBody = "Erstelle einen privaten FlipTok-Login für dieses Profil.", password = "Passwort", confirmPassword = "Passwort bestätigen", passwordsMismatch = "Die Passwörter stimmen nicht überein.", displayNamePlaceholder = "Dein Name ist...", usernamePlaceholder = "Benutzername", passwordPlaceholder = "Mindestens 8 Zeichen", confirmPasswordPlaceholder = "Passwort erneut eingeben", registrationHint = "Ein Sky Cloud-Konto wird einmal benötigt, um ein FlipTok-Profil zu erstellen und zu besitzen.", accountDetails = "Angaben zum Konto", profileDetails = "Profilangaben", account = "Account", signOutTitle = "Von FlipTok abmelden?", signOutBody = "Dein Profil und deine Videos bleiben online. Auf diesem Handy erscheint wieder der FlipTok-Anmeldebildschirm.", signingOut = "Wird abgemeldet...", - accountTypes = { person = "Person", business = "Unternehmen", organization = "Organisation", media = "Medien", event = "Ereignis" }, - activityKinds = { like = "Hast dein Video gefallen", comment = "Hast dein Video kommentiert", follow = "begann, dir zu folgen", verified = "Überprüfung geändert" }, + accountTypes = { person = "Person", business = "Unternehmen", organization = "Organisation", media = "Medien", event = "Event" }, + activityKinds = { like = "gefällt dein Video", comment = "hat dein Video kommentiert", follow = "folgt dir jetzt", verified = "Verifizierung geändert" }, notifications = { like = "{actor} gefällt dein Video.", comment = "{actor} hat dein Video kommentiert.", follow = "{actor} folgt dir jetzt.", verified = "Dein FlipTok Konto ist jetzt verifiziert.", default = "Du hast neue FlipTok Aktivität." }, errors = { invalid_video = "Sieh dir die FlipTok Details an.", invalid_music_url = "Verwende einen gültigen YouTube oder direkten HTTPS-Audio-Datei-Link.", invalid_media = "Wähle gültige Medien von diesem Handy.", invalid_comment = "Gib einen gültigen Kommentar ein.", comments_disabled = "Kommentare sind deaktiviert.", invalid_profile = "Sieh dir deine Profildetails an.", invalid_profile_image = "Wähle ein gültiges Foto von diesem Handy.", invalid_handle = "Benutze 3–24 Buchstaben, Zahlen, Punkte oder Unterstriche.", invalid_display_name = "Gib einen Namen ein.", invalid_password = "Passwort muss 8–72 Zeichen sein.", invalid_credentials = "Benutzername oder Passwort ist falsch.", already_registered = "Dieses Sky Cloud Konto besitzt bereits ein registriertes FlipTok Profil.", handle_taken = "Dieser Benutzername ist bereits vergeben.", video_not_found = "Dieses Video ist nicht verfügbar.", profile_not_found = "Dieses Profil ist nicht verfügbar.", blocked = "Dieses Konto ist gesperrt.", not_authorized = "Du hast keinen Zugang zu Mäßigung.", report_not_found = "Dieser Bericht ist nicht mehr offen.", report_unavailable = "Discord Reporting ist noch nicht konfiguriert.", request_failed = "Deine FlipTok-Anfrage konnte nicht abgeschlossen werden. Versuch es erneut.", rate_limited = "Zu viele Aktionen. Versuch es gleich erneut.", not_authenticated = "Melde dich zuerst bei Sky Cloud an.", default = "FlipTok konnte die Anfrage nicht abschließen." }, }, @@ -633,7 +633,7 @@ Locales["de"] = { attachGif = "GIF-Anlage", attachVideo = "Video anhängen", photos = "Fotos", gifs = "GIF", videos = "Videos", contacts = "Kontakte", shareContact = "Kontakt teilen", noContactsToShare = "Spar dir zuerst einen Kontakt, um ihn hier zu teilen.", contactSaved = "Kontakt gespeichert", noPhotos = "Mach zuerst ein Foto, um es hier anzubringen.", noVideos = "Zeichne zuerst ein Video in der Kamera auf.", searchGifs = "Suche nach GIPHY", loadMore = "Mehr laden", retryGifs = "Erneut versuchen", - moreActions = "Weitere Maßnahmen", inboxActions = "Gesprächsaktionen", sortLabel = "Gesprächssortierung", sortNewest = "Neueste erste", sortOldest = "Ältester Erster", unreadConversation = "{name}, {count} ungelesene Nachrichten", + moreActions = "Weitere Aktionen", inboxActions = "Unterhaltungsaktionen", sortLabel = "Unterhaltungssortierung", sortNewest = "Neueste zuerst", sortOldest = "Älteste zuerst", unreadConversation = "{name}, {count} ungelesene Nachrichten", attachmentPreview = "Ausgewählte Anlagen", attachmentLimit = "Du kannst bis zu {count} Fotos oder Videos anhängen.", removeAttachment = "Anhang {number} entfernen", contactDetails = "Kontaktdetails", contactName = "Name", phoneNumber = "Telefonnummer", company = "Unternehmen", contactActions = "Kontaktaktionen", call = "Anrufen", messageAction = "Nachricht", addContact = "Kontakt hinzufügen", showInContacts = "In Kontakten anzeigen", blockContact = "Kontakt blockieren", @@ -645,7 +645,7 @@ Locales["de"] = { gifLabels = { celebrate = "Feiern!", hearts = "Gefällt mir", party = "Partyzeit", thumbsUp = "Perfekt", wow = "WOW" }, voiceMessage = "Audio-Nachricht", recordVoice = "Audio aufnehmen", playAudio = "Audio abspielen", pauseAudio = "Audio anhalten", seekAudio = "Audio suchen", - recording = "Aufzeichnung", stopAndSend = "Anhalten und senden", cancelRecording = "Aufzeichnung abbrechen", + recording = "Aufnahme", stopAndSend = "Stoppen und senden", cancelRecording = "Aufnahme abbrechen", sending = "Senden...", delivered = "Lieferung", notDelivered = "Nicht geliefert", microphoneUnavailable = "Das Mikrofon ist nicht verfügbar.", recordingTooLarge = "Die Aufnahme ist zu groß.", noSim = "Keine SIM", noSimBody = "Lege in den Einstellungen eine SIM-Karte ein, um Nachrichten zu senden und zu empfangen.", @@ -669,8 +669,8 @@ Locales["de"] = { }, companies = { name = "Unternehmen", - navigation = "Unternehmen Navigation", - actions = "Maßnahmen anfordern", + navigation = "Unternehmensnavigation", + actions = "Anfrageaktionen", request = "Service-Anfrage", verified = "Verifiziertes Unternehmen", back = "Zurück", @@ -680,7 +680,7 @@ Locales["de"] = { routeSet = "GPS-Routeneinstellung.", tabs = { directory = "Entdecken", - requests = "Anträge", + requests = "Anfragen", work = "Arbeit", }, availability = { @@ -694,19 +694,19 @@ Locales["de"] = { in_progress = "In Arbeit", waiting_customer = "Warten", completed = "Abgeschlossen", - cancelled = "Gestorben", + cancelled = "Abgebrochen", }, roles = { - employee = "Arbeitnehmer", - manager = "Verwaltung", + employee = "Mitarbeiter", + manager = "Manager", }, categories = { gastronomy = "Essen & Trinken", vehicles = "Fahrzeuge", transport = "Verkehr", - crafts = "Handel", + crafts = "Handwerk", retail = "Einzelhandel", - real_estate = "Grundstücks- und Wohnungswesen", + real_estate = "Immobilien", media = "Medien", nightlife = "Nachtleben", public_services = "Öffentliche Dienstleistungen", @@ -730,37 +730,37 @@ Locales["de"] = { allCategories = "Alle", allCompanies = "Alle Unternehmen", availableNow = "Jetzt verfügbar", - availableNowHint = "Nur Firmen zeigen, die gerade Anrufe tätigen.", + availableNowHint = "Nur Unternehmen anzeigen, die derzeit Anfragen annehmen.", availableSection = "Jetzt verfügbar", moreFilters = "Mehr Filter", hasLocation = "Standort", - acceptsRequests = "Anträge", + acceptsRequests = "Nimmt Anfragen an", resetFilters = "Filter zurücksetzen", }, loading = { - directory = "Laden von Unternehmen...", + directory = "Unternehmen werden geladen...", profile = "Unternehmensprofil wird geladen...", request = "Anfrage wird geladen...", - requests = "Belade deine Anfragen.", - work = "Arbeitsfläche wird geladen...", + requests = "Deine Anfragen werden geladen...", + work = "Arbeitsbereich wird geladen...", }, states = { directoryError = "Unternehmen sind nicht verfügbar", requestsError = "Anfragen sind nicht verfügbar", - workError = "Arbeitsplatz nicht verfügbar", + workError = "Arbeitsbereich nicht verfügbar", profileError = "Unternehmen nicht verfügbar", - requestError = "Nicht verfügbar anfordern", + requestError = "Anfrage nicht verfügbar", noCompanies = "Keine Unternehmen aufgeführt", noCompaniesBody = "Öffentliche Unternehmen werden hier erscheinen, wenn sie konfiguriert sind.", noResults = "Keine passenden Unternehmen", noResultsBody = "Versuch's noch mal, oder stell deine Filter zurück.", - noOpenRequests = "Keine offenen Anträge", - noClosedRequests = "Keine ausgefüllten Anträge", - noRequestsBody = "Du musst mit einer Firma arbeiten.", + noOpenRequests = "Keine offenen Anfragen", + noClosedRequests = "Keine abgeschlossenen Anfragen", + noRequestsBody = "Deine Serviceanfragen erscheinen hier.", }, actionUnavailable = { - call = "Diese Firma kann man nicht jetzt anrufen.", - message = "Diese Servicezeile akzeptiert keine SMS.", + call = "Dieses Unternehmen kann derzeit nicht angerufen werden.", + message = "Diese Serviceleitung akzeptiert keine Nachrichten.", route = "Diese Firma hat keinen öffentlichen Standort.", request = "Diese Firma akzeptiert keine Serviceanfragen.", }, @@ -768,7 +768,7 @@ Locales["de"] = { updated = "Status aktualisiert {time}", announcement = "Letzte Aktualisierung", location = "Standort", - noLocation = "Keine öffentliche Einrichtung", + noLocation = "Kein öffentlicher Standort", hours = "Öffnungszeiten", closed = "Geschlossen", byAvailability = "Nach Verfügbarkeit geöffnet", @@ -776,73 +776,73 @@ Locales["de"] = { noServices = "Keine öffentlichen Dienstleistungen aufgeführt", call = "Anrufen", message = "Nachricht", - route = "Art der Anwendung", - request = "Antrag", + route = "Route", + request = "Anfrage", }, composer = { title = "Neue Service-Anfrage", chooseService = "Wähle einen Dienst", - subject = "Gegenstand", + subject = "Betreff", subjectPlaceholder = "Was brauchst du?", description = "Einzelheiten", descriptionPlaceholder = "Beschreibe, was passiert ist und wie die Firma helfen kann.", contact = "Kontaktnummer", - registeredSim = "Antworten gehen zu deinem registrierten SIM {number}.", + registeredSim = "Antworten gehen an deine registrierte SIM {number}.", registeredSimRequired = "Für den Versand einer Anfrage ist eine registrierte SIM erforderlich.", addPhotos = "Fotos hinzufügen ({count}/3)", - selectedPhoto = "Ausgewählte Anfrage Foto", + selectedPhoto = "Ausgewähltes Anfragefoto", removePhoto = "Foto entfernen", - review = "Überprüfungsantrag", + review = "Anfrage prüfen", confirmTitle = "Diese Anfrage senden?", confirmBody = "Schick deine {service}-Anfrage an {company}.", edit = "Bearbeiten fortsetzen", send = "Anfrage senden", }, requests = { - open = "Öffnen", + open = "Offen", closed = "Abgeschlossen", findCompany = "Unternehmen suchen", generalService = "Allgemeines", - attachments = "Fotos anfordern", + attachments = "Anhänge", attachedPhoto = "Angehängtes Anfragefoto", timeline = "Zeitleiste", conversation = "Gespräch", noMessages = "Noch keine Antworten", replyPlaceholder = "Schreib eine Antwort", sendReply = "Antwort senden", - call = "Anruf Firma", - cancel = "Antrag auf Widerruf", - cancelTitle = "Diesen Antrag absagen?", + call = "Unternehmen anrufen", + cancel = "Anfrage abbrechen", + cancelTitle = "Diese Anfrage abbrechen?", cancelBody = "Das Unternehmen wird benachrichtigt und kann es nicht mehr abschließen.", keep = "Anfrage weiterführen", - cancelConfirm = "Antrag auf Widerruf", + cancelConfirm = "Anfrage abbrechen", }, timeline = { - created = "Erstellte Anfrage", - assigned = "Auftrag vergeben", - cancelled = "Antrag auf Löschung", - completed = "Antrag abgeschlossen", + created = "Anfrage erstellt", + assigned = "Anfrage zugewiesen", + cancelled = "Anfrage abgebrochen", + completed = "Anfrage abgeschlossen", statusChanged = "Status geändert auf {status}", }, time = { justNow = "gerade jetzt", - minutesAgo = "{count} Vor min", - hoursAgo = "{count} Vor", - daysAgo = "{count} Vor Tagen", + minutesAgo = "vor {count} Min.", + hoursAgo = "vor {count} Std.", + daysAgo = "vor {count} Tagen", }, work = { notAuthorized = "Kein Arbeitsplatz für Unternehmen", notAuthorizedBody = "Dein aktueller Job ist nicht mit einer konfigurierten Firma verbunden.", - workspace = "Unternehmen Arbeitsplatz", + workspace = "Unternehmensarbeitsbereich", publicAvailability = "Öffentliche Verfügbarkeit", - takeCalls = "Unternehmen Anrufe", - takeCallsBody = "Route neue Service-Line-Anrufe auf diese aktive SIM.", - dialServiceLine = "Anruf von der Serviceleitung", + takeCalls = "Unternehmensanrufe", + takeCallsBody = "Leite neue Anrufe der Serviceleitung an diese aktive SIM weiter.", + dialServiceLine = "Über Serviceleitung anrufen", dialServiceLineBody = "Mach einen Anruf, der {number} anzeigt.", dialServiceLineHint = "Der Empfänger sieht {number} als eingehenden Anrufer.", targetNumber = "Telefonnummer", targetNumberHint = "Telefonnummer eingeben", - callNow = "Ruf jetzt an.", + callNow = "Jetzt anrufen", overview = "Heute auf einen Blick", metrics = { new = "Neu", @@ -850,7 +850,7 @@ Locales["de"] = { waiting = "Warten", completedToday = "Heute fertig", }, - queue = "Warteschlange anfordern", + queue = "Anfragewarteschlange", filters = { new = "Neu", assigned = "Meine", @@ -863,25 +863,25 @@ Locales["de"] = { emptyQueueBody = "Keine Anfragen entsprechen diesem Filter.", }, workActions = { - claim = "Antrag auf Antrag", - assign = "Mitarbeiter", + claim = "Anfrage übernehmen", + assign = "Mitarbeiter zuweisen", callCustomer = "Kunden anrufen", setStatus = "Status festlegen: {status}", }, assignment = { - title = "Mitarbeiter", + title = "Mitarbeiter zuweisen", online = "Online", offline = "Offline", - unknownMember = "Unbekannter Arbeitnehmer", - confirm = "Antrag zuweisen", + unknownMember = "Unbekannter Mitarbeiter", + confirm = "Anfrage zuweisen", }, assignmentLabels = { you = "Dir zugewiesen", assigned = "Zugeordnet an einen Kollegen", }, messageAuthors = { - you = "Du bist ja auch da.", - customer = "Abnehmer", + you = "Du", + customer = "Kunde", company = "Unternehmen", }, manager = { @@ -895,9 +895,9 @@ Locales["de"] = { phoneNumber = "Dienstnummer", phoneNumberManaged = "Konfiguriert vom Server und kann hier nicht bearbeitet werden.", noPhoneNumber = "Keine Servicenummer konfiguriert", - chooseCover = "Wähle Coverfoto", + chooseCover = "Titelbild auswählen", chooseLogo = "Logo auswählen", - coverPhoto = "Foto der Firmenabdeckung", + coverPhoto = "Unternehmens-Titelbild", logoPhoto = "Firmenlogo", locationLabel = "Name des Standorts", locationReady = "Eine Kartenposition ist diesem Profil beigefügt.", @@ -911,7 +911,7 @@ Locales["de"] = { dayOpen = "Die Firma ist an diesem Tag geöffnet", opensAt = "Öffnet", closesAt = "Schließt", - saveHours = "Stunden speichern", + saveHours = "Öffnungszeiten speichern", services = "Dienstleistungen", serviceTitle = "Dienstname", serviceDescription = "Beschreibung", @@ -928,15 +928,15 @@ Locales["de"] = { publish = "Ankündigung veröffentlichen", }, feedback = { - requestCreated = "Bitte gesendet.", + requestCreated = "Anfrage gesendet.", requestCancelled = "Anfrage abgesagt.", - requestClaimed = "Bitte beantragt.", - requestAssigned = "Bitte zugewiesen.", - statusUpdated = "Status aktualisiert anfordern.", + requestClaimed = "Anfrage übernommen.", + requestAssigned = "Anfrage zugewiesen.", + statusUpdated = "Anfragestatus aktualisiert.", availabilityUpdated = "Öffentliche Verfügbarkeit aktualisiert.", locationUpdated = "Aktueller Standort ausgewählt.", callsEnabled = "Firmenanrufe aktiviert.", - callsDisabled = "Firmenbesuche deaktiviert.", + callsDisabled = "Unternehmensanrufe deaktiviert.", profileSaved = "Firmenprofil gespeichert.", hoursSaved = "Öffnungszeiten gespeichert.", servicesSaved = "Dienste gespeichert.", @@ -948,10 +948,10 @@ Locales["de"] = { reload = "Nachladen", }, notifications = { - newRequest = "Antrag auf neues Unternehmen", + newRequest = "Neue Unternehmensanfrage", requestUpdated = "Eine Unternehmensanfrage wurde aktualisiert.", newMessage = "Neue Antwort auf deine Firmenanfrage.", - assigned = "Dir wurde ein Firmenwunsch zugeteilt.", + assigned = "Dir wurde eine Unternehmensanfrage zugewiesen.", }, errors = { anonymous_sim = "Für Serviceanfragen ist ein registriertes SIM erforderlich.", @@ -977,40 +977,40 @@ Locales["de"] = { phone = { name = "Telefon", recents = "Anrufliste", contacts = "Kontakte", keypad = "Tastatur", noSim = "Keine SIM", noSimBody = "Füge eine SIM-Karte in Einstellungen ein, um Anrufe zu tätigen.", - noRecents = "Keine aktuellen Ausschreibungen", allCalls = "Alle", missedCalls = "Verpasst", searchRecents = "Anrufe suchen", + noRecents = "Keine letzten Anrufe", allCalls = "Alle", missedCalls = "Verpasst", searchRecents = "Anrufe suchen", contactDetails = "Kontaktdetails", unknownCaller = "Unbekannter Anrufer", callHistory = "Anrufverlauf", noCallHistory = "Noch keine Anrufe mit dieser Nummer.", noContacts = "Keine Kontakte", searchContacts = "Kontakte suchen", favorites = "Favoriten", myCard = "Meine Karte", myNumber = "Meine Nummer", device = "Gerät", contactIndex = "Kontaktindex", addContact = "Neuer Kontakt", newContact = "Neu", editContact = "Kontakt bearbeiten", contactName = "Name", firstName = "Vorname", lastName = "Nachname", companyOrGroup = "Unternehmen oder Gruppe", phoneNumber = "Telefonnummer", officialContact = "Offizieller Ansprechpartner für das Unternehmen", - choosePhoto = "Kontakt Foto auswählen", chooseGallery = "Fotos", takePhoto = "Kamera", removePhoto = "Foto entfernen", - message = "Nachricht", video = "Video", mail = "Mail", defaultLabel = "Vorgabe", privateLabel = "Privat", + choosePhoto = "Kontaktfoto auswählen", chooseGallery = "Fotos", takePhoto = "Kamera", removePhoto = "Foto entfernen", + message = "Nachricht", video = "Video", mail = "Mail", defaultLabel = "Standard", privateLabel = "Privat", contactCard = "Kontaktkarte", removeContact = "Kontakt entfernen", returnToCall = "Zurück zum Anruf", mobile = "Mobil", messagesProfile = "Nachrichten", notes = "Notizen", sendMessage = "Nachricht senden", shareContact = "Kontakt teilen", addFavorite = "Zu Favoriten hinzufügen", removeFavorite = "Aus Favoriten entfernen", addEmergency = "Zu Notfallkontakten hinzufügen", blockContact = "Kontakt blockieren", - block = "Blockieren", blockCaller = "Anrufer blockieren", blockCallerTitle = "Sperrt den Anrufer ab?", - blockCallerBody = "{number} kann diese SIM nicht mehr aufrufen.", - faceTime = "Facetime", hideKeypad = "Tastatur ausblenden", more = "Mehr", mute = "Aus", speaker = "Sprecher", viewContact = "Kontakt anzeigen", + block = "Blockieren", blockCaller = "Anrufer blockieren", blockCallerTitle = "Anrufer blockieren?", + blockCallerBody = "{number} kann diese SIM nicht mehr anrufen.", + faceTime = "FaceTime", hideKeypad = "Tastatur ausblenden", more = "Mehr", mute = "Stumm", speaker = "Lautsprecher", viewContact = "Kontakt anzeigen", call = "Anrufen", callFailed = "Der Anruf konnte nicht gestartet werden.", calling = "Wird angerufen …", incoming = "Eingehender Anruf", incomingDirection = "Eingehend", outgoingDirection = "Ausgehend", connected = "Verbunden", - missed = "Verpasst", declined = "Abgelehnt", busy = "Beschäftigt", unavailable = "Nicht verfügbar", - noAnswer = "Keine Antwort", cancelled = "Gestorben", disconnected = "Getrennt", sim_removed = "SIM entfernt", completed = "Anruf beendet", - answer = "Antwort", decline = "Zurückziehen", hangup = "Ende", addToContacts = "Zu Kontakten hinzufügen", + missed = "Verpasst", declined = "Abgelehnt", busy = "Besetzt", unavailable = "Nicht verfügbar", + noAnswer = "Keine Antwort", cancelled = "Abgebrochen", disconnected = "Getrennt", sim_removed = "SIM entfernt", completed = "Anruf beendet", + answer = "Annehmen", decline = "Ablehnen", hangup = "Auflegen", addToContacts = "Zu Kontakten hinzufügen", deleteContact = "Kontakt löschen", replaceTitle = "SIM ersetzen?", replaceBody = "Die aktuelle SIM wird in dein Inventar zurückgeliefert.", choosePhone = "Wähle ein Handy", choosePhoneBody = "Wähle das Handy, das {number} empfangen soll.", emptyPhone = "Keine SIM eingefügt", errors = { invalid_contact = "Gib einen Namen und eine gültige Telefonnummer ein.", invalid_number = "Gib eine gültige Telefonnummer ein.", invalid_sim = "Die aktive SIM Karte ist nicht verfügbar.", - no_sim = "Dieses Handy hat keine SIM-Karte.", airplane_mode = "Airplane Mode ausschalten, um Anrufe zu tätigen.", + no_sim = "Dieses Handy hat keine SIM-Karte.", airplane_mode = "Schalte den Flugmodus aus, um Anrufe zu tätigen.", self_call = "Du kannst deine eigene Nummer nicht anrufen.", busy = "Die Leitung ist besetzt.", company_unavailable = "Diese Servicelinie ist nicht verfügbar.", readonly_contact = "Offizielle Kontakte zu Unternehmen können nicht verändert werden.", message_unavailable = "Das Gespräch konnte nicht eröffnet werden.", contact_remove_failed = "Der Kontakt konnte nicht entfernt werden.", contact_favorite_failed = "Der Favorit konnte nicht aktualisiert werden.", - blocked = "Diese Nummer ist blockiert.", recipient_not_found = "Diese Zahl ist nicht bekannt.", + blocked = "Diese Nummer ist blockiert.", recipient_not_found = "Diese Nummer ist nicht bekannt.", rate_limited = "Zu viele Anrufe. Versuch es in einer Minute erneut.", voice_unavailable = "Der konfigurierte Sprachdienst ist nicht verfügbar.", call_not_connected = "Schließ den Anruf an, bevor du den Lautsprechermodus ermöglichst.", speaker_unavailable = "Der Lautsprechermodus ist für den konfigurierten Handy-Sprachdienst nicht verfügbar.", speaker_unsupported = "Der konfigurierte Handy-Sprachdienst unterstützt den Lautsprechermodus nicht.", - inventory_full = "Es gibt keinen Platz für die ausgeschüttete SIM-Karte.", request_failed = "Die Telefonanfrage ist fehlgeschlagen.", + inventory_full = "Es gibt keinen Platz für die ausgeworfene SIM-Karte.", request_failed = "Die Telefonanfrage ist fehlgeschlagen.", operation_in_progress = "Eine andere Handy-Aktion wird bereits ausgeführt.", sim_request_expired = "Die SIM-Auswahl ist abgelaufen. Verwende die SIM-Karte erneut.", sim_not_owned = "Diese SIM Karte ist nicht mehr in deinem Inventar.", phone_not_owned = "Das Handy ist nicht mehr in deinem Inventar.", metadata_unsupported = "Das konfigurierte Inventar kann keine einzigartigen SIM Metadaten speichern.", @@ -1018,21 +1018,21 @@ Locales["de"] = { }, }, radio = { - name = "Funk", disconnected = "Nicht verbunden", connectedTo = "Verbunden - {frequency} MHz", + name = "Funk", disconnected = "Nicht verbunden", connectedTo = "Verbunden · {frequency} MHz", noProvider = "Sprachdienst nicht verfügbar", channel = "Kanal", primaryFrequency = "Primärfrequenz", secondaryFrequency = "Sekundärfrequenz", frequencyPlaceholder = "z.B. 120,5", optional = "Optional", mhz = "MHz", volume = "Lautstärke", connect = "Verbinden", disconnect = "Verbindung trennen", - speaker = "Sprecher", speakerDescription = "Radioverkehr durch Lautsprechermodus abspielen", + speaker = "Lautsprecher", speakerDescription = "Funkverkehr über den Lautsprecher wiedergeben", providerFeatureUnavailable = "Nicht unterstützt von diesem Sprachdienst", notSupported = "Nicht unterstützt", - members = "Derzeit angeschlossen ({count})", noMembers = "Keine Teilnehmer", history = "Kürzlich verbundene", - noHistory = "Keine Geschichte", badge = "Dienstnummer", badgePlaceholder = "z.B. 231", - profileSaved = "Radioprofil gespeichert", otherSettings = "Sonstige", autoRejoin = "Automatische Wiedereinstieg", + members = "Aktuell verbunden ({count})", noMembers = "Keine Teilnehmer", history = "Kürzlich verbunden", + noHistory = "Kein Verlauf", badge = "Dienstnummer", badgePlaceholder = "z. B. 231", + profileSaved = "Funkprofil gespeichert", otherSettings = "Sonstiges", autoRejoin = "Automatisch erneut verbinden", displayName = "Name der Funkanzeige", displayNamePlaceholder = "Zeichenname", displayNameDescription = "Lass leer, um deinen Charakternamen zu benutzen. Dieser Name wird anderen Teilnehmern und im eingebauten Radio-Overlay angezeigt.", displayNameNotAllowed = "Dein aktueller Job oder deine Klasse ist nicht erlaubt, den Radioanzeigenamen zu ändern.", - autoRejoinDescription = "Nach dem Laichen oder Neustart der Telefonressource wieder verbinden", - radioNotifications = "Funkmeldungen", notificationsDescription = "Zeigt die Verbindung und die Blätter auf dem aktuellen Kanal an", - memberJoined = "{name} schloss sich dem Radio an", memberLeft = "{name} hat das Radio verlassen", unknownMember = "Unbekannt", + autoRejoinDescription = "Nach dem Spawnen oder einem Neustart der Telefonressource erneut verbinden", + radioNotifications = "Funkmeldungen", notificationsDescription = "Zeigt Beitritte und Austritte im aktuellen Kanal an", + memberJoined = "{name} ist dem Funkkanal beigetreten", memberLeft = "{name} hat den Funkkanal verlassen", unknownMember = "Unbekannt", tabs = { radio = "Funk", settings = "Einstellungen" }, errors = { invalid_frequency = "Gib eine gültige Frequenz ein.", invalid_volume = "Gib eine gültige Lautstärke ein.", @@ -1081,7 +1081,7 @@ Locales["de"] = { recipientPhone = "Telefonnummer", recipientPhonePlaceholder = "Gib die Nummer des Empfängers ein.", chooseContact = "Wähle aus Kontakten", noContacts = "Noch keine Kontakte gespeichert.", amount = "Betrag", amountPlaceholder = "Betrag eingeben", - transactionDetails = "Einzelheiten der Transaktion", transactionDate = "Zeitpunkt", + transactionDetails = "Transaktionsdetails", transactionDate = "Datum", transactionDirection = "Richtung", transactionReference = "Sachgebietsnummer", notifications = { receivedTitle = "Erhaltenes Geld", @@ -1110,7 +1110,7 @@ Locales["de"] = { summary = { open = "Rechnungen öffnen", due = "Fälligkeit", overdue = "Überfällig" }, overview = { eyebrow = "Zahlungsstelle", urgent = "Es ist wichtig, dass wir uns um die Zukunft kümmern.", viewAll = "Alle Rechnungen anzeigen" }, filters = { scope = "Rechnungsquelle", status = "Status", all = "Alle", open = "Öffnen", overdue = "Überfällig", paid = "Bezahlt" }, - status = { open = "Öffnen", overdue = "Überfällig", processing = "Verarbeitung", paid = "Bezahlt", disputed = "Streit", cancelled = "Gestorben", refunded = "Refinanziert" }, + status = { open = "Offen", overdue = "Überfällig", processing = "In Bearbeitung", paid = "Bezahlt", disputed = "Beanstandet", cancelled = "Abgebrochen", refunded = "Erstattet" }, detail = { title = "Einzelheiten zur Rechnungsstellung", noDueDate = "Kein Fälligkeitsdatum", total = "Gesamt", reason = "Begründung", issued = "Ausgestellt", due = "Fälligkeit", paidAt = "Bezahlt", paidOn = "Zahlung auf {date}", @@ -1154,7 +1154,7 @@ Locales["de"] = { valet = { title = "Himmelswert", eyebrow = "Prämienzustellung", body = "Ein professioneller Fahrer bringt dieses Fahrzeug direkt an deinen aktuellen Standort.", - deliver = "Lieferfahrzeug", liveEyebrow = "Live-Lieferung", tracked = "Lebend verfolgt", + deliver = "Fahrzeug liefern", liveEyebrow = "Live-Lieferung", tracked = "Live verfolgt", onDemand = "Auf Anfrage", cancel = "Abbrechen", confirmTitle = "Valet anfordern?", confirmBody = "{vehicle} wirst dir für {price} geliefert.", confirm = "Valet anfordern", ordering = "Bitte...", account = "Die Gebühr wird auf dein {account} Konto berechnet.", @@ -1236,7 +1236,7 @@ Locales["de"] = { safetyNote = "Die Reisedetails werden nur während dieser Fahrt geteilt.", waypointSet = "Die Route ist in dein GPS.", status = { searching = "Einen Fahrer finden", accepted = "Fahrer gefunden", driver_arriving = "Fahrer unterwegs", - arrived = "Fahrer angekommen", in_progress = "Auf dem Weg", completed = "Abgeschlossen", cancelled = "Gestorben", + arrived = "Fahrer angekommen", in_progress = "Auf dem Weg", completed = "Abgeschlossen", cancelled = "Abgebrochen", }, statusBody = { searching = "Wir passen zu dir mit einem Fahrer in der Nähe.", accepted = "Dein Fahrer will dich abholen.", @@ -1255,7 +1255,7 @@ Locales["de"] = { account = "Account", paymentMethod = "Zahlungsmethode", paymentMethods = { bank = "Bank", cash = "Bargeld", default = "Zahlungsmethode" }, safety = "Sicherheitszentrum", safetyBody = "Hilfe, Reisefreigabe und Notfallinformationen", driverMode = "Treibermodus", driverModeBody = "Nimm Fahrwünsche an und verfolge dein Einkommen", completedRides = "Abgeschlossen", - cancelledRides = "Gestorben", acceptance = "Annahme", rating = "Bewertung", notAvailable = "N/A", + cancelledRides = "Abgebrochen", acceptance = "Annahme", rating = "Bewertung", notAvailable = "N/V", profile = "Dein Profil.", editProfile = "Profil bearbeiten", editProfileBody = "Ändere deinen öffentlichen Namen und dein Profilfoto", profilePhoto = "Profilfoto", gallery = "Fotos", camera = "Kamera", profileDetails = "Profilangaben", profileName = "Name", profileNamePlaceholder = "Dein Name ist SkyRide.", removeProfilePhoto = "Profilfoto entfernen", @@ -1321,103 +1321,103 @@ Locales["de"] = { }, calculator = { name = "Rechner", history = "Berechnungsverlauf", changeMode = "Rechnermodus ändern", - scientificFunctions = "Wissenschaftliche Aufgaben", edit = "Bearbeiten", done = "Fertig", closeHistory = "Geschichte schließen", - today = "Heute", yesterday = "Gestern", earlier = "Früher", noHistory = "Noch keine Berechnungen", clearHistory = "Geschichte löschen", + scientificFunctions = "Wissenschaftliche Funktionen", edit = "Bearbeiten", done = "Fertig", closeHistory = "Verlauf schließen", + today = "Heute", yesterday = "Gestern", earlier = "Früher", noHistory = "Noch keine Berechnungen", clearHistory = "Verlauf löschen", }, snake = { - name = "Schlange", backToMenu = "Zurück zum Spielmenü", board = "Snake Spielbrett", + name = "Snake", backToMenu = "Zurück zum Spielmenü", board = "Snake Spielbrett", controls = "Richtungssteuerung", directions = { down = "Nach unten", left = "Nach links bewegen", right = "Nach rechts bewegen", up = "Nach oben" }, - gameOver = "Spiel vorbei", highScore = "Hohe Punktzahl", menu = "Hauptmenu", pause = "Pause Spiel", paused = "Anhalten", - readyBody = "Sammle Früchte, ertrage länger und bleibe von jeder Wand frei.", readyTitle = "Bereit zum Spielen?", + gameOver = "Spiel vorbei", highScore = "Highscore", menu = "Hauptmenü", pause = "Spiel pausieren", paused = "Pausiert", + readyBody = "Sammle Früchte, werde immer länger und meide Wände.", readyTitle = "Bereit zum Spielen?", restart = "Wieder spielen", resume = "Spiel fortsetzen", score = "Ergebnis", speed = "Geschwindigkeit", speeds = { fast = "Schnell", normal = "Normal", relaxed = "Entspannt" }, start = "Spiel starten", - swipeHint = "Benutzt die Pfeiltasten, um zu steuern", + swipeHint = "Wische oder verwende die Pfeiltasten zum Steuern", }, memory = { - name = "Speicher", backToMenu = "Zurück zur Auswahl des Boards", board = "Speicherkarten-Board", + name = "Memory", backToMenu = "Zurück zur Spielfeldauswahl", board = "Memory-Spielfeld", chooseBody = "Finde jedes passende Paar mit so wenigen Zügen wie möglich.", - chooseTitle = "Wähle ein Brett", completeEyebrow = "Ausschuss genehmigt", completeTitle = "Tolle Erinnerung!", - difficulties = { large = "Sachverständiger", medium = "Klassische", small = "Schnell" }, eyebrow = "Passendes Spiel", - hiddenCard = "Versteckte Karte", menu = "Auswahl der Ausschüsse", moves = "Bewegungen", mute = "Laute Spielgeräusche", + chooseTitle = "Wähle ein Spielfeld", completeEyebrow = "Spielfeld geschafft", completeTitle = "Tolles Gedächtnis!", + difficulties = { large = "Experte", medium = "Klassisch", small = "Schnell" }, eyebrow = "Paare finden", + hiddenCard = "Verdeckte Karte", menu = "Spielfeldauswahl", moves = "Züge", mute = "Spielgeräusche stummschalten", noBest = "Noch keine beste Punktzahl", playAgain = "Wieder spielen", restart = "Neustart", revealedCard = "Aufgedeckte Karte", time = "Zeit", unmute = "Spielgeräusche einschalten", }, numberMerge = { - name = "2048", backToMenu = "Zurück zum Spielmenü", best = "Am besten", board = "2048 Spielbrett", + name = "2048", backToMenu = "Zurück zum Spielmenü", best = "Bestwert", board = "2048-Spielfeld", cancel = "Aktuelles Spiel beibehalten", confirmBody = "Dein aktuelles Spielfeld wird ersetzt. Das kann nicht rückgängig gemacht werden.", confirmNew = "Neues Spiel starten", confirmTitle = "Aktuelles Spiel ersetzen?", continueGame = "Spiel fortsetzen", controls = "Steuerungen verschieben", directions = { - down = "Fliesen nach unten verschieben", left = "Fliesen nach links verschieben", right = "Fliesen nach rechts verschieben", up = "Fliesen nach oben verschieben", + down = "Kacheln nach unten verschieben", left = "Kacheln nach links verschieben", right = "Kacheln nach rechts verschieben", up = "Kacheln nach oben verschieben", }, - eyebrow = "Anzahl Puzzle", gameOverBody = "Es bleiben keine leeren Räume oder passenden Nachbarn übrig.", - gameOverTitle = "Keine Bewegungen mehr", highest = "Höchste Kachel", - howToBody = "Swipe, um passende Fliesen zu kombinieren. Jedes Paar wird eine größere Fliesen.", + eyebrow = "Zahlenrätsel", gameOverBody = "Es gibt keine freien Felder oder passenden Nachbarn mehr.", + gameOverTitle = "Keine Züge mehr", highest = "Höchste Kachel", + howToBody = "Wische, um gleiche Kacheln zu verbinden. Jedes Paar wird zu einer größeren Kachel.", howToExample = "2 + 2 = 4 · 4 + 4 = 8 · … · 1024 + 1024 = 2048", - howToGoal = "Tor: Erstelle die 2048er-Fliese, bevor das Board keine Züge mehr hat.", howToTitle = "Wie zu spielen", - keepPlaying = "Spiel weiter", mainMenu = "Hauptmenu", - menuBody = "Schiebe gleiche Zahlen zusammen und arbeite dich bis 2048 vor.", menuTitle = "Bau die 2048 Fliesen", - mute = "Laute Spielgeräusche", newGame = "Neues Spiel", score = "Ergebnis", + howToGoal = "Ziel: Erstelle die 2048-Kachel, bevor keine Züge mehr möglich sind.", howToTitle = "Spielanleitung", + keepPlaying = "Weiterspielen", mainMenu = "Hauptmenü", + menuBody = "Schiebe gleiche Zahlen zusammen und arbeite dich bis 2048 vor.", menuTitle = "Baue die 2048-Kachel", + mute = "Spielgeräusche stummschalten", newGame = "Neues Spiel", score = "Punktzahl", swipeHint = "Das Brett wischen oder Pfeiltasten / WASD verwenden", unmute = "Spielgeräusche einschalten", - wonBody = "Du hast den legendären Stein erreicht. Spiele weiter oder beginne von vorn.", wonTitle = "Du hast 2048 gemacht!", + wonBody = "Du hast die legendäre Kachel erreicht. Spiele weiter oder beginne von vorn.", wonTitle = "Du hast 2048 erreicht!", }, minesweeper = { - name = "Minenräumer", backToMenu = "Zurück zum Spielmenü", board = "Minenräumer", + name = "Minesweeper", backToMenu = "Zurück zum Spielmenü", board = "Minenräumer", chooseBody = "Decke alle sicheren Felder auf. Die Zahlen zeigen, wie viele Minen das jeweilige Feld berühren.", - chooseTitle = "Wähle ein Minenfeld", difficulties = { classic = "Klassische", expert = "Sachverständiger", quick = "Schnell" }, - emptyCell = "Freistehendes Feld", eyebrow = "Meines Puzzles", flaggedCell = "Gekennzeichnetes Feld", - gameHint = "Tippe, um zu zeigen · Halten oder mit der rechten Maustaste auf Flagge", hiddenCell = "Verstecktes Feld", - longPressHint = "Tippe, um zu zeigen · Halte dich an die Flagge", + chooseTitle = "Wähle ein Minenfeld", difficulties = { classic = "Klassisch", expert = "Experte", quick = "Schnell" }, + emptyCell = "Freies Feld", eyebrow = "Minenrätsel", flaggedCell = "Markiertes Feld", + gameHint = "Tippen zum Aufdecken · Halten oder Rechtsklick zum Markieren", hiddenCell = "Verdecktes Feld", + longPressHint = "Tippen zum Aufdecken · Halten zum Markieren", lostBody = "Dort war eine Mine versteckt. Markiere verdächtige Felder und versuch es erneut.", lostTitle = "Ausgelöste Mine", - mainMenu = "Hauptmenu", mineCell = "Meinen enthüllt", mines = "Minen", mute = "Laute Spielgeräusche", + mainMenu = "Hauptmenü", mineCell = "Aufgedeckte Mine", mines = "Minen", mute = "Spielgeräusche stummschalten", noBest = "Noch keine beste Zeit", numberCell = "Aufgedecktes Feld mit benachbarten Minen {count}", playAgain = "Wieder spielen", restart = "Neustart", resume = "Spiel fortsetzen", time = "Zeit", unmute = "Spielgeräusche einschalten", wonBody = "Minenfeld in {time} freigegeben.", wonTitle = "Feld frei!", }, towerStack = { - name = "Turmstapel", backToMenu = "Zurück zum Spielmenü", bestHeight = "Beste Höhe", - bestScore = "Beste Punktzahl", blocks = "Blöcke", eyebrow = "Zeitliche Herausforderung", finalScore = "Endergebnis", + name = "Tower Stack", backToMenu = "Zurück zum Spielmenü", bestHeight = "Beste Höhe", + bestScore = "Beste Punktzahl", blocks = "Blöcke", eyebrow = "Timing-Herausforderung", finalScore = "Endergebnis", gameHint = "Tipp auf das Spielfeld, um den beweglichen Block zu platzieren", gameOver = "Turm stürzte ein", - height = "Höhe", mainMenu = "Hauptmenu", - menuBody = "Haltet jeden beweglichen Block über dem Turm. Nur der überlappende Teil bleibt übrig.", - menuTitle = "Wie hoch kann man bauen?", mute = "Laute Spielgeräusche", newGame = "Neues Spiel starten", - pause = "Pause oder Wiederaufnahme Spiel", paused = "Anhalten", perfect = "PERFEKT!", + height = "Höhe", mainMenu = "Hauptmenü", + menuBody = "Halte jeden beweglichen Block über dem Turm. Nur der überlappende Teil bleibt übrig.", + menuTitle = "Wie hoch kannst du bauen?", mute = "Spielgeräusche stummschalten", newGame = "Neues Spiel starten", + pause = "Spiel pausieren oder fortsetzen", paused = "Pausiert", perfect = "PERFEKT!", placeBlock = "Bewegter Block platzieren", playAgain = "Wieder spielen", ready = "Bereit zum Stapeln?", resume = "Spiel fortsetzen", score = "Ergebnis", start = "Spiel starten", - tapHint = "Jeder Hahn platziert einen Block · perfekte Treffer sammeln Bonuspunkte", + tapHint = "Jedes Tippen platziert einen Block · perfekte Treffer bringen Bonuspunkte", unmute = "Spielgeräusche einschalten", }, skyFlappy = { - name = "Himmel Flappy", backToMenu = "Zurück zum Spielmenü", best = "Am besten", design = "Entwurf des Himmels", - designs = { dawn = "Morgendämmerung", neon = "Neon", storm = "Sturm" }, eyebrow = "Herausforderung für den Himmel", + name = "Sky Flappy", backToMenu = "Zurück zum Spielmenü", best = "Bestwert", design = "Himmelsdesign", + designs = { dawn = "Morgendämmerung", neon = "Neon", storm = "Sturm" }, eyebrow = "Himmelsherausforderung", firstTap = "Tipp auf den Start", flap = "Fliege nach oben", gameHint = "Tippe irgendwo am Himmel, um nach oben zu fliegen", - gameOver = "Flug beendet", highScore = "Hohe Punktzahl", mainMenu = "Hauptmenu", + gameOver = "Flug beendet", highScore = "Highscore", mainMenu = "Hauptmenü", menuBody = "Leite den Himmelsvogel durch jeden Turm, der sich öffnet, ohne die Kanten zu berühren.", - menuTitle = "Flieg zwischen die Türme", mute = "Laute Spielgeräusche", pause = "Pause oder Wiederaufnahme Spiel", - paused = "Anhalten", playAgain = "Noch einmal fliegen", points = "Punkte", ready = "Bereit für den Start?", + menuTitle = "Flieg zwischen den Türmen", mute = "Spielgeräusche stummschalten", pause = "Spiel pausieren oder fortsetzen", + paused = "Pausiert", playAgain = "Noch einmal fliegen", points = "Punkte", ready = "Bereit für den Start?", resume = "Weiterflug", score = "Ergebnis", start = "Flug starten", - tapHint = "Tap to flap · Schwerkraft zieht den Vogel nach unten", unmute = "Spielgeräusche einschalten", + tapHint = "Tippen zum Fliegen · die Schwerkraft zieht den Vogel nach unten", unmute = "Spielgeräusche einschalten", }, neonDrop = { name = "Neon Drop", backToMenu = "Zurück zum Spielmenü", bestLines = "Beste Linien", - bestScore = "Beste Punktzahl", board = "Neon Drop Reaktor Board", controls = "Blocksteuerung", - eyebrow = "Energieblock-puzzle", gameHint = "Den Reaktor schwenken oder die fünf Steuerungen benutzen", + bestScore = "Beste Punktzahl", board = "Neon-Drop-Reaktorfeld", controls = "Blocksteuerung", + eyebrow = "Energieblock-Rätsel", gameHint = "Wische über den Reaktor oder verwende die fünf Steuerelemente", gameOver = "Überlasteter Reaktor", hardDrop = "Block sofort absetzen", howToBody = "Fülle die horizontalen Reihen, um sie aufzulösen und den Reaktor sauber zu halten.", - howToTitle = "Aufladen des Reaktors", left = "Block nach links verschieben", level = "Ebene", lines = "Linien", - mainMenu = "Hauptmenu", menuBody = "Rotierst und platzierst Energieformen. Jede geleerte Reihe erhöht deine Ladung.", - menuTitle = "Komplette Energielinien bauen", mute = "Laute Spielgeräusche", newGame = "Neues Spiel starten", - next = "Weiter", pause = "Pause oder Wiederaufnahme Spiel", paused = "Anhalten", playAgain = "Wieder spielen", + howToTitle = "Reaktor aufladen", left = "Block nach links verschieben", level = "Level", lines = "Reihen", + mainMenu = "Hauptmenü", menuBody = "Drehe und platziere Energieformen. Jede geleerte Reihe erhöht deine Ladung.", + menuTitle = "Vervollständige Energiereihen", mute = "Spielgeräusche stummschalten", newGame = "Neues Spiel starten", + next = "Nächstes", pause = "Spiel pausieren oder fortsetzen", paused = "Pausiert", playAgain = "Wieder spielen", points = "Punkte", ready = "Reaktor bereit", resume = "Spiel fortsetzen", right = "Block nach rechts verschieben", rotate = "Block drehen", score = "Ergebnis", softDrop = "Block nach unten verschieben", start = "Reaktor starten", unmute = "Spielgeräusche einschalten", }, camera = { - name = "Kamera", flash = "Blitz", flip = "Kamera abdrehen", landscape = "Zur Landschaft wechseln", + name = "Kamera", flash = "Blitz", flip = "Kamera wechseln", landscape = "Zum Querformat wechseln", portrait = "Zum Hochformat wechseln", photo = "Foto", video = "Video", microphoneOn = "Mikrofon eingeschaltet", microphoneOff = "Mikrofon stumm", - focusHelp = "Halte Platz, um sich umzusehen", returnHelp = "Freiraum für Steuerungen", lockCamera = "Kamerabewegung verriegeln", - unlockCamera = "Kamerabewegung entsperren", spaceKey = "Raum", uploading = "{count} Hochladen", + focusHelp = "Halte die Leertaste gedrückt, um dich umzusehen", returnHelp = "Leertaste für Steuerung", lockCamera = "Kamerabewegung sperren", + unlockCamera = "Kamerabewegung entsperren", spaceKey = "Leertaste", uploading = "Upload: {count}", saving = "Video speichern...", openGallery = "Fotos öffnen", takePhoto = "Foto machen", startRecording = "Aufnahme starten", stopRecording = "Aufnahme beenden", saved = "Auf Fotos gespeichert.", zoom = "Kamerazoom auf {zoom} einstellen", @@ -1425,20 +1425,20 @@ Locales["de"] = { cancelled = "Die Aufnahme ist abgesagt.", capture_failed = "Die Spielansicht kann nicht erfasst werden.", invalid_media_type = "Der hochgeladene Medientyp ist ungültig.", invalid_upload = "Der Upload konnte nicht überprüft werden.", invalid_upload_token = "Die Upload-Sitzung ist nicht mehr gültig.", missing_config = "Kamera-Uploads sind nicht konfiguriert.", - microphone_unavailable = "Mikrofonzugriff zulassen oder vor der Aufnahme stumm stummen.", + microphone_unavailable = "Erlaube den Mikrofonzugriff oder schalte das Mikrofon vor der Aufnahme stumm.", not_found = "Das Medienobjekt existiert nicht mehr.", owner_changed = "Das aktive Telefonkonto hat sich beim Upload geändert.", operation_in_progress = "Eine weitere Medienoperation ist bereits im Gange.", rate_limited = "Zu viele Medienaktionen. Versuch es gleich erneut.", request_failed = "Die Kameraanfrage ist fehlgeschlagen.", - request_timeout = "Der Mediendienst hat sich verhört.", unsupported = "Videoaufzeichnung wird nicht unterstützt.", - upload_failed = "Der Medien-Upload ist fehlgeschlagen.", upload_timeout = "Die Medien laden Zeit aus.", + request_timeout = "Der Mediendienst hat nicht rechtzeitig geantwortet.", unsupported = "Videoaufzeichnung wird nicht unterstützt.", + upload_failed = "Der Medien-Upload ist fehlgeschlagen.", upload_timeout = "Der Medien-Upload hat zu lange gedauert.", }, }, calendar = { name = "Kalender", today = "Heute", calendars = "Kalender", calendar = "Kalender", calendarName = "Sky Cloud", - searchPlaceholder = "Ereignisse suchen", previousMonth = "Vormonat", nextMonth = "Nächsten Monat", - eyebrow = "Deine Zeit ist klar geplant.", schedule = "Fälligkeitsplan", event = "Ereignis", appointment = "Ernennung", - newEvent = "Neue Veranstaltung", editEvent = "Ereignis bearbeiten", deleteEvent = "Ereignis löschen", details = "Einzelheiten der Veranstaltung", - title = "Titel", titlePlaceholder = "Was ist geplant?", date = "Zeitpunkt", allDay = "Ganztägig", starts = "Beginn", ends = "Ende", + searchPlaceholder = "Ereignisse suchen", previousMonth = "Vormonat", nextMonth = "Nächster Monat", + eyebrow = "Deine Zeit ist klar geplant.", schedule = "Zeitplan", event = "Ereignis", appointment = "Termin", + newEvent = "Neues Ereignis", editEvent = "Ereignis bearbeiten", deleteEvent = "Ereignis löschen", details = "Ereignisdetails", + title = "Titel", titlePlaceholder = "Was ist geplant?", date = "Datum", allDay = "Ganztägig", starts = "Beginn", ends = "Ende", reminder = "Erinnerung", reminderNotification = "Kommend: {title}", note = "Anmerkung", notePlaceholder = "Füge Details, eine Adresse oder so was hinzu...", noEvents = "Nichts geplant", noEventsBody = "Dieser Tag ist noch frei. Füge ein Ereignis hinzu, sobald du bereit bist.", @@ -1459,11 +1459,11 @@ Locales["de"] = { }, }, clock = { - name = "Uhr", lap = "Lappen", minutes = "Protokoll", add = "Alarm hinzufügen", location = "Los Santos", + name = "Uhr", lap = "Runde", minutes = "Minuten", add = "Wecker hinzufügen", location = "Los Santos", tabs = { world = "Uhr", alarm = "Wecker", stopwatch = "Stoppuhr", timer = "Timer" }, alarm = { - add = "Alarm hinzufügen", edit = "Erinnerung bearbeiten", ringing = "Wecker", time = "Zeit", hours = "Stunden", ["repeat"] = "Wiederholen", note = "Anmerkung", - notePlaceholder = "Wecker", sound = "Geräusch", delete = "Alarm löschen", never = "Niemals", + add = "Wecker hinzufügen", edit = "Wecker bearbeiten", ringing = "Wecker", time = "Zeit", hours = "Stunden", ["repeat"] = "Wiederholen", note = "Notiz", + notePlaceholder = "Wecker", sound = "Ton", delete = "Wecker löschen", never = "Niemals", everyDay = "Jeden Tag", weekdays = "Wochentage", weekends = "Wochenenden", days = { sunday = "Sonntag", monday = "Montag", tuesday = "Dienstag", wednesday = "Mittwoch", thursday = "Donnerstag", friday = "Freitag", saturday = "Samstag" }, daysShort = { sunday = "So", monday = "Mo", tuesday = "Di", wednesday = "Mi", thursday = "Do", friday = "Fr", saturday = "Sa" }, @@ -1473,9 +1473,9 @@ Locales["de"] = { }, }, timer = { - time = "Dauer der Zeit", hours = "Stunden", hoursShort = "Std.", minutes = "Protokoll", minutesShort = "min", - seconds = "Sekunden", secondsShort = "Abschnitt", - description = "Beschreibung", note = "Anmerkung", notePlaceholder = "Timer", sound = "Geräusch", ringing = "Timer", + time = "Dauer", hours = "Stunden", hoursShort = "Std.", minutes = "Minuten", minutesShort = "Min.", + seconds = "Sekunden", secondsShort = "Sek.", + description = "Beschreibung", note = "Notiz", notePlaceholder = "Timer", sound = "Ton", ringing = "Timer", }, }, mail = { @@ -1485,30 +1485,30 @@ Locales["de"] = { accountEyebrow = "Dein Sky Cloud Konto", emailPlaceholder = "Name@ifruit.com", passwordPlaceholder = "Passwort eingeben", passwordWarning = "Verwende ein In-Character-Passwort. Nutze kein echtes Passwort erneut.", mailboxes = "Postfächer", editMailboxes = "Bearbeiten", newMailbox = "Neue Mailbox", - mailboxName = "Name", mailboxNamePlaceholder = "Name der Postfach", mailboxLocation = "Postfach-Standort", + mailboxName = "Name", mailboxNamePlaceholder = "Name des Postfachs", mailboxLocation = "Postfach", deleteMailbox = "{mailbox} löschen", deleteMailboxTitle = "Mailbox löschen?", deleteMailboxBody = "Nachrichten in {mailbox} werden in ihr Original-Postfach zurückkehren.", moveToMailbox = "In Mailbox verschieben", movedToMailbox = "Nach {mailbox} übertragen.", inbox = "Posteingang", sent = "Gesendet", drafts = "Entwürfe", trash = "Papierkorb", compose = "Neue Nachricht", recipients = "An", recipientHint = "Trenne bis zu 10 Adressen mit Kommas.", - recipientPlaceholder = "Name@ifruit.com", subject = "Gegenstand", subjectPlaceholder = "Worum geht es hier?", - body = "Nachricht", messagePlaceholder = "Schreib eine Nachricht...", search = "Suchmail", messages = "Nachrichten", + recipientPlaceholder = "Name@ifruit.com", subject = "Betreff", subjectPlaceholder = "Worum geht es?", + body = "Nachricht", messagePlaceholder = "Schreib eine Nachricht...", search = "Mails durchsuchen", messages = "Nachrichten", toolbar = "Mail-Werkzeuge", filterTitle = "Filtern", filteredBy = "Gefiltert nach", filterActiveLabel = "Gefiltert durch {filter}", - filterStatus = "Status", filterAddressed = "Anschrift", filterDirection = "Richtung", filterOther = "Sonstige", - filterUnreadLabel = "Ungelesen", filterReadLabel = "Lesen", filterReceived = "Erhalten", + filterStatus = "Status", filterAddressed = "Adressiert", filterDirection = "Richtung", filterOther = "Sonstiges", + filterUnreadLabel = "Ungelesen", filterReadLabel = "Gelesen", filterReceived = "Empfangen", filterSent = "Gesendet", filterToMe = "Zu mir", filterFromMe = "Von mir", filterToday = "Heute", filterMultiple = "{count} Filter", select = "Auswählen", selectedCount = "{count} ausgewählt", deleteSelected = "Ausgewählte löschen", selectionLimit = "Du kannst bis zu {count} Nachrichten auf einmal auswählen.", - noMessageContent = "Kein Nachrichteninhalt", noMail = "Keine Post", - noMailBody = "Hier werden Nachrichten in diesem Briefkasten erscheinen.", noResults = "Keine Ergebnisse", - noResultsBody = "Versuch's mit einer anderen Suche.", untitled = "(kein Gegenstand)", loadMore = "Mehr laden", - logout = "Abmelden", from = "Von", to = "An", reply = "Antwort", replyAll = "Alle Antworten", forward = "Vorwärts", - formatBold = "Mutig", formatItalic = "kursiv", formatBulletList = "Liste der Kugeln", + noMessageContent = "Kein Nachrichteninhalt", noMail = "Keine Mails", + noMailBody = "Nachrichten in diesem Postfach erscheinen hier.", noResults = "Keine Ergebnisse", + noResultsBody = "Versuch es mit einer anderen Suche.", untitled = "(kein Betreff)", loadMore = "Mehr laden", + logout = "Abmelden", from = "Von", to = "An", reply = "Antworten", replyAll = "Allen antworten", forward = "Weiterleiten", + formatBold = "Fett", formatItalic = "Kursiv", formatBulletList = "Aufzählung", formatNumberedList = "Nummerierte Liste", formatQuote = "Zitat", undo = "Rückgängig", redo = "Wiederherstellen", - markRead = "Lesen", markUnread = "Als ungelesen markieren", delete = "Löschen", moveToTrash = "In den Mülleimer wechseln", restore = "Wiederherstellen", - deleteForever = "Für immer löschen", emptyTrash = "Leerer Müll", emptyTrashTitle = "Leerer Müll?", + markRead = "Als gelesen markieren", markUnread = "Als ungelesen markieren", delete = "Löschen", moveToTrash = "In den Papierkorb verschieben", restore = "Wiederherstellen", + deleteForever = "Dauerhaft löschen", emptyTrash = "Papierkorb leeren", emptyTrashTitle = "Papierkorb leeren?", emptyTrashBody = "Jede Nachricht im Papierkorb wird dauerhaft gelöscht.", deleteDraft = "Entwurf streichen", sentSuccess = "Nachricht gesendet.", newMessage = "Neue Mail von {sender}", passwordsMismatch = "Passwörter stimmen nicht überein.", errors = { @@ -1520,9 +1520,9 @@ Locales["de"] = { recipient_not_found = "Ein oder mehrere Empfänger existieren nicht.", invalid_draft = "Dieser Entwurf enthält ungültige Inhalte.", not_authenticated = "Deine Mail-Sitzung ist abgelaufen. Melde dich erneut an.", request_failed = "Mail ist vorübergehend nicht verfügbar.", invalid_request = "Die Mailanfrage war ungültig.", - invalid_mailbox = "Gib einen Briefkastennamen mit bis zu 50 Zeichen ein.", - mailbox_exists = "Ein Briefkasten mit diesem Namen existiert bereits.", - mailbox_limit = "Du hast das Briefkastenlimit erreicht.", + invalid_mailbox = "Gib einen Postfachnamen mit bis zu 50 Zeichen ein.", + mailbox_exists = "Ein Postfach mit diesem Namen existiert bereits.", + mailbox_limit = "Du hast das Postfachlimit erreicht.", mailbox_not_found = "Das Postfach ist nicht mehr verfügbar.", message_not_found = "Diese Nachricht ist nicht mehr verfügbar.", invalid_filter = "Dieser Mailfilter ist nicht verfügbar.", @@ -1534,11 +1534,11 @@ Locales["de"] = { tabs = { discover = "Entdecken", search = "Suchen", sell = "Verkauf", inbox = "Posteingang", profile = "Profil" }, categories = { vehicles = "Fahrzeuge", property = "Eigentum", electronics = "Elektronik", clothing = "Bekleidung", - tools = "Werkzeuge", leisure = "Freizeit", services = "Dienstleistungen", jobs = "Arbeitsplätze", wanted = "Gesucht", other = "Sonstige", + tools = "Werkzeuge", leisure = "Freizeit", services = "Dienstleistungen", jobs = "Jobs", wanted = "Gesucht", other = "Sonstiges", }, - conditions = { new = "Neu", very_good = "Sehr gut", used = "Verwendet", defective = "Mängel" }, - priceTypes = { fixed = "Festpreis", negotiable = "Verhandelbar", free = "Frei" }, - offerStatus = { pending = "Öffnen", accepted = "Angenommen", declined = "Abgelehnt", countered = "Verhandlungen" }, + conditions = { new = "Neu", very_good = "Sehr gut", used = "Gebraucht", defective = "Defekt" }, + priceTypes = { fixed = "Festpreis", negotiable = "Verhandelbar", free = "Kostenlos" }, + offerStatus = { pending = "Offen", accepted = "Angenommen", declined = "Abgelehnt", countered = "Gegenangebot" }, districts = { los_santos = "Los Santos", vinewood = "Weinholz", vespucci = "Vespucci", south_los_santos = "Süd Los Santos", sandy_shores = "Sandige Ufer", @@ -1551,7 +1551,7 @@ Locales["de"] = { freshOffers = "Frische Angebote", offers = "Angebote", compactView = "Kompakte Ansicht", largeView = "Große Ansicht", noListings = "Keine Angebote gefunden", noListingsBody = "Probiere eine andere Suche oder Kategorie aus.", noDistrict = "Kein Bezirk", free = "Frei", negotiablePrice = "${price} verhandelbar", money = "${price}", - hoursAgo = "{count}Vor", daysAgo = "{count}Vor", photos = "Fotos", activeListings = "aktive Auflistungen", + hoursAgo = "vor {count} Std.", daysAgo = "vor {count} Tagen", photos = "Fotos", activeListings = "aktive Inserate", signInTitle = "Bei Sky Cloud anmelden", signInBody = "Logge dich in dein CityMarkt Profil ein, um zu verkaufen, zu speichern oder zu nachrichten.", authEyebrow = "CityMarkt-Konto", authLoginTitle = "Willkommen zurück", authRegisterTitle = "Profil erstellen", authLoginBody = "Melde dich mit dem iFruit-Konto an, das mit diesem Telefon verknüpft ist.", @@ -1570,34 +1570,34 @@ Locales["de"] = { rate_limited = "Zu viele Versuche. Versuche es in einer Minute erneut.", default = "Die CityMarkt-Anmeldung ist fehlgeschlagen.", }, - noMessages = "Keine Gespräche", noMessagesBody = "Hier werden Nachrichten über Angebote erscheinen.", - myListings = "Meine Einträge", favorites = "Favoriten", addFavorite = "Zu Favoriten hinzufügen", removeFavorite = "Von Favoriten entfernen", noProfileListings = "Hier ist noch nichts.", - createProfile = "Erstell dein CityMarkt Profil", editProfile = "Profil bearbeiten", + noMessages = "Keine Unterhaltungen", noMessagesBody = "Nachrichten zu Inseraten erscheinen hier.", + myListings = "Meine Inserate", favorites = "Favoriten", addFavorite = "Zu Favoriten hinzufügen", removeFavorite = "Von Favoriten entfernen", noProfileListings = "Hier ist noch nichts.", + createProfile = "Erstelle dein CityMarkt-Profil", editProfile = "Profil bearbeiten", profileIntro = "Deine iFruit E-Mail bleibt mit diesem Profil verknüpft.", profileEmail = "iFruit-E-Mail", - displayName = "Name der Anzeige", profileBio = "Über dich", saveProfile = "Profil speichern", - cancel = "Abbrechen", profileSaved = "Dein Profil wurde gerettet.", chooseGallery = "Fotos", + displayName = "Anzeigename", profileBio = "Über dich", saveProfile = "Profil speichern", + cancel = "Abbrechen", profileSaved = "Dein Profil wurde gespeichert.", chooseGallery = "Fotos", takePhoto = "Kamera", removeProfilePhoto = "Foto entfernen", phone = "Telefon", contactSeller = "Verkäufer kontaktieren", messagePlaceholder = "Hi, ist das noch verfügbar?", signInToMessage = "Melde dich bei deinem Sky Cloud-Konto an, um eine Nachricht zu senden.", - edit = "Bearbeiten", makeActive = "Aktiv werden", markSold = "Verkauft", remove = "Entfernen", - createListing = "Liste erstellen", editListing = "Liste bearbeiten", save = "Speichern", step = "Schritt {current} von {total}", next = "Weiter", previous = "Zurück", publish = "Veröffentlichen", + edit = "Bearbeiten", makeActive = "Aktivieren", markSold = "Als verkauft markieren", remove = "Entfernen", + createListing = "Inserat erstellen", editListing = "Inserat bearbeiten", save = "Speichern", step = "Schritt {current} von {total}", next = "Weiter", previous = "Zurück", publish = "Veröffentlichen", addPhotos = "Fotos hinzufügen", addPhotosBody = "Wähle bis zu sechs Fotos aus Fotos oder nimm neue auf. Fotos sind optional; das erste wird zum Titelbild.", chooseGalleryBody = "Verwende Fotos, die auf diesem Handy gespeichert sind.", takePhotos = "Fotos aufnehmen", takePhotosBody = "Nimm mehrere neue Fotos für dieses Angebot auf.", selectedPhotos = "Ausgewählte Fotos", gallery = "Fotos", camera = "Kamera", cameraHint = "Nimm so viele Fotos auf, wie du benötigst. Jede Aufnahme wird automatisch zum Angebot hinzugefügt.", noPhoto = "Kein Foto verfügbar", noPhotoBody = "Der Verkäufer hat diesem Angebot kein Foto hinzugefügt.", noPhotoOptional = "Du kannst ohne Foto veröffentlichen oder eines aus der Galerie beziehungsweise Kamera hinzufügen.", previousPhoto = "Vorheriges Foto", nextPhoto = "Nächstes Foto", photo = "Foto", removePhoto = "Foto {number} entfernen", photoLimit = "Du kannst bis zu sechs Fotos hinzufügen.", - describeOffer = "Beschreibe dein Angebot.", title = "Titel", description = "Beschreibung", category = "Kategorie", condition = "Bedingung", + describeOffer = "Beschreibe dein Angebot.", title = "Titel", description = "Beschreibung", category = "Kategorie", condition = "Zustand", characterCount = "{current} / {maximum} Zeichen", minimumCharacters = "mind. {minimum}", priceAndPlace = "Preis und Lage", priceType = "Preisart", price = "Preis", district = "Bezirk", - showPhone = "Meine aktuelle Telefonnummer anzeigen", preview = "Vorschau", published = "Deine Liste ist live.", - writeMessage = "Eine Nachricht schreiben", reserveForBuyer = "Reserve für diesen Käufer", statusChanged = "Liste aktualisiert.", + showPhone = "Meine aktuelle Telefonnummer anzeigen", preview = "Vorschau", published = "Dein Inserat ist live.", + writeMessage = "Eine Nachricht schreiben", reserveForBuyer = "Für diesen Käufer reservieren", statusChanged = "Inserat aktualisiert.", offer = "Angebot", counterOffer = "Gegenangebot", makeOffer = "Mach ein Angebot.", offeredByYou = "Du hast dieses Angebot geschickt.", offeredToYou = "Du hast dieses Angebot bekommen.", - acceptOffer = "Akzeptieren", declineOffer = "Zurückziehen", negotiateOffer = "Zähler", + acceptOffer = "Akzeptieren", declineOffer = "Ablehnen", negotiateOffer = "Gegenangebot", offerFor = "Dein Angebot für", offerAmount = "Angebot", sendOffer = "Angebot senden", offerSent = "Dein Angebot wurde gesendet.", offerAccepted = "Angebot angenommen.", offerDeclined = "Das Angebot wurde abgelehnt.", - reportListing = "Liste der Berichte", reportWhy = "Warum meldest du das?", reportDetails = "Details hinzufügen (optional)", - sendReport = "Bericht senden", blockSeller = "Verkäufer blockieren", reported = "Bericht gesendet.", blocked = "Verkäufer blockiert.", + reportListing = "Inserat melden", reportWhy = "Warum meldest du das?", reportDetails = "Details hinzufügen (optional)", + sendReport = "Meldung senden", blockSeller = "Verkäufer blockieren", reported = "Meldung gesendet.", blocked = "Verkäufer blockiert.", newMessage = "Neue CityMarkt Nachricht von {sender}", newOffer = "{sender} bot ${price} an.", offerAcceptedNotification = "{sender} hat dein Angebot über ${price} angenommen.", @@ -1605,10 +1605,10 @@ Locales["de"] = { errors = { invalid_listing = "Sieh dir die Details an.", invalid_price = "Gib einen gültigen Preis ein.", invalid_images = "Wähle gültige Fotos von diesem Handy.", phone_unavailable = "Füge eine SIM ein oder verstecke deine Nummer.", - listing_limit = "Du hast das aktive Listing Limit erreicht.", listing_not_found = "Diese Auflistung ist nicht mehr verfügbar.", + listing_limit = "Du hast das Limit für aktive Inserate erreicht.", listing_not_found = "Dieses Inserat ist nicht mehr verfügbar.", invalid_message = "Gib eine gültige Nachricht ein.", inquiry_not_found = "Dieses Gespräch ist nicht mehr verfügbar.", invalid_offer = "Gib ein gültiges Ganzzahl-Angebot ein.", invalid_offer_response = "Diese Angebotsaktion ist ungültig.", - offer_listing_unavailable = "Diese Auflistung ist nicht mehr für Angebote verfügbar.", + offer_listing_unavailable = "Dieses Inserat nimmt keine Angebote mehr an.", offer_closed = "Diese Verhandlungen sind bereits abgeschlossen.", offer_waiting = "Warte, bis die andere Person antwortet.", offer_not_allowed = "Du kannst jetzt kein Angebot machen.", offer_not_actionable = "Dieses Angebot kann nicht mehr geändert werden.", offer_conflict = "Das Angebot hat sich geändert. Versuch es erneut.", @@ -1625,9 +1625,9 @@ Locales["de"] = { name = "Lokale Seiten", eyebrow = "Deine Stadt, deine Geschichten.", cityPulse = "Live von Los Santos", heroTitle = "Was passiert in der Nähe?", heroBody = "Entdecke Orte, Leute und lokale Tipps.", searchPlaceholder = "Suche nach Beiträgen und Orten", search = "Suchen", allCategories = "Alle Kategorien", - allLosSantos = "Los Santos", hoursAgo = "{count}Vor", daysAgo = "{count}Vor", + allLosSantos = "Los Santos", hoursAgo = "vor {count} Std.", daysAgo = "vor {count} Tagen", categories = { recommendation = "Empfohlen", wanted = "Gesucht", service = "Dienstleistung", event = "Ereignis", place = "Ort", community = "Gemeinschaft", citymarkt = "CityMarkt" }, - discover = "Entdecken", create = "Posten", profile = "Profil", post = "Beitrag", posts = "Stellen", + discover = "Entdecken", create = "Posten", profile = "Profil", post = "Beitrag", posts = "Beiträge", noPosts = "Hier ist noch nichts.", noPostsBody = "Sei der Erste, der etwas mit der Stadt teilt.", noPhoto = "Kein Foto angebracht", signInTitle = "Dein lokales Seitenprofil", signInBody = "Melde dich bei Sky Cloud in Einstellungen an, um Beiträge zu veröffentlichen und zu speichern.", authEyebrow = "Lokale Seiten", authWelcome = "Willkommen auf den lokalen Seiten", @@ -1637,15 +1637,15 @@ Locales["de"] = { authEmail = "iFruit-Adresse", authEmailPlaceholder = "Dein Name", authPassword = "Passwort", authPasswordPlaceholder = "Mindestens 6 Zeichen", authConfirmPassword = "Passwort bestätigen", authConfirmPlaceholder = "Gib das Passwort wieder ein", showPassword = "Passwort anzeigen", hidePassword = "Passwort ausblenden", - localCreator = "Lokale Ersteller", myPosts = "Meine Posten", saved = "Gespeichert", save = "Speichern", likes = "mag", + localCreator = "Lokale Creator", myPosts = "Meine Beiträge", saved = "Gespeichert", save = "Speichern", likes = "Likes", createProfile = "Profil erstellen", editProfile = "Profil bearbeiten", profileSetupBody = "Deine iFruit E-Mail wird automatisch verlinkt.", profileEmail = "iFruit-E-Mail", profileHandle = "Benutzername", profileHandlePlaceholder = "Dein Name", profileHandleHint = "Benutze 3-24 Kleinbuchstaben, Zahlen, Punkte oder Unterstriche.", profileBio = "Bio", - profileBioPlaceholder = "Erzähl der Stadt ein bisschen von dir.", profilePhoto = "Profilfoto", profilePhotoHint = "Wähle ein Foto von Fotos oder nimm ein neues.", removeProfilePhoto = "Foto entfernen", profileSave = "Profil speichern", profileCancel = "Abbrechen", profileSaved = "Dein Profil wurde gerettet.", - location = "Standort", sharedFrom = "Von CityMarkt geteilt", openCityMarkt = "CityMarkt-Auflistung öffnen", - newPost = "Neue lokale Post", shareWithCity = "Mit der Stadt teilen", publish = "Veröffentlichen", published = "Dein Posten ist live.", deleted = "Post gelöscht.", + profileBioPlaceholder = "Erzähl der Stadt ein bisschen von dir.", profilePhoto = "Profilfoto", profilePhotoHint = "Wähle ein Foto aus deiner Galerie oder nimm ein neues auf.", removeProfilePhoto = "Foto entfernen", profileSave = "Profil speichern", profileCancel = "Abbrechen", profileSaved = "Dein Profil wurde gespeichert.", + location = "Standort", sharedFrom = "Von CityMarkt geteilt", openCityMarkt = "CityMarkt-Inserat öffnen", + newPost = "Neuer lokaler Beitrag", shareWithCity = "Mit der Stadt teilen", publish = "Veröffentlichen", published = "Dein Beitrag ist live.", deleted = "Beitrag gelöscht.", title = "Titel", body = "Deine Geschichte", category = "Kategorie", titlePlaceholder = "Was sollten die Leute wissen?", bodyPlaceholder = "Füge Details, eine Empfehlung oder eine Wegbeschreibung hinzu...", - photos = "Fotos", optional = "fakultativ", camera = "Kamera", gallery = "Fotos", photoLimit = "Du kannst bis zu sechs Fotos hinzufügen.", + photos = "Fotos", optional = "optional", camera = "Kamera", gallery = "Fotos", photoLimit = "Du kannst bis zu sechs Fotos hinzufügen.", cityMarktShare = "Auf lokalen Seiten teilen", cityMarktShared = "Auf lokale Seiten geteilt.", cityMarktShareHint = "Ein CityMarkt-Anteil pro Tag", cityMarktAlreadyShared = "Bereits veröffentlicht", cityMarktOpenSharedHint = "Öffne die lokalen Seiten", cityMarktAppMissing = "Lokale Seiten sind nicht installiert", cityMarktInstallHint = "Lokale Seiten installieren, um diese Liste zu teilen", @@ -1658,7 +1658,7 @@ Locales["de"] = { profile_handle_taken = "Dieser Benutzername für lokale Seiten ist bereits belegt.", rate_limited = "Zu viele Versuche. Versuch es gleich erneut.", default = "Lokale Seiten konnten die Anfrage nicht abschließen.", }, - errors = { profile_required = "Erstell zuerst dein Profil auf den lokalen Seiten.", invalid_profile = "Prüfe deinen Benutzernamen und deine Bio.", invalid_profile_image = "Wähle ein gültiges Foto von diesem Handy.", profile_handle_taken = "Dieser Benutzername ist bereits vergeben.", invalid_post = "Gib einen Titel und ein bisschen mehr Details.", invalid_images = "Wähle gültige Fotos von diesem Handy.", invalid_request = "Diese Aktion ist nicht gültig.", post_not_found = "Dieser Beitrag ist nicht mehr verfügbar.", citymarkt_not_found = "Dieses CityMarkt-Verzeichnis ist nicht verfügbar.", citymarkt_daily_limit = "Du hast heute schon eine CityMarkt-Liste geteilt.", citymarkt_already_shared = "Diese Auflistung wurde bereits geteilt.", not_authenticated = "Melde dich zuerst bei Sky Cloud an.", rate_limited = "Zu viele Anfragen. Versuch es gleich erneut.", request_failed = "Der Posten konnte nicht gerettet werden.", default = "Lokale Seiten sind vorübergehend nicht verfügbar." }, + errors = { profile_required = "Erstelle zuerst dein Profil auf den lokalen Seiten.", invalid_profile = "Prüfe deinen Benutzernamen und deine Bio.", invalid_profile_image = "Wähle ein gültiges Foto von diesem Handy.", profile_handle_taken = "Dieser Benutzername ist bereits vergeben.", invalid_post = "Gib einen Titel und etwas mehr Details ein.", invalid_images = "Wähle gültige Fotos von diesem Handy.", invalid_request = "Diese Aktion ist ungültig.", post_not_found = "Dieser Beitrag ist nicht mehr verfügbar.", citymarkt_not_found = "Dieses CityMarkt-Inserat ist nicht verfügbar.", citymarkt_daily_limit = "Du hast heute bereits ein CityMarkt-Inserat geteilt.", citymarkt_already_shared = "Dieses Inserat wurde bereits geteilt.", not_authenticated = "Melde dich zuerst bei Sky Cloud an.", rate_limited = "Zu viele Anfragen. Versuch es gleich erneut.", request_failed = "Der Beitrag konnte nicht gespeichert werden.", default = "Lokale Seiten sind vorübergehend nicht verfügbar." }, }, citywarn = { name = "CityWarn", navigation = "CityWarn Navigation", loading = "Warnungen werden geladen...", @@ -1684,9 +1684,9 @@ Locales["de"] = { locationUnavailable = "Dein aktueller Standort ist nicht verfügbar.", imageError = "Das Kartenbild konnte nicht geladen werden.", switchStyle = "Kartentyp wechseln", addMarker = "Marker hinzufügen", placeMarker = "Ort Marker", placeMarkerHint = "Zieh die Karte, bis das Fadenkreuz über dem Ziel ist.", - addHere = "Hier hinzufügen", newMarker = "Neuer Marker", newMarkerDescription = "Gib diesem geretteten Ort einen Namen und Farbe.", + addHere = "Hier hinzufügen", newMarker = "Neue Markierung", newMarkerDescription = "Gib diesem gespeicherten Ort einen Namen und eine Farbe.", markerName = "Name", markerNamePlaceholder = "z.B. Treffpunkt", markerColor = "Markierungsfarbe", - saveMarker = "Marker speichern", deleteMarker = "Markierung löschen", setWaypoint = "Wegpunkt setzen", markerSaved = "Marker hat gerettet.", markerDeleted = "Marker gelöscht.", waypointSet = "Wegpunkt gesetzt.", + saveMarker = "Markierung speichern", deleteMarker = "Markierung löschen", setWaypoint = "Wegpunkt setzen", markerSaved = "Markierung gespeichert.", markerDeleted = "Markierung gelöscht.", waypointSet = "Wegpunkt gesetzt.", colors = { blue = "Blau", green = "Grün", orange = "Orangen", purple = "Lila", red = "Rot" }, errors = { invalid_marker = "Gib einen gültigen Markernamen und Position ein.", marker_limit = "Dieses Handy hat seine Markergrenze erreicht.", @@ -1720,8 +1720,8 @@ Locales["de"] = { featured = "Für die Stadt gemacht", recentlyAdded = "Zuletzt hinzugefügt", songs = "Lieder", play = "Abspielen", previous = "Vorheriger Titel", next = "Nächster Titel", nowPlaying = "Wird abgespielt", progress = "Wiedergabeposition", volume = "Lautstärke", addMusic = "Musik hinzufügen", addYouTube = "Von YouTube hinzufügen", youtubeBody = "Füge einen öffentlichen YouTube-Link ein. Lass die optionalen Felder leer, um Videotitel und Kanal automatisch zu übernehmen.", - youtubeUrl = "YouTube URL", youtubePlaceholder = "https://youtube.com/watch?v=...", youtubeTitle = "Titel des Liedes (fakultativ)", - youtubeTitlePlaceholder = "Benutze den YouTube Videotitel", youtubeArtist = "Künstler (fakultativ)", + youtubeUrl = "YouTube-URL", youtubePlaceholder = "https://youtube.com/watch?v=...", youtubeTitle = "Titel (optional)", + youtubeTitlePlaceholder = "YouTube-Videotitel verwenden", youtubeArtist = "Künstler (optional)", youtubeArtistPlaceholder = "Benutzt den YouTube-Kanal", addToLibrary = "Zur Bibliothek hinzufügen", songAdded = "Song wurde in deine Bibliothek aufgenommen.", songRemoved = "Song aus deiner Bibliothek entfernt.", newPlaylist = "Neue Wiedergabeliste", renamePlaylist = "Wiedergabeliste umbenennen", addSongs = "Lieder hinzufügen", @@ -1753,23 +1753,23 @@ Locales["de"] = { name = "Notizen", note = "Anmerkung", back = "Zurück", actions = "Aktionen beachten", newNote = "Neue Anmerkung", searchPlaceholder = "Suchhinweise", title = "Titel", titlePlaceholder = "Anmerkungstitel", body = "Anmerkung", bodyPlaceholder = "Beginne zu schreiben...", - untitled = "Unbenannt", noText = "Kein zusätzlicher Text", emptyBadge = "ON DEVICE", emptyTitle = "Keine Anmerkungen", + untitled = "Unbenannt", noText = "Kein zusätzlicher Text", emptyBadge = "AUF DEM GERÄT", emptyTitle = "Keine Notizen", emptyBody = "Erstelle eine Notiz, um wichtige Details nahe bei der Hand zu halten.", noResults = "Keine Ergebnisse", noResultsBody = "Versuch nach einem anderen Wort oder Satz zu suchen.", pin = "Hinweis zum Stift", unpin = "Anmerkung zum Öffnen", deleteNote = "Anmerkung löschen", deleteTitle = "Notiz löschen?", deleteBody = "Diese Notiz wird dauerhaft gelöscht.", tools = { - bold = "Mutig", bulletList = "Liste der Kugeln", decreaseText = "Kleinerer Text", increaseText = "Größerer Text", + bold = "Fett", bulletList = "Aufzählung", decreaseText = "Kleinerer Text", increaseText = "Größerer Text", italic = "kursiv", numberedList = "Nummerierte Liste", quote = "Zitat", redo = "Wiederherstellen", strike = "Durchschlagen", toolbar = "Formatierungswerkzeuge", underline = "Unterstreichung", undo = "Rückgängig", }, }, memos = { - name = "Memos", memo = "Sprach-Memo", back = "Zurück", actions = "Maßnahmen zur Erinnerung", + name = "Memos", memo = "Sprachmemo", back = "Zurück", actions = "Memo-Aktionen", newMemo = "Neue Aufzeichnung", newMemoWithDate = "Neue Aufnahme · {date}", searchPlaceholder = "Aufzeichnungsdateien durchsuchen", emptyTitle = "Keine Aufnahmen", emptyBody = "Tippe auf den Aufnahmeknopf, um dein erstes Sprachmemory zu erfassen.", noResults = "Keine Ergebnisse", - noResultsBody = "Versuch nach einem anderen Titel oder einer anderen Note zu suchen.", recording = "Aufzeichnung", paused = "Anhalten", + noResultsBody = "Versuche es mit einem anderen Titel oder einer anderen Notiz.", recording = "Aufnahme", paused = "Pausiert", preparing = "Mikrofon vorbereiten...", saving = "Aufzeichnung speichern...", stop = "Stoppen", pause = "Pause", resume = "Wiederaufnahme", cancel = "Abbrechen", done = "Fertig", play = "Aufzeichnung abspielen", pausePlayback = "Aufzeichnung ausschalten", skipBack = "15 Sekunden zurück", skipForward = "15 Sekunden vorwärts", @@ -1786,7 +1786,7 @@ Locales["de"] = { invalid_memo = "Das Sprachmemory enthält ungültige Daten.", invalid_request = "Die Memo-Anfrage ist ungültig.", invalid_upload = "Die hochgeladene Aufnahme konnte nicht verifiziert werden.", invalid_upload_token = "Der Upload der Aufzeichnung konnte nicht überprüft werden.", - memo_limit = "Die Grenze von 100 Sprach-Memorandums wurde erreicht.", + memo_limit = "Das Limit von 100 Sprachmemos wurde erreicht.", memo_not_found = "Dieses Sprachmemory existiert nicht mehr.", media_provider_failed = "Der Sprach-Memo-Upload-Dienst ist nicht verfügbar.", media_provider_rate_limited = "Der Upload-Dienst für Sprachmemos ist ausgelastet. Versuch es gleich erneut.", @@ -1801,22 +1801,22 @@ Locales["de"] = { }, }, easyShare = { - name = "EasyShare", incoming = "Eintreffender Anteil", recentChats = "Kontakte und Chats", + name = "EasyShare", incoming = "Eingehende Freigabe", recentChats = "Kontakte und Chats", destinations = "Freigabeziele", newMessage = "Neue Nachricht", sentToChat = "An den Chat gesendet.", shareProfile = "Profil teilen", share = "Teilen", chooseConversation = "Eine Konversation wählen", kinds = { - contact = "Ansprechpartner", document = "Dokument", link = "Verknüpfung", location = "Standort", media = "Medien", + contact = "Kontakt", document = "Dokument", link = "Link", location = "Standort", media = "Medien", note = "Anmerkung", photo = "Foto", playlist = "Wiedergabeliste", post = "Beitrag", - profile = "Profil", text = "Nachricht", track = "Gleis", video = "Video", + profile = "Profil", text = "Nachricht", track = "Titel", video = "Video", }, savedToNotes = "Auf Notizen gespeichert.", copied = "Kopiert.", copy = "Kopieren", copyLink = "Link kopieren", - nearby = "Menschen in der Nähe", history = "Übertragungshistorie", noHistory = "Noch keine Transfers.", + nearby = "Personen in der Nähe", history = "Übertragungsverlauf", noHistory = "Noch keine Übertragungen.", noNearby = "Keine sichtbaren Spieler sind in der Nähe.", visibility = "Sichtbarkeit", requestSent = "Warten auf die Annahme...", incomingFrom = "{name} will teilen", distance = "{distance} m entfernt", cancel = "Abbrechen", - visibilityOptions = { everyone = "Alle", contacts = "Nur Kontakte", hidden = "Empfangen" }, + visibilityOptions = { everyone = "Alle", contacts = "Nur Kontakte", hidden = "Verborgen" }, status = { pending = "Warten auf Annahme", transferring = "Übertragung", completed = "Abgeschlossen", - declined = "Abgelehnt", cancelled = "Gestorben", expired = "Abgelaufen", failed = "Fehler", + declined = "Abgelehnt", cancelled = "Abgebrochen", expired = "Abgelaufen", failed = "Fehlgeschlagen", accepted = "Angenommen", }, errors = { @@ -1864,7 +1864,7 @@ Locales["de"] = { linkLabel = "Bild- oder Video-Link", linkPlaceholder = "https://...", linkCompleted = "Medien importiert.", loading = "Medien werden geladen...", emptyTitle = "Keine Medien", emptyBody = "Diese Website hat keine Medien dieser Art.", loadMore = "Mehr laden", - alreadyImported = "Einfuhr", failed = "Fehler", completed = "Importierte {imported} Medien.", + alreadyImported = "Bereits importiert", failed = "Fehlgeschlagen", completed = "{imported} Medien importiert.", partial = "Importiert {imported} von {total} Medien.", }, errors = { @@ -1881,12 +1881,12 @@ Locales["de"] = { not_found = "Das Medienobjekt existiert nicht mehr.", profile_photo_required = "Dies ist das letzte Foto in deinem Flare-Profil. Füge vor dem Löschen ein weiteres Profilfoto hinzu.", owner_changed = "Das aktive Telefonkonto hat sich geändert.", operation_in_progress = "Eine weitere Medienoperation ist bereits im Gange.", rate_limited = "Zu viele Medienaktionen. Versuch es gleich erneut.", request_failed = "Die Anfrage Fotos ist fehlgeschlagen.", - request_timeout = "Der Mediendienst hat sich verhört.", unsupported = "Dieses Medienformat wird nicht unterstützt.", - upload_failed = "Der Medien-Upload ist fehlgeschlagen.", upload_timeout = "Die Medien laden Zeit aus.", + request_timeout = "Der Mediendienst hat nicht rechtzeitig geantwortet.", unsupported = "Dieses Medienformat wird nicht unterstützt.", + upload_failed = "Der Medien-Upload ist fehlgeschlagen.", upload_timeout = "Der Medien-Upload hat zu lange gedauert.", }, }, appStore = { - name = "App Store", get = "GET", open = "OPEN", installing = "Installation", + name = "App Store", get = "LADEN", open = "ÖFFNEN", installing = "Wird installiert", player = "Spieler", profileLabel = "{name} Profil", searchPlaceholder = "Spiele, Apps, Geschichten und mehr", appsTitle = "Eingebaute Apps", gamesTitle = "Spiele", selected = "Für dein Sky Phone verfügbare Apps", @@ -1904,35 +1904,35 @@ Locales["de"] = { }, filters = { all = "Alle", social = "Sozial", utilities = "Dienstprogramme", shopping = "Shopping", - productivity = "Produktivität", arcade = "Arkade", puzzle = "Puzzle", classic = "Klassiker", + productivity = "Produktivität", arcade = "Arcade", puzzle = "Rätsel", classic = "Klassiker", }, browse = { categories = "Speichern von Kategorien", availableNow = "Jetzt verfügbar", featureTitle = "{app} entdecken", featuredPages = "Empfohlene Apps", - handPicked = "Für dich ausgewählt", essentialApps = "Wesentliche Apps", - essentialGames = "Wesentliche Spiele", + handPicked = "Für dich ausgewählt", essentialApps = "Unverzichtbare Apps", + essentialGames = "Unverzichtbare Spiele", }, taglines = { health = "Gesundheit, Aktivität und medizinische Informationen", ["weazel-news"] = "Lokale Nachrichten und Stadtgeschichten", companies = "Unternehmen, Arbeitsplätze und Dienstleistungen", music = "Lieder, Wiedergabelisten und Audio", - picstagram = "Foto-Sharing und soziales Futter", feather = "Kurze Posts und Stadtgespräche", + picstagram = "Foto-Sharing und sozialer Feed", feather = "Kurze Beiträge und Stadtgespräche", fliptok = "Videos und Trends", flare = "Soziale Beiträge und Live-Momente", calendar = "Veranstaltungen, Termine und Erinnerungen", radio = "Live-Radio und Team-Kommunikation", - ["local-pages"] = "Lokale Unternehmen und Community-Seiten", crewlink = "Besatzungen, Mitglieder und Koordination", + ["local-pages"] = "Lokale Unternehmen und Community-Seiten", crewlink = "Crews, Mitglieder und Koordination", phone = "Anrufe, Kontakte und Voicemail", messages = "Privattexte und Medien", darkchat = "Private Nachrichten und sichere Gruppen", garage = "Fahrzeuge, Parkplätze und Parkservice", house = "Wohnhäuser, Zugang und Management", map = "Navigation und nahe gelegene Orte", skyride = "Stadtfahrten und Fahrerbuchung", banking = "Konten, Überweisungen und Zahlungen", billing = "Rechnungen und Zahlungsaufforderungen", mail = "E-Mail und Anhänge", - notes = "Anmerkungen, Listen und Ideen", memos = "Tonträger und Memos", + notes = "Notizen, Listen und Ideen", memos = "Sprachaufnahmen und Memos", calculator = "Tägliche und wissenschaftliche Berechnungen", camera = "Fotos und Videoaufnahmen", - clock = "Zeit, Alarme und Zeitschaltuhren", weather = "Vorausschätzungen und aktuelle Bedingungen", + clock = "Zeit, Wecker und Timer", weather = "Vorhersagen und aktuelles Wetter", photos = "Foto- und Video-Bibliothek", settings = "Telefoneinstellungen und Sicherheit", - snake = "Klassische Arkadenschlange", memory = "Herausforderung für Kartenabgleich", - ["number-merge"] = "Zahlen-Rätsel und Strategie", minesweeper = "Klassische Logik-puzzle", - ["tower-stack"] = "Präzision Turm-Gebäude Spiel", ["sky-flappy"] = "Schnelle Arcade-Flug Herausforderung", - citymarkt = "Lokale Marktplatz und Listings", ["neon-drop"] = "Neon Block-Dropping-puzzle", + snake = "Klassisches Arcade-Snake", memory = "Herausforderung zum Kartenpaare-Finden", + ["number-merge"] = "Zahlenrätsel und Strategie", minesweeper = "Klassisches Logikrätsel", + ["tower-stack"] = "Präzises Turmbauspiel", ["sky-flappy"] = "Schnelle Arcade-Flugherausforderung", + citymarkt = "Lokaler Marktplatz und Inserate", ["neon-drop"] = "Neon-Blockrätsel", }, search = { recommended = "Empfohlen", discover = "Entdecken", results = "Suchergebnisse", @@ -1968,43 +1968,43 @@ Locales["de"] = { settings = { name = "Einstellungen", searchPlaceholder = "Suchen", airplaneMode = "Flugmodus", streamerMode = "Streamermodus", wallpaper = "Hintergrundbild", on = "An", off = "Aus", accountName = "Sky Cloud", - accountDetail = "Wichtige Daten & Cloud", accountLocalDetail = "Nicht unterzeichnet in", - accountCloudDetail = "Wichtige Daten synchronisieren sich über Sky Cloud", accountLoginBody = "Melde dich an, um deine wichtigen Daten sicher zu halten. Ohne ein Sky Cloud Konto wird es mit dem Handy verloren gehen.", + accountDetail = "Wichtige Daten & Cloud", accountLocalDetail = "Nicht angemeldet", + accountCloudDetail = "Wichtige Daten werden über Sky Cloud synchronisiert", accountLoginBody = "Melde dich an, um deine wichtigen Daten zu schützen. Ohne Sky-Cloud-Konto gehen sie mit diesem Handy verloren.", accountInformation = "Kontoinformationen", accountStatus = "Kontostatus", accountStatusValue = "Aktiv", accountStorage = "Cloud-Speicher", accountStorageValue = "Auf dem Gerät", accountPurchases = "Medien & Einkäufe", accountPurchasesValue = "Verfügbar", notifications = "Mitteilungen", sounds = "Sounds & Haptik", general = "Allgemeine Einstellungen", security = "Passcode & Sicherheit", appearance = "Darstellung", allowNotifications = "Benachrichtigungen zulassen", - connectivity = "Verbindung", connections = "Verbindungen", wifi = "WLAN", bluetooth = "Bluetooth", cellular = "Zellulär", - connectivityDescription = "WLAN, Bluetooth und zellulare Einstellungen werden auf diesem Handy gespeichert.", - focus = "Fokus", focusMode = "Fokus", focusDescription = "Fokus stillt nicht-kritische Benachrichtigungen, während Alarme und wichtige Warnungen zur Verfügung.", + connectivity = "Verbindung", connections = "Verbindungen", wifi = "WLAN", bluetooth = "Bluetooth", cellular = "Mobilfunk", + connectivityDescription = "WLAN-, Bluetooth- und Mobilfunkeinstellungen werden auf diesem Handy gespeichert.", + focus = "Fokus", focusMode = "Fokus", focusDescription = "Fokus schaltet unwichtige Benachrichtigungen stumm, während Wecker und wichtige Warnungen weiterhin verfügbar bleiben.", notificationSounds = "Töne", notificationDuration = "Mitteilungsdauer", seconds = "{seconds} Sekunden", - ringtoneVolume = "Klingelton-Volumen", notificationVolume = "Meldungsvolumen", + ringtoneVolume = "Klingeltonlautstärke", notificationVolume = "Benachrichtigungslautstärke", ringtone = "Klingelton", notificationSound = "Benachrichtigungs-Sound", graphicsMode = "Grafikmodus", - performanceMode = "Leistungsmodus", performanceModeDescription = "Blurfreies Glas simulation für bessere CEF-Leistung", - ultimateMode = "Letzter Modus", ultimateModeDescription = "Volle Unschärfe und Glaseffekte für kompatible FiveM Clients", - appearanceMode = "Erscheinungsbild-Modus", - automatic = "Automatisch", light = "Leicht", dark = "Dunkel", phoneScale = "Telefonwaage", phoneFrame = "Telefonrahmen", + performanceMode = "Leistungsmodus", performanceModeDescription = "Glaseffekt ohne Unschärfe für bessere CEF-Leistung", + ultimateMode = "Ultimativer Modus", ultimateModeDescription = "Volle Unschärfe- und Glaseffekte für kompatible FiveM-Clients", + appearanceMode = "Erscheinungsbild", + automatic = "Automatisch", light = "Hell", dark = "Dunkel", phoneScale = "Telefongröße", phoneFrame = "Telefonrahmen", screenBrightness = "Bildschirmhelligkeit", - development = "Entwicklung", skyUiKitchenSink = "Sky UI Küchenspülung", - skyUiKitchenSinkDescription = "Interaktiver Katalog von Komponenten von Erstanbietern Sky UI", + development = "Entwicklung", skyUiKitchenSink = "Sky-UI-Komponentenkatalog", + skyUiKitchenSinkDescription = "Interaktiver Katalog der offiziellen Sky-UI-Komponenten", about = "Info", deviceName = "Gerätename", deviceNameValue = "Sky Phone", softwareVersion = "Softwareversion", - language = "Sprache", languageValue = "Englisch", localStorage = "Lokale Speicherung", localStorageValue = "Auf dem Gerät", - back = "Einstellungen", wallpaperPicker = "Himmelsbilder", deviceInformation = "Geräteinformationen", + language = "Sprache", languageValue = "Deutsch", localStorage = "Lokaler Speicher", localStorageValue = "Auf dem Gerät", + back = "Einstellungen", wallpaperPicker = "Hintergrundbilder", deviceInformation = "Geräteinformationen", wallpaperFromPhotos = "Wähle aus Fotos", wallpaperFromPhotosDescription = "Wende eines deiner Fotos an.", wallpaperFromCamera = "Foto machen", wallpaperFromCameraDescription = "Neues Hintergrundbild erstellen", wallpaperCustomUpload = "Eigenes Bild hochladen", wallpaperCustomUploadDescription = "Verwende einen geprüften HTTPS-Bildlink", - wallpaperHistory = "Letzte", wallpaperCustom = "Fototapete", - wallpaperTarget = "Hintergrundbilder Ziel", wallpaperHomeScreen = "Bildschirm Home", wallpaperLockScreen = "Sperrbildschirm", + wallpaperHistory = "Zuletzt verwendet", wallpaperCustom = "Eigenes Hintergrundbild", + wallpaperTarget = "Zielbildschirm", wallpaperHomeScreen = "Home-Bildschirm", wallpaperLockScreen = "Sperrbildschirm", imei = "IMEI", linkedDevices = "Verknüpfte Geräte", thisDevice = "Dieses Handy", removeDevice = "Gerät entfernen", - simCard = "SIM Karte", simNumber = "Telefonnummer", simType = "SIM Typ", registeredSim = "Registriert", - anonymousSim = "Anonym", noSim = "Keine SIM", ejectSim = "Auswurf SIM", ejectSimBody = "Bringst diese SIM-Karte in dein Inventar zurück?", + simCard = "SIM-Karte", simNumber = "Telefonnummer", simType = "SIM-Typ", registeredSim = "Registriert", + anonymousSim = "Anonym", noSim = "Keine SIM", ejectSim = "SIM auswerfen", ejectSimBody = "SIM-Karte zurück ins Inventar legen?", removeDeviceBody = "Gib dein Sky Cloud Passwort ein, um dieses Gerät aus dem Konto zu entfernen.", signOut = "Abmelden", reset = "Handy übertragen oder zurücksetzen", transferOrReset = "Handy übertragen oder zurücksetzen", transferOrResetDescription = "Bereite dieses Handy für eine neue Einrichtung vor.", resetHeroTitle = "Ein sauberer Anfang, ohne zu verlieren, was dir folgt", resetHeroBody = "Lösche lokale Inhalte und Einstellungen von diesem Handy. Informationen auf deiner SIM oder in unterstützten Konten bleiben erhalten.", erasedFromPhone = "Aus diesem Handy gelöscht", eraseDeviceSettings = "Passcode, Einstellungen und Layout des Home Screens", eraseLocalContent = "Fotos, Notizen und Inhalte nur lokal gespeichert", eraseLocalApps = "Gespeicherte Apps und lokale App-Sitzungen", - keptSafe = "Sicher aufbewahrt", keepSimData = "SIM Karten- und Telefonnummer", keepSimDataBody = "Deine Nummer und SIM-gestützte Kommunikation bleiben auf dem SIM.", + keptSafe = "Sicher aufbewahrt", keepSimData = "SIM-Karte und Telefonnummer", keepSimDataBody = "Deine Nummer und SIM-basierte Kommunikation bleiben auf der SIM erhalten.", keepCloudData = "Konto- & Cloud-Daten", keepCloudDataBody = "Unterstützte Informationen verbleiben in der App oder im Sky Cloud-Konto.", resetSetupAssistant = "Wenn das Löschen abgeschlossen ist, startet der Setup Assistant automatisch.", factoryReset = "Alle Inhalte und Einstellungen löschen", factoryResetBody = "Dies kann nicht rückgängig gemacht werden. Lokale Einstellungen, Apps und nicht synchronisierte Inhalte werden gelöscht. Deine SIM-Nummer und kontogesicherten Daten bleiben erhalten.", @@ -2016,10 +2016,10 @@ Locales["de"] = { factoryResetSimSafe = "SIM geschützt", factoryResetCloudSafe = "Cloud geschützt", passcode = { description = "Ein Passcode schützt den Inhalt dieses Telefons. Er bleibt beim Wechsel des SIM- oder Sky Cloud-Kontos beim Gerät.", - status = "Code für den Pass", codeLength = "Code-Länge", sixDigit = "6-stelliger Code", fourDigit = "4-stelliger Code", + status = "Passcode-Status", codeLength = "Passcode-Länge", sixDigit = "6-stelliger Code", fourDigit = "4-stelliger Code", turnOn = "Passcode einschalten", turnOff = "Passcode ausschalten", change = "Passcode ändern", - enterNew = "Neuen Passcode eingeben", confirmNew = "Neuer Passcode überprüfen", enterCurrent = "Aktuelle Passcode eingeben", - screenSubtitle = "Zieh 4 oder 6 Zahlen ein.", incorrect = "Falscher Passcode.", mismatch = "Die Passcodes stimmten nicht überein.", + enterNew = "Neuen Passcode eingeben", confirmNew = "Neuen Passcode bestätigen", enterCurrent = "Aktuellen Passcode eingeben", + screenSubtitle = "Gib 4 oder 6 Ziffern ein.", incorrect = "Falscher Passcode.", mismatch = "Die Passcodes stimmen nicht überein.", locked = "Zu viele falsche Versuche. Versuch es später erneut.", rateLimited = "Zu viele Versuche. Bitte warte.", failed = "Der Passcode konnte nicht aktualisiert werden.", saved = "Passcode gespeichert.", disabled = "Passcode deaktiviert.", }, @@ -2031,7 +2031,7 @@ Locales["de"] = { }, toggle = { airplaneMode = "Flugmodus umschalten", streamerMode = "Streamermodus umschalten", focusMode = "Fokus einschalten", - wifiEnabled = "W-LAN umschalten", bluetoothEnabled = "Bluetooth umschalten", cellularEnabled = "Zellenbezogene Daten umschalten", + wifiEnabled = "WLAN umschalten", bluetoothEnabled = "Bluetooth umschalten", cellularEnabled = "Mobile Daten umschalten", notifications = "Umschalten von Benachrichtigungen für {app}", notificationSounds = "Benachrichtigungs-Sounds für {app} umschalten", }, frames = { diff --git a/sky_phone/source/server/lb_phone_migration.lua b/sky_phone/source/server/lb_phone_migration.lua index dda8234..2880842 100644 --- a/sky_phone/source/server/lb_phone_migration.lua +++ b/sky_phone/source/server/lb_phone_migration.lua @@ -7,6 +7,14 @@ local source_name = "lb-phone" local marker_prefix = "lb-phone:" local running = false +-- Increment a domain version whenever an idempotent repair must run for +-- installations that already recorded the previous importer as complete. +local domain_versions = { + picstagram = 2, + flipTok = 2, + feather = 2, +} + local source_prefix = migration_config.SourcePrefix or "phone_" if type(source_prefix) ~= "string" or not source_prefix:match("^[%w_]+$") then error("[sky_phone] LB Phone migration SourcePrefix must contain only letters, numbers, and underscores.") @@ -159,10 +167,15 @@ local function insert_many(prefix, width, rows, suffix) return inserted end +local function migration_name(domain) + local version = domain_versions[domain] + return marker_prefix .. domain .. (version and (":v" .. version) or "") +end + local function migration_done(domain) local rows = Bridge.Database.Query([[ SELECT 1 FROM `sky_phone_migrations` WHERE `name` = ? LIMIT 1 - ]], { marker_prefix .. domain }) + ]], { migration_name(domain) }) return rows[1] ~= nil end @@ -170,7 +183,7 @@ local function record_migration(domain, stats) Bridge.Database.Query([[ INSERT IGNORE INTO `sky_phone_migrations` (`name`, `source`, `stats`) VALUES (?, ?, ?) - ]], { marker_prefix .. domain, source_name, json.encode(stats or {}) }) + ]], { migration_name(domain), source_name, json.encode(stats or {}) }) end local function load_roster() @@ -1321,6 +1334,206 @@ local function extract_media(value, maximum) return result end +local function social_login_table_available(table_name) + return table_exists(table_name) + and table_has_column(table_name, "phone_number") + and table_has_column(table_name, "app") + and table_has_column(table_name, "username") + and table_has_column(table_name, "active") +end + +local function prepare_social_profile_accounts(entries, options) + local profile_table = tostring(options.profile_table or "") + if not profile_table:match("^[%w_]+$") then + error("[sky_phone] Invalid social migration profile table.") + end + + local preferred = {} + local logged_table = source_table("logged_in_accounts") + if social_login_table_available(logged_table) then + local app_placeholders = {} + local parameters = { source_name } + for index = 1, #options.apps do + app_placeholders[index] = "?" + parameters[#parameters + 1] = tostring(options.apps[index]):lower() + end + local logged = Bridge.Database.Query(([[ + SELECT LOWER(login.`username`) AS `username`, owner.`account_id` + FROM `%s` login + JOIN `sky_phone_migration_owners` owner + ON owner.`source` = ? AND owner.`source_phone_number` = login.`phone_number` + WHERE login.`active` = 1 AND LOWER(login.`app`) IN (%s) + ORDER BY owner.`account_id`, login.`phone_number`, login.`username` + ]]):format(logged_table, table.concat(app_placeholders, ",")), parameters) + for index = 1, #logged do + local account_id = tonumber(logged[index].account_id) + if account_id then + preferred[account_id] = preferred[account_id] + or tostring(logged[index].username or ""):lower() + end + end + end + for index = 1, #entries do + local owner_account_id = tonumber(entries[index].source.owner_account_id) + if owner_account_id then + preferred[owner_account_id] = preferred[owner_account_id] + or tostring(entries[index].source.username or ""):lower() + end + end + + local cloud_rows = {} + for index = 1, #entries do + local entry = entries[index] + entry.email = synthetic_email(options.synthetic_namespace, entry.handle) + cloud_rows[#cloud_rows + 1] = { + entry.email, + deterministic_hex(options.account_namespace, entry.handle), + } + end + insert_many("INSERT IGNORE INTO `sky_phone_accounts` (`email`, `password`) VALUES", 2, cloud_rows) + + local target_accounts = Bridge.Database.Query("SELECT `id`, `email` FROM `sky_phone_accounts`", {}) + local account_by_email = {} + local email_by_account = {} + for index = 1, #target_accounts do + local account_id = tonumber(target_accounts[index].id) + local email = tostring(target_accounts[index].email or ""):lower() + account_by_email[email] = account_id + email_by_account[account_id] = email + end + for index = 1, #entries do + entries[index].synthetic_account_id = account_by_email[entries[index].email] + end + + local target_profiles = Bridge.Database.Query(([[ + SELECT profile.`id`, profile.`account_id`, profile.`handle`, account.`email` AS `account_email` + FROM `%s` profile + JOIN `sky_phone_accounts` account ON account.`id` = profile.`account_id` + ]]):format(profile_table), {}) + local existing_by_handle = {} + local occupied_accounts = {} + for index = 1, #target_profiles do + local profile = target_profiles[index] + profile.account_id = tonumber(profile.account_id) + profile.account_email = tostring(profile.account_email or ""):lower() + existing_by_handle[tostring(profile.handle or ""):lower()] = profile + occupied_accounts[profile.account_id] = profile + end + + local entry_by_handle = {} + local preferred_entry_by_owner = {} + for index = 1, #entries do + local entry = entries[index] + entry_by_handle[entry.handle] = entry + local owner_account_id = tonumber(entry.source.owner_account_id) + if owner_account_id + and preferred[owner_account_id] == tostring(entry.source.username or ""):lower() + then + preferred_entry_by_owner[owner_account_id] = entry + end + end + + local function is_imported_profile(profile, entry) + if not profile or not entry then + return false + end + if entry.profile_id and tostring(profile.id) == tostring(entry.profile_id) then + return true + end + if profile.account_email == entry.email then + return true + end + return tonumber(profile.account_id) == tonumber(entry.source.owner_account_id) + end + + local repaired = 0 + local function move_profile(profile, entry, target_account_id) + local previous_account_id = tonumber(profile.account_id) + target_account_id = tonumber(target_account_id) + if not previous_account_id or not target_account_id or previous_account_id == target_account_id then + return true + end + if occupied_accounts[target_account_id] then + return false + end + local moved = affected_rows(Bridge.Database.Query(([[ + UPDATE `%s` SET `account_id` = ? WHERE `id` = ? AND `account_id` = ? + ]]):format(profile_table), { target_account_id, profile.id, previous_account_id })) + if moved < 1 then + return false + end + Bridge.Database.Query([[ + UPDATE `sky_phone_media` SET `account_id` = ? + WHERE `account_id` = ? AND `origin` = 'website_import' AND `source_id` LIKE ? + ]], { target_account_id, previous_account_id, options.media_pattern }) + occupied_accounts[previous_account_id] = nil + occupied_accounts[target_account_id] = profile + profile.account_id = target_account_id + profile.account_email = email_by_account[target_account_id] or "" + repaired = repaired + 1 + return true + end + + -- If the active LB profile changed since a previous import, first move the + -- old imported profile back to its deterministic cloud account. + for owner_account_id, preferred_entry in pairs(preferred_entry_by_owner) do + local occupant = occupied_accounts[owner_account_id] + if occupant and tostring(occupant.handle or ""):lower() ~= preferred_entry.handle then + local occupant_entry = entry_by_handle[tostring(occupant.handle or ""):lower()] + if occupant_entry + and tonumber(occupant_entry.source.owner_account_id) == owner_account_id + and is_imported_profile(occupant, occupant_entry) + then + move_profile(occupant, occupant_entry, occupant_entry.synthetic_account_id) + end + end + end + + local owner_assigned = {} + for owner_account_id, preferred_entry in pairs(preferred_entry_by_owner) do + local existing = existing_by_handle[preferred_entry.handle] + local occupant = occupied_accounts[owner_account_id] + if not occupant or occupant == existing then + if not existing + or tonumber(existing.account_id) == owner_account_id + or (is_imported_profile(existing, preferred_entry) + and move_profile(existing, preferred_entry, owner_account_id)) + then + owner_assigned[preferred_entry] = true + end + end + end + + local accepted = {} + local skipped = 0 + for index = 1, #entries do + local entry = entries[index] + local desired_account_id = owner_assigned[entry] + and tonumber(entry.source.owner_account_id) + or entry.synthetic_account_id + local existing = existing_by_handle[entry.handle] + if not desired_account_id then + skipped = skipped + 1 + elseif existing then + if tonumber(existing.account_id) == desired_account_id + and is_imported_profile(existing, entry) + then + entry.account_id = desired_account_id + accepted[#accepted + 1] = entry + else + skipped = skipped + 1 + end + elseif occupied_accounts[desired_account_id] then + skipped = skipped + 1 + else + entry.account_id = desired_account_id + accepted[#accepted + 1] = entry + occupied_accounts[desired_account_id] = { pending = true, handle = entry.handle } + end + end + return accepted, skipped, repaired +end + local function run_picstagram(dry_run) local accounts_table = source_table("instagram_accounts") if not table_exists(accounts_table) then @@ -1332,39 +1545,21 @@ local function run_picstagram(dry_run) FROM `%s` account JOIN `sky_phone_migration_owners` owner ON owner.`source` = ? AND owner.`source_phone_number` = account.`phone_number` + ORDER BY account.`date_joined`, account.`phone_number`, account.`username` ]]):format(accounts_table), { source_name }) - local existing_profiles = Bridge.Database.Query( - "SELECT `id`, `handle`, `account_id` FROM `sky_phone_picstagram_profiles`", {} - ) - local existing_by_handle = {} - for index = 1, #existing_profiles do - existing_by_handle[tostring(existing_profiles[index].handle):lower()] = existing_profiles[index] - end local entries = {} - local cloud_accounts = {} local skipped = 0 for index = 1, #source_accounts do local row = source_accounts[index] local handle = normalize_picstagram_handle(row.username) if handle then local profile_id = deterministic_uuid("lb-phone-picstagram-profile", handle) - local existing = existing_by_handle[handle] - if not existing or existing.id == profile_id then - local email = synthetic_email("ig", handle) - entries[#entries + 1] = { - source = row, - handle = handle, - profile_id = profile_id, - email = email, - } - cloud_accounts[#cloud_accounts + 1] = { - email, - deterministic_hex("lb-phone-picstagram-account", handle), - } - else - skipped = skipped + 1 - end + entries[#entries + 1] = { + source = row, + handle = handle, + profile_id = profile_id, + } else skipped = skipped + 1 end @@ -1381,17 +1576,19 @@ local function run_picstagram(dry_run) } end - insert_many("INSERT IGNORE INTO `sky_phone_accounts` (`email`, `password`) VALUES", 2, cloud_accounts) - local all_cloud_accounts = Bridge.Database.Query("SELECT `id`, `email` FROM `sky_phone_accounts`", {}) - local account_by_email = {} - for index = 1, #all_cloud_accounts do - account_by_email[tostring(all_cloud_accounts[index].email):lower()] = tonumber(all_cloud_accounts[index].id) - end + local accepted, assignment_skipped, ownership_repaired = prepare_social_profile_accounts(entries, { + profile_table = "sky_phone_picstagram_profiles", + synthetic_namespace = "ig", + account_namespace = "lb-phone-picstagram-account", + media_pattern = "lb-ig-%", + apps = { "instagram", "instapic", "picstagram" }, + }) + entries = accepted + skipped = skipped + assignment_skipped local avatar_rows = {} for index = 1, #entries do local entry = entries[index] - entry.account_id = account_by_email[entry.email] local avatar = valid_media_url(entry.source.profile_image) if entry.account_id and avatar then entry.avatar_source = "lb-ig-avatar:" .. entry.profile_id @@ -1430,7 +1627,7 @@ local function run_picstagram(dry_run) (entry.source.verified == true or tonumber(entry.source.verified) == 1) and 1 or 0, entry.source.migrated_at, } - profile_by_username[tostring(entry.source.username)] = entry + profile_by_username[tostring(entry.source.username):lower()] = entry end end insert_many([[ @@ -1454,7 +1651,7 @@ local function run_picstagram(dry_run) local session_candidates = {} local logged_table = source_table("logged_in_accounts") - if table_exists(logged_table) then + if social_login_table_available(logged_table) then local logged = Bridge.Database.Query(([[ SELECT login.`phone_number`, login.`username`, owner.`device_imei` FROM `%s` login @@ -1464,7 +1661,7 @@ local function run_picstagram(dry_run) AND LOWER(login.`app`) IN ('instagram', 'instapic', 'picstagram') ]]):format(logged_table), { source_name }) for index = 1, #logged do - local profile = profile_by_username[tostring(logged[index].username)] + local profile = profile_by_username[tostring(logged[index].username):lower()] if profile then session_candidates[tostring(logged[index].device_imei)] = profile.profile_id end @@ -1487,8 +1684,10 @@ local function run_picstagram(dry_run) session_rows[#session_rows + 1] = { imei, profile_id } end insert_many([[ - INSERT IGNORE INTO `sky_phone_picstagram_sessions` (`device_imei`, `profile_id`) VALUES - ]], 2, session_rows) + INSERT INTO `sky_phone_picstagram_sessions` (`device_imei`, `profile_id`) VALUES + ]], 2, session_rows, [[ + ON DUPLICATE KEY UPDATE `profile_id` = VALUES(`profile_id`), `updated_at` = CURRENT_TIMESTAMP + ]]) local posts_table = source_table("instagram_posts") local post_entries = {} @@ -1501,7 +1700,7 @@ local function run_picstagram(dry_run) ]]):format(posts_table), {}) for index = 1, #posts do local row = posts[index] - local profile = profile_by_username[tostring(row.username)] + local profile = profile_by_username[tostring(row.username):lower()] local media = extract_media(row.media, Config.Picstagram.MaxPostMedia) if profile and #media > 0 then local post_id = deterministic_uuid("lb-phone-picstagram-post", row.id) @@ -1572,7 +1771,7 @@ local function run_picstagram(dry_run) ]]):format(comments_table), {}) for index = 1, #rows do local post = post_entries[tostring(rows[index].post_id)] - local profile = profile_by_username[tostring(rows[index].username)] + local profile = profile_by_username[tostring(rows[index].username):lower()] if post and profile then local comment_id = deterministic_uuid("lb-phone-picstagram-comment", rows[index].id) comment_entries[tostring(rows[index].id)] = comment_id @@ -1597,7 +1796,7 @@ local function run_picstagram(dry_run) local post_likes = {} local comment_likes = {} for index = 1, #likes do - local profile = profile_by_username[tostring(likes[index].username)] + local profile = profile_by_username[tostring(likes[index].username):lower()] if profile and (likes[index].is_comment == true or tonumber(likes[index].is_comment) == 1) then local comment_id = comment_entries[tostring(likes[index].id)] if comment_id then @@ -1623,8 +1822,8 @@ local function run_picstagram(dry_run) if table_exists(follows_table) then local follows = Bridge.Database.Query(("SELECT * FROM `%s`"):format(follows_table), {}) for index = 1, #follows do - local follower = profile_by_username[tostring(follows[index].follower)] - local following = profile_by_username[tostring(follows[index].followed)] + local follower = profile_by_username[tostring(follows[index].follower):lower()] + local following = profile_by_username[tostring(follows[index].followed):lower()] if follower and following and follower.profile_id ~= following.profile_id then follow_rows[#follow_rows + 1] = { follower.profile_id, @@ -1644,8 +1843,8 @@ local function run_picstagram(dry_run) FROM `%s` request ]]):format(requests_table), {}) for index = 1, #requests do - local requester = profile_by_username[tostring(requests[index].requester)] - local requestee = profile_by_username[tostring(requests[index].requestee)] + local requester = profile_by_username[tostring(requests[index].requester):lower()] + local requestee = profile_by_username[tostring(requests[index].requestee):lower()] if requester and requestee and requester.profile_id ~= requestee.profile_id then follow_rows[#follow_rows + 1] = { requester.profile_id, requestee.profile_id, "pending", requests[index].migrated_at, @@ -1682,7 +1881,7 @@ local function run_picstagram(dry_run) FROM `%s` story ]]):format(stories_table), {}) for index = 1, #stories do - local profile = profile_by_username[tostring(stories[index].username)] + local profile = profile_by_username[tostring(stories[index].username):lower()] local url = valid_media_url(stories[index].image) if profile and url then local story_id = deterministic_uuid("lb-phone-picstagram-story", stories[index].id) @@ -1733,7 +1932,7 @@ local function run_picstagram(dry_run) ]]):format(views_table), {}) for index = 1, #views do local story = story_entries[tostring(views[index].story_id)] - local viewer = profile_by_username[tostring(views[index].viewer)] + local viewer = profile_by_username[tostring(views[index].viewer):lower()] if story and viewer then view_rows[#view_rows + 1] = { story.id, viewer.profile_id, views[index].migrated_at } end @@ -1753,6 +1952,7 @@ local function run_picstagram(dry_run) follows = #follow_rows, stories = count_rows(stories_table), story_views = #view_rows, + ownership_repaired = ownership_repaired, skipped = skipped, unsupported_messages = count_rows(source_table("instagram_messages")), unsupported_notifications = count_rows(source_table("instagram_notifications")), @@ -2230,6 +2430,7 @@ local function run_flip_tok(dry_run) FROM `%s` account JOIN `sky_phone_migration_owners` owner ON owner.`source` = ? AND owner.`source_phone_number` = account.`phone_number` + ORDER BY account.`date_joined`, account.`phone_number`, account.`username` ]]):format(accounts_table), { source_name }) local entries = {} local skipped = 0 @@ -2240,7 +2441,6 @@ local function run_flip_tok(dry_run) entries[#entries + 1] = { source = row, handle = handle, - email = synthetic_email("flip", handle), } else skipped = skipped + 1 @@ -2257,54 +2457,34 @@ local function run_flip_tok(dry_run) } end - local cloud_rows = {} - for index = 1, #entries do - cloud_rows[#cloud_rows + 1] = { - entries[index].email, - deterministic_hex("lb-phone-fliptok-account", entries[index].handle), - } - end - insert_many("INSERT IGNORE INTO `sky_phone_accounts` (`email`, `password`) VALUES", 2, cloud_rows) - local target_accounts = Bridge.Database.Query("SELECT `id`, `email` FROM `sky_phone_accounts`", {}) - local account_by_email = {} - for index = 1, #target_accounts do - account_by_email[tostring(target_accounts[index].email):lower()] = tonumber(target_accounts[index].id) - end - local target_profiles = Bridge.Database.Query( - "SELECT `id`, `account_id`, `handle` FROM `sky_phone_fliptok_profiles`", {} - ) - local existing_by_handle = {} - for index = 1, #target_profiles do - existing_by_handle[tostring(target_profiles[index].handle):lower()] = target_profiles[index] - end + local accepted, assignment_skipped, ownership_repaired = prepare_social_profile_accounts(entries, { + profile_table = "sky_phone_fliptok_profiles", + synthetic_namespace = "flip", + account_namespace = "lb-phone-fliptok-account", + media_pattern = "lb-flip-%", + apps = { "tiktok", "fliptok" }, + }) + skipped = skipped + assignment_skipped - local accepted = {} local avatar_rows = {} - for index = 1, #entries do - local entry = entries[index] - entry.account_id = account_by_email[entry.email] - local existing = existing_by_handle[entry.handle] - if entry.account_id and (not existing or tonumber(existing.account_id) == entry.account_id) then - accepted[#accepted + 1] = entry - local avatar = valid_media_url(entry.source.avatar) - if avatar then - entry.avatar_source = "lb-flip-avatar:" .. deterministic_uuid( - "lb-phone-fliptok-avatar", - entry.handle - ) - avatar_rows[#avatar_rows + 1] = { - entry.account_id, - avatar, - entry.avatar_source, - "photo", - "website_import", - entry.avatar_source, - entry.source.migrated_at, - entry.source.migrated_at, - } - end - else - skipped = skipped + 1 + for index = 1, #accepted do + local entry = accepted[index] + local avatar = valid_media_url(entry.source.avatar) + if avatar then + entry.avatar_source = "lb-flip-avatar:" .. deterministic_uuid( + "lb-phone-fliptok-avatar", + entry.handle + ) + avatar_rows[#avatar_rows + 1] = { + entry.account_id, + avatar, + entry.avatar_source, + "photo", + "website_import", + entry.avatar_source, + entry.source.migrated_at, + entry.source.migrated_at, + } end end insert_many([[ @@ -2330,7 +2510,7 @@ local function run_flip_tok(dry_run) INSERT IGNORE INTO `sky_phone_fliptok_profiles` (`account_id`, `handle`, `display_name`, `bio`, `avatar_media_id`, `verified`, `created_at`) VALUES ]], 7, profile_rows) - target_profiles = Bridge.Database.Query( + local target_profiles = Bridge.Database.Query( "SELECT `id`, `account_id`, `handle` FROM `sky_phone_fliptok_profiles`", {} ) local profile_by_handle = {} @@ -2360,7 +2540,7 @@ local function run_flip_tok(dry_run) local session_candidates = {} local logged_table = source_table("logged_in_accounts") - if table_exists(logged_table) then + if social_login_table_available(logged_table) then local logged = Bridge.Database.Query(([[ SELECT login.`username`, owner.`device_imei` FROM `%s` login @@ -2394,8 +2574,10 @@ local function run_flip_tok(dry_run) end end insert_many([[ - INSERT IGNORE INTO `sky_phone_fliptok_sessions` (`device_imei`, `profile_id`) VALUES - ]], 2, session_rows) + INSERT INTO `sky_phone_fliptok_sessions` (`device_imei`, `profile_id`) VALUES + ]], 2, session_rows, [[ + ON DUPLICATE KEY UPDATE `profile_id` = VALUES(`profile_id`), `updated_at` = CURRENT_TIMESTAMP + ]]) local videos_table = source_table("tiktok_videos") local video_entries = {} @@ -2603,6 +2785,7 @@ local function run_flip_tok(dry_run) follows = #follows, reactions = #reactions, notifications = #notification_rows, + ownership_repaired = ownership_repaired, skipped = skipped, password_policy_mismatches = policy_passwords, unsupported_messages = count_rows(source_table("tiktok_messages")), @@ -2621,10 +2804,9 @@ local function run_feather(dry_run) FROM `%s` account JOIN `sky_phone_migration_owners` owner ON owner.`source` = ? AND owner.`source_phone_number` = account.`phone_number` - ORDER BY account.`phone_number`, account.`date_joined`, account.`username` + ORDER BY account.`date_joined`, account.`phone_number`, account.`username` ]]):format(accounts_table), { source_name }) local entries = {} - local entry_by_source_username = {} local skipped = 0 for index = 1, #source_accounts do local row = source_accounts[index] @@ -2632,7 +2814,6 @@ local function run_feather(dry_run) if handle then local entry = { source = row, handle = handle } entries[#entries + 1] = entry - entry_by_source_username[tostring(row.username)] = entry else skipped = skipped + 1 end @@ -2650,96 +2831,34 @@ local function run_feather(dry_run) } end - local preferred = {} - local logged_table = source_table("logged_in_accounts") - if table_exists(logged_table) then - local logged = Bridge.Database.Query(([[ - SELECT login.`username`, owner.`account_id` - FROM `%s` login - JOIN `sky_phone_migration_owners` owner - ON owner.`source` = ? AND owner.`source_phone_number` = login.`phone_number` - WHERE login.`active` = 1 AND LOWER(login.`app`) IN ('twitter', 'feather') - ]]):format(logged_table), { source_name }) - for index = 1, #logged do - local account_id = tonumber(logged[index].account_id) - if account_id then - preferred[account_id] = tostring(logged[index].username):lower() - end - end - end - for index = 1, #entries do - local owner_account_id = tonumber(entries[index].source.owner_account_id) - if owner_account_id then - preferred[owner_account_id] = preferred[owner_account_id] - or tostring(entries[index].source.username):lower() - end - end + local accepted, assignment_skipped, ownership_repaired = prepare_social_profile_accounts(entries, { + profile_table = "sky_phone_feather_profiles", + synthetic_namespace = "feather", + account_namespace = "lb-phone-feather-account", + media_pattern = "lb-feather-%", + apps = { "twitter", "feather" }, + }) + skipped = skipped + assignment_skipped - local target_profiles = Bridge.Database.Query( - "SELECT `id`, `account_id`, `handle` FROM `sky_phone_feather_profiles`", {} - ) - local occupied_accounts = {} - local existing_by_handle = {} - for index = 1, #target_profiles do - occupied_accounts[tonumber(target_profiles[index].account_id)] = target_profiles[index] - existing_by_handle[tostring(target_profiles[index].handle):lower()] = target_profiles[index] - end - local cloud_rows = {} - for index = 1, #entries do - local entry = entries[index] - local owner_account_id = tonumber(entry.source.owner_account_id) - local existing = existing_by_handle[entry.handle] - local preferred_for_owner = owner_account_id - and preferred[owner_account_id] == tostring(entry.source.username):lower() - local may_use_owner = preferred_for_owner and ( - not occupied_accounts[owner_account_id] - or (existing and tonumber(existing.account_id) == owner_account_id) - ) - if may_use_owner then - entry.account_id = owner_account_id - occupied_accounts[owner_account_id] = occupied_accounts[owner_account_id] or true - else - entry.email = synthetic_email("feather", entry.handle) - cloud_rows[#cloud_rows + 1] = { - entry.email, - deterministic_hex("lb-phone-feather-account", entry.handle), - } - end - end - insert_many("INSERT IGNORE INTO `sky_phone_accounts` (`email`, `password`) VALUES", 2, cloud_rows) - local target_accounts = Bridge.Database.Query("SELECT `id`, `email` FROM `sky_phone_accounts`", {}) - local account_by_email = {} - for index = 1, #target_accounts do - account_by_email[tostring(target_accounts[index].email):lower()] = tonumber(target_accounts[index].id) - end - - local accepted = {} local avatar_rows = {} - for index = 1, #entries do - local entry = entries[index] - entry.account_id = entry.account_id or account_by_email[entry.email] - local existing = existing_by_handle[entry.handle] - if entry.account_id and (not existing or tonumber(existing.account_id) == entry.account_id) then - accepted[#accepted + 1] = entry - local avatar = valid_media_url(entry.source.profile_image) - if avatar then - entry.avatar_source = "lb-feather-avatar:" .. deterministic_uuid( - "lb-phone-feather-avatar", - entry.handle - ) - avatar_rows[#avatar_rows + 1] = { - entry.account_id, - avatar, - entry.avatar_source, - "photo", - "website_import", - entry.avatar_source, - entry.source.migrated_at, - entry.source.migrated_at, - } - end - else - skipped = skipped + 1 + for index = 1, #accepted do + local entry = accepted[index] + local avatar = valid_media_url(entry.source.profile_image) + if avatar then + entry.avatar_source = "lb-feather-avatar:" .. deterministic_uuid( + "lb-phone-feather-avatar", + entry.handle + ) + avatar_rows[#avatar_rows + 1] = { + entry.account_id, + avatar, + entry.avatar_source, + "photo", + "website_import", + entry.avatar_source, + entry.source.migrated_at, + entry.source.migrated_at, + } end end insert_many([[ @@ -2766,7 +2885,7 @@ local function run_feather(dry_run) INSERT IGNORE INTO `sky_phone_feather_profiles` (`account_id`, `handle`, `display_name`, `bio`, `avatar_media_id`, `verified`, `created_at`) VALUES ]], 7, profile_rows) - target_profiles = Bridge.Database.Query( + local target_profiles = Bridge.Database.Query( "SELECT `id`, `account_id`, `handle` FROM `sky_phone_feather_profiles`", {} ) local profile_by_handle = {} @@ -2999,6 +3118,7 @@ local function run_feather(dry_run) likes = #reaction_rows, follows = #follow_rows, notifications = #notification_rows, + ownership_repaired = ownership_repaired, skipped = skipped, unsupported_private_profiles = private_profiles, unsupported_follow_requests = count_rows(source_table("twitter_follow_requests")), @@ -3007,6 +3127,245 @@ local function run_feather(dry_run) } end +local function run_flare(dry_run) + local accounts_table = source_table("tinder_accounts") + if not table_exists(accounts_table) then + return { source = 0, profiles = 0, profile_photos = 0, swipes = 0, matches = 0, skipped = 0 } + end + + local source_accounts = Bridge.Database.Query(([[ + SELECT account.*, owner.`account_id` AS `owner_account_id`, + TIMESTAMPDIFF(YEAR, account.`dob`, CURDATE()) AS `age`, + DATE_FORMAT(account.`last_seen`, '%%Y-%%m-%%d %%H:%%i:%%s') AS `migrated_at` + FROM `%s` account + JOIN `sky_phone_migration_owners` owner + ON owner.`source` = ? AND owner.`source_phone_number` = account.`phone_number` + ORDER BY account.`phone_number` + ]]):format(accounts_table), { source_name }) + + local entries = {} + local photo_count = 0 + local skipped = 0 + for index = 1, #source_accounts do + local row = source_accounts[index] + local account_id = tonumber(row.owner_account_id) + local name = clamp_text(row.name, 32):match("^%s*(.-)%s*$") + if #name < 2 then + name = "LB User" + end + local age = math.max(18, math.min(99, math.floor(tonumber(row.age) or 18))) + local interested_men = row.interested_men == true or tonumber(row.interested_men) == 1 + local interested_women = row.interested_women == true or tonumber(row.interested_women) == 1 + local interested_in = interested_men and not interested_women and "man" + or interested_women and not interested_men and "woman" + or "everyone" + if account_id then + local photos = extract_media(row.photos, 6) + local photo_entries = {} + for position = 1, #photos do + if photos[position].media_type == "photo" and photos[position].url:match("^https://") then + photo_entries[#photo_entries + 1] = { + url = photos[position].url, + source_id = "lb-flare-photo:" .. deterministic_uuid( + "lb-phone-flare-photo", + tostring(row.phone_number) .. ":" .. tostring(position) + ), + } + end + end + photo_count = photo_count + #photo_entries + entries[#entries + 1] = { + source = row, + account_id = account_id, + name = name, + age = age, + bio = clamp_text(row.bio, 300), + gender = (row.is_male == true or tonumber(row.is_male) == 1) and "man" or "woman", + interested_in = interested_in, + discoverable = (row.active == true or tonumber(row.active) == 1) and 1 or 0, + photos = photo_entries, + } + else + skipped = skipped + 1 + end + end + + local swipes_table = source_table("tinder_swipes") + local source_swipes = {} + if table_exists(swipes_table) then + source_swipes = Bridge.Database.Query(([[ + SELECT swipe.*, swiper_owner.`account_id` AS `swiper_account_id`, + target_owner.`account_id` AS `target_account_id` + FROM `%s` swipe + JOIN `sky_phone_migration_owners` swiper_owner + ON swiper_owner.`source` = ? AND swiper_owner.`source_phone_number` = swipe.`swiper` + JOIN `sky_phone_migration_owners` target_owner + ON target_owner.`source` = ? AND target_owner.`source_phone_number` = swipe.`swipee` + ]]):format(swipes_table), { source_name, source_name }) + end + if dry_run then + return { + source = #source_accounts, + profiles = #entries, + profile_photos = photo_count, + swipes = #source_swipes, + skipped = skipped, + } + end + + local existing_profiles = Bridge.Database.Query( + "SELECT `account_id` FROM `sky_phone_flare_profiles`", {} + ) + local existing_profile_accounts = {} + for index = 1, #existing_profiles do + existing_profile_accounts[tonumber(existing_profiles[index].account_id)] = true + end + for index = 1, #entries do + entries[index].import_profile = not existing_profile_accounts[entries[index].account_id] + end + + local media_rows = {} + for index = 1, #entries do + local entry = entries[index] + if entry.import_profile then + for position = 1, #entry.photos do + local photo = entry.photos[position] + media_rows[#media_rows + 1] = { + entry.account_id, + photo.url, + photo.source_id, + "photo", + "website_import", + photo.source_id, + entry.source.migrated_at, + entry.source.migrated_at, + } + end + end + end + insert_many([[ + INSERT IGNORE INTO `sky_phone_media` + (`account_id`, `url`, `remote_id`, `media_type`, `origin`, `source_id`, + `verified_at`, `created_at`) VALUES + ]], 8, media_rows) + + local profile_rows = {} + for index = 1, #entries do + local entry = entries[index] + if entry.import_profile then + profile_rows[#profile_rows + 1] = { + entry.account_id, + entry.name, + entry.age, + entry.bio, + entry.gender, + entry.interested_in, + 18, + 99, + 0, + json.encode({}), + "longTerm", + entry.discoverable, + entry.source.migrated_at, + entry.source.migrated_at, + } + end + end + local inserted_profiles = insert_many([[ + INSERT IGNORE INTO `sky_phone_flare_profiles` + (`account_id`, `name`, `age`, `bio`, `gender`, `interested_in`, `min_age`, `max_age`, + `avatar`, `interests`, `looking_for`, `discoverable`, `created_at`, `updated_at`) VALUES + ]], 14, profile_rows) + + local target_profiles = Bridge.Database.Query( + "SELECT `id`, `account_id` FROM `sky_phone_flare_profiles`", {} + ) + local profile_by_account = {} + for index = 1, #target_profiles do + profile_by_account[tonumber(target_profiles[index].account_id)] = tonumber(target_profiles[index].id) + end + local media_ids = load_media_ids("lb-flare-photo:%") + local photo_rows = {} + for index = 1, #entries do + local entry = entries[index] + local profile_id = profile_by_account[entry.account_id] + if profile_id and entry.import_profile then + for position = 1, #entry.photos do + local media_id = media_ids[entry.photos[position].source_id] + if media_id then + photo_rows[#photo_rows + 1] = { profile_id, media_id, position } + end + end + end + end + insert_many([[ + INSERT IGNORE INTO `sky_phone_flare_profile_photos` + (`profile_id`, `media_id`, `sort_order`) VALUES + ]], 3, photo_rows) + + local swipe_rows = {} + local liked_pairs = {} + for index = 1, #source_swipes do + local row = source_swipes[index] + local swiper_account_id = tonumber(row.swiper_account_id) + local target_account_id = tonumber(row.target_account_id) + if swiper_account_id and target_account_id and swiper_account_id ~= target_account_id + and profile_by_account[swiper_account_id] and profile_by_account[target_account_id] + then + local liked = row.liked == true or tonumber(row.liked) == 1 + swipe_rows[#swipe_rows + 1] = { + swiper_account_id, + target_account_id, + liked and "like" or "pass", + } + if liked then + liked_pairs[tostring(swiper_account_id) .. ":" .. tostring(target_account_id)] = true + end + else + skipped = skipped + 1 + end + end + insert_many([[ + INSERT IGNORE INTO `sky_phone_flare_swipes` + (`swiper_account_id`, `target_account_id`, `choice`) VALUES + ]], 3, swipe_rows) + + local match_rows = {} + local matched_pairs = {} + for key in pairs(liked_pairs) do + local first, second = key:match("^(%d+):(%d+)$") + local first_id = tonumber(first) + local second_id = tonumber(second) + if first_id and second_id + and liked_pairs[tostring(second_id) .. ":" .. tostring(first_id)] + then + local account_a_id = math.min(first_id, second_id) + local account_b_id = math.max(first_id, second_id) + local pair_key = tostring(account_a_id) .. ":" .. tostring(account_b_id) + if not matched_pairs[pair_key] then + matched_pairs[pair_key] = true + match_rows[#match_rows + 1] = { + deterministic_uuid("lb-phone-flare-match", pair_key), + account_a_id, + account_b_id, + } + end + end + end + insert_many([[ + INSERT IGNORE INTO `sky_phone_flare_matches` (`id`, `account_a_id`, `account_b_id`) VALUES + ]], 3, match_rows) + + return { + source = #source_accounts, + profiles = inserted_profiles, + profile_photos = #photo_rows, + swipes = #swipe_rows, + matches = #match_rows, + skipped = skipped, + } +end + local domain_order = { "devices", "settings", @@ -3025,6 +3384,7 @@ local domain_order = { "darkChat", "flipTok", "feather", + "flare", } local domain_runners = { @@ -3045,6 +3405,7 @@ local domain_runners = { darkChat = run_dark_chat, flipTok = run_flip_tok, feather = run_feather, + flare = run_flare, } local preview_tables = { @@ -3089,6 +3450,7 @@ local preview_tables = { "twitter_follows", "twitter_notifications", }, + flare = { "tinder_accounts", "tinder_swipes" }, } local function preview_domain(domain) @@ -3120,12 +3482,17 @@ local domain_labels = { darkChat = "DarkChat", flipTok = "FlipTok", feather = "Feather", + flare = "Flare", } local summary_metrics = { { "imported", "imported" }, { "resolved", "matched" }, { "profiles", "profiles" }, + { "ownership_repaired", "ownership repairs" }, + { "profile_photos", "profile photos" }, + { "swipes", "swipes" }, + { "matches", "matches" }, { "sessions", "sessions" }, { "posts", "posts" }, { "post_media", "media" }, @@ -3450,6 +3817,56 @@ local function rollback_lb_phone() end stats.picstagram = delete_values("sky_phone_picstagram_profiles", "id", picstagram_profiles) + local flare_match_ids = {} + local flare_swipe_rows = rollback_source_rows("tinder_swipes", { "swiper", "swipee", "liked" }) + local liked_flare_pairs = {} + local removed_flare_swipes = 0 + for index = 1, #flare_swipe_rows do + local swiper_owner = owner_by_number[tostring(flare_swipe_rows[index].swiper)] + local target_owner = owner_by_number[tostring(flare_swipe_rows[index].swipee)] + local swiper_account_id = swiper_owner and tonumber(swiper_owner.account_id) + local target_account_id = target_owner and tonumber(target_owner.account_id) + if swiper_account_id and target_account_id and swiper_account_id ~= target_account_id then + removed_flare_swipes = removed_flare_swipes + affected_rows(Bridge.Database.Query([[ + DELETE FROM `sky_phone_flare_swipes` + WHERE `swiper_account_id` = ? AND `target_account_id` = ? + ]], { swiper_account_id, target_account_id })) + if flare_swipe_rows[index].liked == true or tonumber(flare_swipe_rows[index].liked) == 1 then + liked_flare_pairs[tostring(swiper_account_id) .. ":" .. tostring(target_account_id)] = true + end + end + end + for key in pairs(liked_flare_pairs) do + local first, second = key:match("^(%d+):(%d+)$") + local first_id = tonumber(first) + local second_id = tonumber(second) + if first_id and second_id + and liked_flare_pairs[tostring(second_id) .. ":" .. tostring(first_id)] + and first_id < second_id + then + flare_match_ids[#flare_match_ids + 1] = deterministic_uuid( + "lb-phone-flare-match", + tostring(first_id) .. ":" .. tostring(second_id) + ) + end + end + stats.flareMatches = delete_values("sky_phone_flare_matches", "id", flare_match_ids) + stats.flareSwipes = removed_flare_swipes + + local flare_accounts = rollback_source_rows("tinder_accounts", { "phone_number", "last_seen" }) + local removed_flare_profiles = 0 + for index = 1, #flare_accounts do + local owner = owner_by_number[tostring(flare_accounts[index].phone_number)] + local account_id = owner and tonumber(owner.account_id) + if account_id and flare_accounts[index].last_seen then + removed_flare_profiles = removed_flare_profiles + affected_rows(Bridge.Database.Query([[ + DELETE FROM `sky_phone_flare_profiles` + WHERE `account_id` = ? AND `created_at` = ? AND `updated_at` = ? + ]], { account_id, flare_accounts[index].last_seen, flare_accounts[index].last_seen })) + end + end + stats.flareProfiles = removed_flare_profiles + local feather_post_ids = {} local tweets = rollback_source_rows("twitter_tweets", { "id" }) for index = 1, #tweets do