From 39f5511f24ee930713161125ce424b30d0abcd58 Mon Sep 17 00:00:00 2001 From: SeoFood Date: Wed, 7 Oct 2026 17:38:23 +0200 Subject: [PATCH 1/7] Add the Android beta page, Android privacy details and account deletion - /android/ explains how to join the closed beta: Google Group first, then the Play opt-in. Both links live in src/lib/android-beta.ts. - Release status lists Android as Beta; the footer links the beta page. - The privacy policy gains an Android section (on-device speech, cloud engines and writing profiles, keyboard, storage, Premium, no tracking), an account deletion section and the contact email. Last updated: October 2026. - /delete-account/ describes how to delete a Premium account in the app or by email, and what is and is not deleted, as Google Play requires. - check-i18n now also checks the privacy, Android and account deletion chunks. --- scripts/check-i18n.mjs | 3 + src/components/layout/footer.astro | 4 + src/i18n/locales/de/android.json | 37 ++++++ src/i18n/locales/de/common.json | 13 ++- src/i18n/locales/de/delete-account.json | 29 +++++ src/i18n/locales/de/index.ts | 4 + src/i18n/locales/de/privacy.json | 28 ++++- src/i18n/locales/en/android.json | 37 ++++++ src/i18n/locales/en/common.json | 13 ++- src/i18n/locales/en/delete-account.json | 29 +++++ src/i18n/locales/en/index.ts | 4 + src/i18n/locales/en/privacy.json | 28 ++++- src/lib/android-beta.ts | 9 ++ src/pages/[locale]/android.astro | 143 ++++++++++++++++++++++++ src/pages/[locale]/delete-account.astro | 75 +++++++++++++ src/pages/[locale]/privacy.astro | 33 +++++- src/pages/[locale]/release-status.astro | 22 +++- src/pages/android.astro | 4 + src/pages/delete-account.astro | 4 + src/styles/utility.css | 5 + tests/android-beta.spec.ts | 100 +++++++++++++++++ tests/android-beta.test.ts | 29 +++++ tests/client-messages.spec.ts | 2 + 23 files changed, 635 insertions(+), 20 deletions(-) create mode 100644 src/i18n/locales/de/android.json create mode 100644 src/i18n/locales/de/delete-account.json create mode 100644 src/i18n/locales/en/android.json create mode 100644 src/i18n/locales/en/delete-account.json create mode 100644 src/lib/android-beta.ts create mode 100644 src/pages/[locale]/android.astro create mode 100644 src/pages/[locale]/delete-account.astro create mode 100644 src/pages/android.astro create mode 100644 src/pages/delete-account.astro create mode 100644 tests/android-beta.spec.ts create mode 100644 tests/android-beta.test.ts diff --git a/scripts/check-i18n.mjs b/scripts/check-i18n.mjs index 510c4594..9d57c989 100644 --- a/scripts/check-i18n.mjs +++ b/scripts/check-i18n.mjs @@ -14,6 +14,9 @@ const requiredChunks = [ "support.json", "sponsors.json", "open-source-accessibility.json", + "privacy.json", + "android.json", + "delete-account.json", "docs/common.json", "docs/mac.json", ]; diff --git a/src/components/layout/footer.astro b/src/components/layout/footer.astro index 7ba16a46..be855546 100644 --- a/src/components/layout/footer.astro +++ b/src/components/layout/footer.astro @@ -139,6 +139,9 @@ const community = [ data-tracking-placement="footer">{t(locale, "footer.ios")} +
  • + {t(locale, "footer.android")} +
  • @@ -167,6 +170,7 @@ const community = [
  • {t(locale, "footer.privacy")}
  • {t(locale, "footer.terms")}
  • {t(locale, "footer.legalNotice")}
  • +
  • {t(locale, "footer.deleteAccount")}
  • diff --git a/src/i18n/locales/de/android.json b/src/i18n/locales/de/android.json new file mode 100644 index 00000000..4e229bf0 --- /dev/null +++ b/src/i18n/locales/de/android.json @@ -0,0 +1,37 @@ +{ + "android.meta.title": "TypeWhisper für Android (Beta)", + "android.meta.description": "Mach mit bei der TypeWhisper-Beta für Android: private Spracherkennung mit KI auf dem Gerät, einer Diktier-Tastatur und Schreibprofilen.", + "android.label": "Beta", + "android.title": "TypeWhisper für Android", + "android.lede": "Private Spracherkennung für Android mit KI auf dem Gerät, einer Diktier-Tastatur und Schreibprofilen.", + "android.requirement": "Android 10 oder neuer", + "android.join.label": "Bei der Beta mitmachen", + "android.join.title": "So machst du bei der Beta mit", + "android.join.lede": "Die Beta läuft über Google Play. Zwei Schritte, danach kommen Updates wie bei jeder anderen App.", + "android.join.step1.title": "Tritt der Google-Gruppe bei", + "android.join.step1.text": "Jedes Google-Konto funktioniert. Die Gruppe dient nur als Testerliste; dort werden keine E-Mails verschickt.", + "android.join.step1.button": "Google-Gruppe beitreten", + "android.join.step2.title": "Melde dich an und installiere aus Google Play", + "android.join.step2.text": "Öffne die Testseite mit dem Google-Konto, mit dem du der Gruppe beigetreten bist, tippe auf Tester werden und installiere TypeWhisper dann aus Google Play. Nach dem Beitritt zur Gruppe kann es ein paar Minuten dauern, bis die Seite dich hereinlässt.", + "android.join.step2.button": "Bei Google Play anmelden", + "android.try.label": "Was du ausprobieren kannst", + "android.try.title": "Was du ausprobieren kannst", + "android.try.lede": "Alles hier ist in der Beta enthalten. Probier aus, was du jeden Tag nutzen würdest.", + "android.try.item1": "Diktieren auf dem Gerät mit Parakeet (25 europäische Sprachen) oder Whisper (99 Sprachen).", + "android.try.item2": "Optionale Cloud-Spracherkennung mit deinem eigenen API-Schlüssel.", + "android.try.item3": "Die TypeWhisper-Tastatur in jeder App, mit QWERTZ- oder QWERTY-Layout.", + "android.try.item4": "Schreibprofile: Aufräumen, Nachricht, E-Mail, Notiz, Kalendertermin, Checkliste, Zusammenfassung, Übersetzung oder eigene Profile.", + "android.try.item5": "Die Inbox für schnelle Diktate über die Schnelleinstellungs-Kachel, das Widget, eine App-Verknüpfung oder die Assistenten-Geste.", + "android.try.item6": "Dateien transkribieren, mit Untertiteln.", + "android.try.item7": "Premium: die Profile für E-Mail und Kalender, Live-Transkription mit Nemotron und Sprechererkennung.", + "android.notice.label": "Hinweis zur Beta", + "android.notice.title": "Hinweis zur Beta", + "android.notice.p1": "Das ist eine Beta. Rechne mit Fehlern und unfertigen Stellen.", + "android.notice.p2": "Bitte bleib mindestens 14 Tage angemeldet. Google erlaubt die öffentliche Veröffentlichung erst, wenn 12 Tester 14 Tage lang in der Beta geblieben sind.", + "android.notice.p3": "Feedback kannst du in der App unter Einstellungen → Feedback senden, auf Discord oder per E-Mail schicken.", + "android.notice.discord": "Discord", + "android.notice.discordText": "Fragen, Fehler und Ideen mit anderen Testern.", + "android.notice.emailText": "Fehlerberichte und alles, was du nicht öffentlich posten möchtest.", + "android.requirements.title": "Voraussetzungen", + "android.requirements.text": "Android 10 oder neuer und ein Google-Konto für Google Play." +} diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index bb26339a..14c05197 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -49,6 +49,7 @@ "footer.changelog": "Changelog", "footer.windows": "Windows", "footer.ios": "iOS", + "footer.android": "Android-Beta", "footer.discord": "Discord", "footer.github": "GitHub", "footer.kofi": "Ko-fi", @@ -56,10 +57,11 @@ "footer.privacy": "Datenschutz", "footer.terms": "Nutzungsbedingungen", "footer.legalNotice": "Impressum", + "footer.deleteAccount": "Konto löschen", "madeInGermany.label": "Made in Germany", "madeInGermany.craft": "Unabhängig in Deutschland entwickelt", "releaseStage.stable": "Stabil", - "releaseStage.beta": "Vorschau", + "releaseStage.beta": "Beta", "releaseStage.alpha": "Alpha", "releaseStage.pending": "App-Store-Release in Vorbereitung", "benchmark.title": "Benchmark - TypeWhisper", @@ -67,8 +69,8 @@ "changelog.title": "Changelog - TypeWhisper", "changelog.description": "Release Notes und Updates für TypeWhisper.", "releaseStatus.title": "Release-Status", - "releaseStatus.meta.description": "Aktuelle Release-Stufen für TypeWhisper auf macOS, Windows und iOS.", - "releaseStatus.subtitle": "TypeWhisper bietet stabile Editionen für macOS, Windows, iPhone und iPad.", + "releaseStatus.meta.description": "Aktuelle Release-Stufen für TypeWhisper auf macOS, Windows, iOS und Android.", + "releaseStatus.subtitle": "TypeWhisper bietet stabile Editionen für macOS, Windows, iPhone und iPad. Android ist in der Beta.", "releaseStatus.mac.intro": "macOS {macSeries} ist das unterstützte stabile Release.", "releaseStatus.mac.detail": "Dies ist der offizielle stabile Pfad für täglichen Gebrauch, Dokumentation, Support und Launch-Kommunikation.", "releaseStatus.win.intro": "Windows ist ein stabiles Desktop-Release.", @@ -88,6 +90,11 @@ "releaseStatus.ios.description": "Private Sprache-zu-Text mit lokalen Engines, Diktier-Tastatur, Profilen, Dateien, Kurzbefehlen, Wörterbuch, Textbausteine und Apple-Watch-Unterstützung. Version 1.1 bringt lokale Schreibmodelle, Wörterbuchimporte mit Vorschau und Aufnahmesteuerungen für das Kontrollzentrum und die Aktionstaste.", "releaseStatus.ios.download": "Im App Store laden", "releaseStatus.ios.docs": "iOS-Dokumentation lesen", + "releaseStatus.android.title": "Android", + "releaseStatus.android.intro": "Android ist in einer geschlossenen Beta bei Google Play.", + "releaseStatus.android.detail": "Tester bekommen Diktieren auf dem Gerät, die Diktier-Tastatur, Schreibprofile und die Inbox vor der öffentlichen Veröffentlichung. Rechne mit Fehlern.", + "releaseStatus.android.description": "Benötigt Android 10 oder neuer. Jeder mit einem Google-Konto kann bei der Beta mitmachen.", + "releaseStatus.android.join": "Bei der Android-Beta mitmachen", "notFound.title": "404", "notFound.heading": "Seite nicht gefunden", "notFound.description": "Die Seite, die du suchst, existiert nicht oder wurde verschoben.", diff --git a/src/i18n/locales/de/delete-account.json b/src/i18n/locales/de/delete-account.json new file mode 100644 index 00000000..b5f0c97c --- /dev/null +++ b/src/i18n/locales/de/delete-account.json @@ -0,0 +1,29 @@ +{ + "deleteAccount.meta.title": "TypeWhisper-Konto löschen - TypeWhisper", + "deleteAccount.meta.description": "So löschst du dein TypeWhisper-Premium-Konto auf Android, iPhone, iPad und Mac, und das wird dabei gelöscht.", + "deleteAccount.heading": "TypeWhisper-Konto löschen", + "deleteAccount.lede": "Du kannst dein TypeWhisper-Premium-Konto in der App oder per E-Mail löschen.", + "deleteAccount.section.who.title": "Für wen diese Seite ist", + "deleteAccount.section.who.p1": "Diese Seite gilt für TypeWhisper-Premium-Konten auf Android, iPhone, iPad und Mac. Ein solches Konto hast du, wenn du dich für Premium mit Google oder Apple angemeldet hast. TypeWhisper funktioniert ohne Konto; hast du dich nie angemeldet, gibt es nichts zu löschen.", + "deleteAccount.section.inApp.title": "In der App löschen", + "deleteAccount.section.inApp.p1": "Das ist der schnellste Weg.", + "deleteAccount.section.inApp.item1": "Android: Einstellungen → Premium → Konto löschen.", + "deleteAccount.section.inApp.item2": "iPhone, iPad und Mac: in den Premium-Einstellungen der App.", + "deleteAccount.section.email.title": "Per E-Mail löschen", + "deleteAccount.section.email.p1": "Wenn du die App nicht nutzen kannst, schreib eine E-Mail an hello@typewhisper.com mit dem Betreff „Delete my TypeWhisper account“. Schreib von der E-Mail-Adresse des Google- oder Apple-Kontos, mit dem du dich angemeldet hast, und gib an, ob du Google oder Apple genutzt hast.", + "deleteAccount.section.deleted.title": "Was gelöscht wird", + "deleteAccount.section.deleted.p1": "Beim Löschen entfernt der TypeWhisper-Premium-Dienst:", + "deleteAccount.section.deleted.item1": "Dein TypeWhisper-Konto", + "deleteAccount.section.deleted.item2": "Die damit verbundenen Geräte", + "deleteAccount.section.deleted.item3": "Deine Premium-Berechtigungen", + "deleteAccount.section.deleted.item4": "Die Einträge zu deinen Käufen über Google Play", + "deleteAccount.section.deleted.item5": "Deine Anmeldesitzungen", + "deleteAccount.section.deleted.p2": "Ist eine Polar-Lizenz von typewhisper.com mit dem Konto verknüpft, werden ihre Aktivierungen für dieses Konto deaktiviert.", + "deleteAccount.section.kept.title": "Was nicht gelöscht wird", + "deleteAccount.section.kept.item1": "Daten auf deinen Geräten, etwa Verlauf, Wörterbuch, Profile und Aufnahmen, bleiben, bis du sie in der App löschst. Synchronisierte Dateien in iCloud oder einem von dir gewählten Ordner bleiben, bis du sie dort löschst.", + "deleteAccount.section.kept.item2": "Google Play, Apple und Polar behalten ihre eigenen Kaufbelege. Deine Käufe bleiben bei deinem Google- oder Apple-Konto, du kannst sie also später nach einer neuen Anmeldung wiederherstellen.", + "deleteAccount.section.kept.item3": "Das Löschen des Kontos kündigt kein Abo. Kündige es separat in Google Play oder im App Store, sonst wird es weiter abgerechnet.", + "deleteAccount.section.privacy.title": "Mehr Details", + "deleteAccount.section.privacy.beforeLink": "Was der Premium-Dienst speichert, solange dein Konto besteht, steht in der ", + "deleteAccount.section.privacy.linkText": "Datenschutzerklärung" +} diff --git a/src/i18n/locales/de/index.ts b/src/i18n/locales/de/index.ts index b2c9ddce..5d464af9 100644 --- a/src/i18n/locales/de/index.ts +++ b/src/i18n/locales/de/index.ts @@ -8,6 +8,8 @@ import support from "./support.json"; import sponsors from "./sponsors.json"; import openSourceAccessibility from "./open-source-accessibility.json"; import privacy from "./privacy.json"; +import android from "./android.json"; +import deleteAccount from "./delete-account.json"; import docsCommon from "./docs/common.json"; import docsMac from "./docs/mac.json"; import platformReleases from "./platform-releases.json"; @@ -23,6 +25,8 @@ const translations = { ...sponsors, ...openSourceAccessibility, ...privacy, + ...android, + ...deleteAccount, ...docsCommon, ...docsMac, ...platformReleases, diff --git a/src/i18n/locales/de/privacy.json b/src/i18n/locales/de/privacy.json index 67c07fc4..aa03e2c7 100644 --- a/src/i18n/locales/de/privacy.json +++ b/src/i18n/locales/de/privacy.json @@ -1,10 +1,10 @@ { "privacy.meta.title": "Datenschutzerklärung - TypeWhisper", - "privacy.meta.description": "TypeWhisper Datenschutzerklärung. Standardmäßig lokal, keine verpflichtende Telemetrie und explizites Opt-in für Cloud- oder Custom-Endpunkte.", + "privacy.meta.description": "TypeWhisper Datenschutzerklärung für macOS, Windows, iPhone, iPad und Android. Standardmäßig lokal, keine verpflichtende Telemetrie und explizites Opt-in für Cloud- oder Custom-Endpunkte.", "privacy.heading": "Datenschutzerklärung", - "privacy.updated": "Zuletzt aktualisiert: Juli 2026", + "privacy.updated": "Zuletzt aktualisiert: Oktober 2026", "privacy.section.overview.title": "Überblick", - "privacy.section.overview.p1": "TypeWhisper ist eine lokal arbeitende App für macOS, iPhone, iPad und Windows. Standardmäßig werden Daten vollständig auf deinem Gerät verarbeitet. Nur wenn du bewusst einen Cloud- oder Custom-Endpunkt konfigurierst, werden die für diese Anfrage nötigen Daten an diesen Anbieter übertragen.", + "privacy.section.overview.p1": "TypeWhisper ist eine lokal arbeitende App für macOS, iPhone, iPad, Windows und Android (in der Beta). Standardmäßig werden Daten vollständig auf deinem Gerät verarbeitet. Nur wenn du bewusst einen Cloud- oder Custom-Endpunkt konfigurierst, werden die für diese Anfrage nötigen Daten an diesen Anbieter übertragen.", "privacy.section.dataCollection.title": "Datenerfassung", "privacy.section.dataCollection.p1": "TypeWhisper erfasst standardmäßig keine Telemetrie. Audioverarbeitung, Transkription und Speicherung erfolgen standardmäßig lokal auf deinem Gerät. An von TypeWhisper betriebene Server werden keine Audio- oder Transkriptionsdaten gesendet, außer du installierst und aktivierst Improve TypeWhisper ausdrücklich, prüfst ausgewählte Korrekturen und bestätigst den manuellen Versand. Wenn du aktiv eine Cloud-Engine nutzt oder einen Custom-Endpunkt konfigurierst, werden Daten für diese Anfrage an den gewählten Anbieter übertragen.", "privacy.section.analytics.title": "Website-Analyse und Attribution", @@ -17,17 +17,35 @@ "privacy.section.localStorage.p2": "Diese Daten werden standardmäßig auf deinem Gerät gespeichert und sind für uns nicht zugänglich. Optionale Cloud- oder Custom-Endpunkte erhalten nur die Anfragedaten, die du bewusst an sie weiterleitest.", "privacy.section.premiumSync.title": "Optionales Premium-Konto und Sync", "privacy.section.premiumSync.p1": "Premium Sync ist optional. Über „Mit Apple anmelden“ erhält der TypeWhisper-Berechtigungsdienst eine stabile Konto-ID. Der Dienst speichert ausschließlich gehashte Konto-, Lizenz-, StoreKit-Transaktions-, Sitzungs- und Gerätereferenzen sowie Berechtigungsstatus, Tarif, Ablaufdatum, Gerätelimit und Prüfzeitpunkt. Polar-Lizenzschlüssel werden nur zur Validierung verwendet und weder im Original gespeichert noch protokolliert. Inhalte aus Wörterbuch und Snippets laufen niemals über den TypeWhisper-Berechtigungsdienst: Sie verbleiben im privaten iCloud-Container oder in dem von dir gewählten Files-/Finder-Ordner. Abmeldung oder eine abgelaufene Berechtigung pausiert den Sync, ohne lokale oder bereits synchronisierte Daten zu löschen. Die Kontolöschung in der App entfernt das TypeWhisper-Konto und seine Berechtigungsverknüpfungen; Cloud-Dateien bleiben bestehen, bis du sie separat löschst.", + "privacy.section.android.title": "Android-App (Beta)", + "privacy.section.android.p1": "Die Android-App ist in der Beta und arbeitet ebenso lokal. Dieser Abschnitt beschreibt, was sie mit deinen Daten macht. Ihr Quellcode ist nicht öffentlich.", + "privacy.section.android.speech.title": "Spracherkennung", + "privacy.section.android.speech.p1": "Standardmäßig wandelt die App Sprache auf deinem Handy in Text um, mit Parakeet, Whisper oder Nemotron. Audio verlässt das Handy nur, wenn du eine Cloud-Spracherkennung wählst und deinen eigenen API-Schlüssel einträgst. Es geht dann direkt von deinem Handy an diesen Anbieter: Groq, OpenAI, ElevenLabs, Soniox, Meta, Reson8, Deepgram, AssemblyAI oder deinen eigenen Server. Modelldateien werden von Hugging Face geladen; die App sendet dabei keine Konto- oder Gerätekennungen mit.", + "privacy.section.android.writing.title": "Schreibprofile", + "privacy.section.android.writing.p1": "Schreibprofile senden den diktierten oder markierten Text an den Anbieter des Sprachmodells, den du einrichtest: OpenAI, OpenRouter, Groq, Meta, Anthropic, Google Gemini oder deinen eigenen Server. Das passiert erst, nachdem du in der App zugestimmt hast. Für das, was du an diese Anbieter sendest, gelten deren Datenschutzerklärungen.", + "privacy.section.android.keyboard.title": "Tastatur", + "privacy.section.android.keyboard.p1": "Die TypeWhisper-Tastatur ist eine Android-Eingabemethode. Sie verarbeitet, was du tippst, und den umgebenden Text nur auf deinem Handy. Text aus Passwortfeldern wird nie an ein Schreibmodell gesendet, und Text aus Inkognito-Feldern wird nicht im Verlauf gespeichert.", + "privacy.section.android.storage.title": "Speicherung auf deinem Handy", + "privacy.section.android.storage.p1": "Verlauf, Wörterbuch, Profile, Snippets und behaltene Aufnahmen bleiben im Speicher der App auf deinem Handy. Die Android-Sicherung ist für die App ausgeschaltet. Stimmprofile für die Sprechererkennung verlassen das Handy nie. API-Schlüssel werden mit dem Android Keystore verschlüsselt gespeichert. Unter Einstellungen → Daten und Sicherung kannst du deine Daten exportieren oder löschen.", + "privacy.section.android.premium.title": "Premium unter Android", + "privacy.section.android.premium.p1": "Premium kaufst du über Google Play Billing. Um Premium an ein Konto zu binden, meldest du dich mit Google oder Apple an. Der TypeWhisper-Premium-Dienst (app.typewhisper.com, gehostet bei Cloudflare) speichert nur eine gehashte Konto-ID, eine gehashte zufällige Installations-ID, gehashte Google-Play-Kauftokens und deine Berechtigung. Name und E-Mail-Adresse speichert er nicht. Eine auf typewhisper.com gekaufte Polar-Lizenz lässt sich im Web mit dem Konto verknüpfen.", + "privacy.section.android.tracking.title": "Kein Tracking", + "privacy.section.android.tracking.p1": "Die Android-App enthält keine SDKs für Werbung, Analyse, Absturzberichte oder Tracking. Feedback erreicht uns nur als E-Mail, die du selbst aus deiner Mail-App sendest. Sie kann App-Version, Android-Version, Gerätemodell und ein kurzes Fehlerprotokoll ohne diktierten Text enthalten.", + "privacy.section.deleteAccount.title": "Konto löschen", + "privacy.section.deleteAccount.beforeLink": "Du kannst dein TypeWhisper-Premium-Konto in der App löschen (unter Android: Einstellungen → Premium → Konto löschen) oder per E-Mail. Die ", + "privacy.section.deleteAccount.linkText": "Seite zur Kontolöschung", + "privacy.section.deleteAccount.afterLink": " erklärt beide Wege und was gelöscht wird.", "privacy.section.contributions.title": "Optionale Korrekturbeiträge", "privacy.section.contributions.p1": "Improve TypeWhisper ist ein optionales Marketplace-Add-on und standardmäßig deaktiviert. Nach der Aktivierung werden lokal ausschließlich Vorher-/Nachher-Texte vorgemerkt, die du manuell geändert hast; Audio, Ziel-App, URL und unveränderte Diktate sind ausgeschlossen. Es wird nichts hochgeladen, bevor du Korrekturen auswählst, die vollständigen Texte prüfst und den Versand bestätigst. Übermittelte Texte werden beim Transport geschützt und serverseitig als zusätzlich anwendungsverschlüsselter Ciphertext gespeichert. Ein anonymer Contributor-Token liegt im Schlüsselbund; eine E-Mail-Adresse oder ein TypeWhisper-Konto ist nicht erforderlich. Autorisierte TypeWhisper-Prüfer und ein privater Validierungsablauf mit Codex/OpenAI können die eingesendeten Textpaare zur Bewertung der Trainingsqualität verarbeiten. Du kannst eine Einsendung löschen, solange sie noch nicht in einen validierten Datensatz-Snapshot übernommen wurde. Abgelehnte, ausstehende oder zurückgestellte Einsendungen, die nicht übernommen wurden, werden nach 90 Tagen automatisch gelöscht. Angenommene Beiträge dürfen für Modellevaluation und Training aufbewahrt werden. Nach der Übernahme in einen Datensatz oder ein Modell kann eine Entfernung aus bereits erzeugten Datensätzen, Modellartefakten oder erlernten Parametern technisch nicht mehr möglich sein.", "privacy.section.network.title": "Netzwerkzugriff", "privacy.section.network.p1": "Standardmäßig verbindet sich TypeWhisper nur für Produktdienste wie Updates und den Download von KI-Modellen mit dem Internet. Nach dem Download lokaler Modelle kann die lokale Transkription offline genutzt werden. Wenn du eine Cloud-Engine aktivierst, einen Custom-Endpunkt konfigurierst, Premium-Kontodienste verwendest oder über Improve TypeWhisper Korrekturen manuell einsendest, verbindet sich die App zusätzlich mit dem jeweiligen gewählten Dienst.", "privacy.section.thirdParty.title": "Drittanbieter-Dienste", - "privacy.section.thirdParty.p1": "Die Website verwendet selbst gehostetes Plausible für aggregierte Analysen und Polar für Checkout, Zahlung, Lizenzierung und Kaufattribution. Apple stellt „Mit Apple anmelden“, iCloud, Files, StoreKit-Zahlungen und Abonnementstatus bereit. Cloudflare betreibt die TypeWhisper-Berechtigungs-API sowie die optionale Beitrags-API und leitet Zugriffe auf den Analyse-Endpunkt weiter. OpenAI kann manuell eingesendete Korrekturpaare verarbeiten, wenn Codex im privaten Validierungsablauf eingesetzt wird. Die Apps enthalten keine Werbetracker und senden keine Nutzungs-, Audio- oder Transkriptions-Telemetrie an TypeWhisper.", + "privacy.section.thirdParty.p1": "Die Website verwendet selbst gehostetes Plausible für aggregierte Analysen und Polar für Checkout, Zahlung, Lizenzierung und Kaufattribution. Apple stellt „Mit Apple anmelden“, iCloud, Files, StoreKit-Zahlungen und Abonnementstatus bereit. Unter Android stellt Google Google Play Billing und „Mit Google anmelden“ bereit, Modelldateien kommen von Hugging Face. Cloudflare betreibt die TypeWhisper-Berechtigungs-API sowie die optionale Beitrags-API und leitet Zugriffe auf den Analyse-Endpunkt weiter. OpenAI kann manuell eingesendete Korrekturpaare verarbeiten, wenn Codex im privaten Validierungsablauf eingesetzt wird. Die Apps enthalten keine Werbetracker und senden keine Nutzungs-, Audio- oder Transkriptions-Telemetrie an TypeWhisper.", "privacy.section.securityReview.title": "Kanzlei-Security-Review", "privacy.section.securityReview.beforeLink": "Für Kanzlei-IT und Datenschutzprüfung gibt es zusätzlich eine ", "privacy.section.securityReview.linkText": "öffentliche Prüfliste zu Datenfluss, Speicherorten und optionalen Cloud-Bausteinen", "privacy.section.openSource.title": "Open Source", "privacy.section.openSource.p1": "TypeWhisper ist Open Source unter der GPLv3-Lizenz. Du kannst den gesamten Quellcode auf GitHub einsehen.", "privacy.section.contact.title": "Kontakt", - "privacy.section.contact.p1": "Bei Fragen zu dieser Datenschutzerklärung erstelle bitte ein Issue in unserer GitHub-Organisation." + "privacy.section.contact.p1": "Bei Fragen zu dieser Datenschutzerklärung schreib an hello@typewhisper.com oder erstelle ein Issue in unserer GitHub-Organisation." } diff --git a/src/i18n/locales/en/android.json b/src/i18n/locales/en/android.json new file mode 100644 index 00000000..5e0e9851 --- /dev/null +++ b/src/i18n/locales/en/android.json @@ -0,0 +1,37 @@ +{ + "android.meta.title": "TypeWhisper for Android (Beta)", + "android.meta.description": "Join the TypeWhisper beta for Android: private speech-to-text with on-device AI, a voice keyboard, and writing profiles.", + "android.label": "Beta", + "android.title": "TypeWhisper for Android", + "android.lede": "Private speech-to-text for Android with on-device AI, a voice keyboard, and writing profiles.", + "android.requirement": "Android 10 or later", + "android.join.label": "Join the beta", + "android.join.title": "How to join the beta", + "android.join.lede": "The beta runs through Google Play. Two steps, and updates then arrive like any other app update.", + "android.join.step1.title": "Join the Google Group", + "android.join.step1.text": "Any Google account works. The group only serves as the tester list; no emails are posted there.", + "android.join.step1.button": "Join the Google Group", + "android.join.step2.title": "Opt in and install from Google Play", + "android.join.step2.text": "Open the testing page with the Google account that joined the group, tap Become a tester, then install TypeWhisper from Google Play. After joining the group, it can take a few minutes until the page lets you in.", + "android.join.step2.button": "Opt in on Google Play", + "android.try.label": "What to try", + "android.try.title": "What to try", + "android.try.lede": "Everything below is in the beta. Try what you would use every day.", + "android.try.item1": "On-device dictation with Parakeet (25 European languages) or Whisper (99 languages).", + "android.try.item2": "Optional cloud speech engines with your own API key.", + "android.try.item3": "The TypeWhisper keyboard in any app, with a QWERTZ or QWERTY layout.", + "android.try.item4": "Writing profiles: clean up, message, email, note, calendar event, checklist, summary, translation, or your own.", + "android.try.item5": "The Inbox for quick dictations from the quick settings tile, the widget, an app shortcut, or the assist gesture.", + "android.try.item6": "File transcription with subtitles.", + "android.try.item7": "Premium: the email and calendar profiles, live transcription with Nemotron, and speaker detection.", + "android.notice.label": "Beta notice", + "android.notice.title": "Beta notice", + "android.notice.p1": "This is a beta, so expect bugs and rough edges.", + "android.notice.p2": "Please stay opted in for at least 14 days. Google only allows the public release after 12 testers have stayed in the beta for 14 days.", + "android.notice.p3": "Send feedback in the app under Settings → Send feedback, on Discord, or by email.", + "android.notice.discord": "Discord", + "android.notice.discordText": "Questions, bugs, and ideas with other testers.", + "android.notice.emailText": "Bug reports and anything you would rather not post publicly.", + "android.requirements.title": "Requirements", + "android.requirements.text": "Android 10 or later and a Google account for Google Play." +} diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index f1ffab36..947bcfa3 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -49,6 +49,7 @@ "footer.changelog": "Changelog", "footer.windows": "Windows", "footer.ios": "iOS", + "footer.android": "Android Beta", "footer.discord": "Discord", "footer.github": "GitHub", "footer.kofi": "Ko-fi", @@ -56,10 +57,11 @@ "footer.privacy": "Privacy", "footer.terms": "Terms", "footer.legalNotice": "Legal Notice", + "footer.deleteAccount": "Delete account", "madeInGermany.label": "Made in Germany", "madeInGermany.craft": "Independently crafted in Germany", "releaseStage.stable": "Stable", - "releaseStage.beta": "Preview", + "releaseStage.beta": "Beta", "releaseStage.alpha": "Alpha", "releaseStage.pending": "App Store release in preparation", "benchmark.title": "Benchmark - TypeWhisper", @@ -67,8 +69,8 @@ "changelog.title": "Changelog - TypeWhisper", "changelog.description": "Release notes and updates for TypeWhisper.", "releaseStatus.title": "Release Status", - "releaseStatus.meta.description": "Current release stages for TypeWhisper on macOS, Windows, and iOS.", - "releaseStatus.subtitle": "TypeWhisper ships stable editions for macOS, Windows, iPhone, and iPad.", + "releaseStatus.meta.description": "Current release stages for TypeWhisper on macOS, Windows, iOS, and Android.", + "releaseStatus.subtitle": "TypeWhisper ships stable editions for macOS, Windows, iPhone, and iPad. Android is in beta.", "releaseStatus.mac.intro": "macOS {macSeries} is the supported stable release.", "releaseStatus.mac.detail": "This is the official stable path for daily use, documentation, support, and launch messaging.", "releaseStatus.win.intro": "Windows is a stable desktop release.", @@ -88,6 +90,11 @@ "releaseStatus.ios.description": "Private speech-to-text with on-device engines, a voice keyboard, profiles, files, Shortcuts, dictionary, snippets, and Apple Watch support. Version 1.1 adds on-device writing models, dictionary imports with a preview, and recording controls for Control Center and the Action Button.", "releaseStatus.ios.download": "Download on the App Store", "releaseStatus.ios.docs": "Read the iOS docs", + "releaseStatus.android.title": "Android", + "releaseStatus.android.intro": "Android is in a closed beta on Google Play.", + "releaseStatus.android.detail": "Testers get on-device dictation, the voice keyboard, writing profiles, and the Inbox before the public release. Expect bugs.", + "releaseStatus.android.description": "Requires Android 10 or later. Anyone with a Google account can join the beta.", + "releaseStatus.android.join": "Join the Android beta", "notFound.title": "404", "notFound.heading": "Page not found", "notFound.description": "The page you're looking for doesn't exist or has been moved.", diff --git a/src/i18n/locales/en/delete-account.json b/src/i18n/locales/en/delete-account.json new file mode 100644 index 00000000..aa0a0cc0 --- /dev/null +++ b/src/i18n/locales/en/delete-account.json @@ -0,0 +1,29 @@ +{ + "deleteAccount.meta.title": "Delete your TypeWhisper account - TypeWhisper", + "deleteAccount.meta.description": "How to delete your TypeWhisper Premium account on Android, iPhone, iPad, and Mac, and what is deleted.", + "deleteAccount.heading": "Delete your TypeWhisper account", + "deleteAccount.lede": "You can delete your TypeWhisper Premium account in the app or by email.", + "deleteAccount.section.who.title": "Who this is for", + "deleteAccount.section.who.p1": "This page is for TypeWhisper Premium accounts on Android, iPhone, iPad, and Mac. You have such an account if you signed in with Google or Apple to use Premium. TypeWhisper works without an account; if you never signed in, there is nothing to delete.", + "deleteAccount.section.inApp.title": "Delete in the app", + "deleteAccount.section.inApp.p1": "This is the fastest way.", + "deleteAccount.section.inApp.item1": "Android: Settings → Premium → Delete account.", + "deleteAccount.section.inApp.item2": "iPhone, iPad, and Mac: in the app's Premium settings.", + "deleteAccount.section.email.title": "Delete by email", + "deleteAccount.section.email.p1": "If you can't use the app, send an email to hello@typewhisper.com with the subject “Delete my TypeWhisper account”. Write from the email address of the Google or Apple account you signed in with, and tell us whether you used Google or Apple.", + "deleteAccount.section.deleted.title": "What is deleted", + "deleteAccount.section.deleted.p1": "When the account is deleted, the TypeWhisper Premium service removes:", + "deleteAccount.section.deleted.item1": "Your TypeWhisper account", + "deleteAccount.section.deleted.item2": "The devices registered to it", + "deleteAccount.section.deleted.item3": "Your Premium entitlements", + "deleteAccount.section.deleted.item4": "The records of your Google Play purchases", + "deleteAccount.section.deleted.item5": "Your sign-in sessions", + "deleteAccount.section.deleted.p2": "If a Polar license from typewhisper.com is linked to the account, its activations for the account are deactivated.", + "deleteAccount.section.kept.title": "What is not deleted", + "deleteAccount.section.kept.item1": "Data on your devices, such as history, dictionary, profiles, and recordings, stays until you delete it in the app. Synced files in iCloud or a folder you chose stay until you delete them there.", + "deleteAccount.section.kept.item2": "Google Play, Apple, and Polar keep their own purchase records. Your purchases stay with your Google or Apple account, so you can restore them later by signing in again.", + "deleteAccount.section.kept.item3": "Deleting the account does not cancel a subscription. Cancel it separately in Google Play or the App Store, or billing continues.", + "deleteAccount.section.privacy.title": "More details", + "deleteAccount.section.privacy.beforeLink": "What the Premium service stores while your account exists is described in the ", + "deleteAccount.section.privacy.linkText": "privacy policy" +} diff --git a/src/i18n/locales/en/index.ts b/src/i18n/locales/en/index.ts index b2c9ddce..5d464af9 100644 --- a/src/i18n/locales/en/index.ts +++ b/src/i18n/locales/en/index.ts @@ -8,6 +8,8 @@ import support from "./support.json"; import sponsors from "./sponsors.json"; import openSourceAccessibility from "./open-source-accessibility.json"; import privacy from "./privacy.json"; +import android from "./android.json"; +import deleteAccount from "./delete-account.json"; import docsCommon from "./docs/common.json"; import docsMac from "./docs/mac.json"; import platformReleases from "./platform-releases.json"; @@ -23,6 +25,8 @@ const translations = { ...sponsors, ...openSourceAccessibility, ...privacy, + ...android, + ...deleteAccount, ...docsCommon, ...docsMac, ...platformReleases, diff --git a/src/i18n/locales/en/privacy.json b/src/i18n/locales/en/privacy.json index 358209fa..baf7d08d 100644 --- a/src/i18n/locales/en/privacy.json +++ b/src/i18n/locales/en/privacy.json @@ -1,10 +1,10 @@ { "privacy.meta.title": "Privacy Policy - TypeWhisper", - "privacy.meta.description": "TypeWhisper privacy policy. Local-first by default, no mandatory telemetry, and explicit opt-in for cloud or custom endpoints.", + "privacy.meta.description": "TypeWhisper privacy policy for macOS, Windows, iPhone, iPad, and Android. Local-first by default, no mandatory telemetry, and explicit opt-in for cloud or custom endpoints.", "privacy.heading": "Privacy Policy", - "privacy.updated": "Last updated: July 2026", + "privacy.updated": "Last updated: October 2026", "privacy.section.overview.title": "Overview", - "privacy.section.overview.p1": "TypeWhisper is a local-first app for macOS, iPhone, iPad, and Windows. By default, data is processed entirely on your device. If you explicitly configure a cloud or custom endpoint, the data needed for that request is sent to that provider.", + "privacy.section.overview.p1": "TypeWhisper is a local-first app for macOS, iPhone, iPad, Windows, and Android (in beta). By default, data is processed entirely on your device. If you explicitly configure a cloud or custom endpoint, the data needed for that request is sent to that provider.", "privacy.section.dataCollection.title": "Data Collection", "privacy.section.dataCollection.p1": "TypeWhisper does not collect telemetry by default. Audio processing, transcription, and storage happen locally on your device by default. No audio or transcription data is sent to TypeWhisper-operated servers unless you explicitly install and enable Improve TypeWhisper, review selected corrections, and confirm a manual submission. If you opt into a cloud engine or configure a custom endpoint, data for that request is sent to the selected provider.", "privacy.section.analytics.title": "Website Analytics and Attribution", @@ -17,17 +17,35 @@ "privacy.section.localStorage.p2": "This data is stored on your device by default and is not accessible to us. Optional cloud or custom endpoints only receive the request data you explicitly route to them.", "privacy.section.premiumSync.title": "Optional Premium Account and Sync", "privacy.section.premiumSync.p1": "Premium Sync is optional. Sign in with Apple gives the TypeWhisper entitlement service a stable account identifier. The service stores only hashed account, license, StoreKit transaction, session, and device references plus entitlement status, tier, expiry, device limit, and verification time. Polar license keys are used for validation and are not stored or logged in their original form. Dictionary and snippet contents never pass through the TypeWhisper entitlement service: they remain in the private iCloud container or Files/Finder folder selected by you. Signing out or an expired entitlement pauses sync without deleting local or already synchronized data. In-app account deletion removes the TypeWhisper account and its entitlement links; cloud files remain until you delete them separately.", + "privacy.section.android.title": "Android App (Beta)", + "privacy.section.android.p1": "The Android app is in beta and follows the same local-first approach. This section describes what it does with your data. Its source code is not public.", + "privacy.section.android.speech.title": "Speech recognition", + "privacy.section.android.speech.p1": "By default, the app turns speech into text on your phone, with Parakeet, Whisper, or Nemotron. Audio leaves the phone only if you pick a cloud speech engine and enter your own API key. It then goes directly from your phone to that provider: Groq, OpenAI, ElevenLabs, Soniox, Meta, Reson8, Deepgram, AssemblyAI, or your own server. Model files are downloaded from Hugging Face; the app sends no account or device identifiers with these downloads.", + "privacy.section.android.writing.title": "Writing profiles", + "privacy.section.android.writing.p1": "Writing profiles send the dictated or selected text to the language model provider you configure: OpenAI, OpenRouter, Groq, Meta, Anthropic, Google Gemini, or your own server. This only happens after you agree to it in the app. The privacy policies of these providers apply to what you send them.", + "privacy.section.android.keyboard.title": "Keyboard", + "privacy.section.android.keyboard.p1": "The TypeWhisper keyboard is an Android input method. It processes what you type and the surrounding text only on your phone. Text from password fields is never sent to a writing model, and text from incognito fields is not saved to the history.", + "privacy.section.android.storage.title": "Storage on your phone", + "privacy.section.android.storage.p1": "History, dictionary, profiles, snippets, and kept recordings stay in the app's storage on your phone. Android backup is turned off for the app. Voice profiles for speaker detection never leave the phone. API keys are stored encrypted with the Android Keystore. You can export or delete your data in Settings → Data and backup.", + "privacy.section.android.premium.title": "Premium on Android", + "privacy.section.android.premium.p1": "Premium is bought through Google Play Billing. To tie Premium to an account, you sign in with Google or Apple. The TypeWhisper Premium service (app.typewhisper.com, hosted on Cloudflare) stores only a hashed account identifier, a hashed random install ID, hashed Google Play purchase tokens, and your entitlement. It does not store your name or email address. A Polar license bought on typewhisper.com can be linked to the account on the web.", + "privacy.section.android.tracking.title": "No tracking", + "privacy.section.android.tracking.p1": "The Android app contains no advertising, analytics, crash reporting, or tracking SDKs. Feedback reaches us only as an email that you send from your own mail app. It can include the app version, Android version, device model, and a short error log without dictated text.", + "privacy.section.deleteAccount.title": "Deleting Your Account", + "privacy.section.deleteAccount.beforeLink": "You can delete your TypeWhisper Premium account in the app (on Android: Settings → Premium → Delete account) or by email. The ", + "privacy.section.deleteAccount.linkText": "account deletion page", + "privacy.section.deleteAccount.afterLink": " explains both ways and what is deleted.", "privacy.section.contributions.title": "Optional Correction Contributions", "privacy.section.contributions.p1": "Improve TypeWhisper is an optional marketplace add-on and is disabled by default. When enabled, only before-and-after text that you manually changed is queued locally; audio, target application, URL, and unchanged dictations are excluded. Nothing is uploaded until you select corrections, review the complete texts, and confirm sending. Submitted text is protected in transit and stored as application-level encrypted ciphertext. An anonymous contributor token is stored in Keychain; no email address or TypeWhisper account is required. Authorized TypeWhisper reviewers and a private validation workflow using Codex/OpenAI may process submitted pairs to assess training quality. You can delete a submission until it is incorporated into a validated dataset snapshot. Rejected, pending, or quarantined submissions that are not incorporated are automatically deleted after 90 days. Accepted contributions may be retained for model evaluation and training. Once incorporated into a dataset or model, removing the contribution from already created datasets, model artifacts, or learned parameters may no longer be technically possible.", "privacy.section.network.title": "Network Access", "privacy.section.network.p1": "By default, TypeWhisper connects to the internet only for product services such as updates and AI model downloads. After local models are downloaded, local transcription can function offline. If you enable a cloud engine, configure a custom endpoint, use Premium account services, or manually submit corrections through Improve TypeWhisper, the app also connects to the corresponding selected service.", "privacy.section.thirdParty.title": "Third-Party Services", - "privacy.section.thirdParty.p1": "The website uses self-hosted Plausible for aggregate analytics and Polar for checkout, payment, licensing, and purchase attribution. Apple provides Sign in with Apple, iCloud, Files, StoreKit payments, and subscription status. Cloudflare operates the TypeWhisper entitlement and optional contribution APIs and forwards requests to the analytics endpoint. OpenAI may process manually submitted correction pairs when Codex is used in the private validation workflow. The apps do not contain advertising trackers and do not send usage, audio, or transcription telemetry to TypeWhisper.", + "privacy.section.thirdParty.p1": "The website uses self-hosted Plausible for aggregate analytics and Polar for checkout, payment, licensing, and purchase attribution. Apple provides Sign in with Apple, iCloud, Files, StoreKit payments, and subscription status. On Android, Google provides Google Play Billing and Sign in with Google, and model files are downloaded from Hugging Face. Cloudflare operates the TypeWhisper entitlement and optional contribution APIs and forwards requests to the analytics endpoint. OpenAI may process manually submitted correction pairs when Codex is used in the private validation workflow. The apps do not contain advertising trackers and do not send usage, audio, or transcription telemetry to TypeWhisper.", "privacy.section.securityReview.title": "Law-Firm Security Review", "privacy.section.securityReview.beforeLink": "For firm IT and privacy review, there is also a ", "privacy.section.securityReview.linkText": "public checklist covering data flow, storage locations, and optional cloud building blocks", "privacy.section.openSource.title": "Open Source", "privacy.section.openSource.p1": "TypeWhisper is open source under the GPLv3 license. You can audit the entire source code on GitHub.", "privacy.section.contact.title": "Contact", - "privacy.section.contact.p1": "If you have questions about this privacy policy, please open an issue on our GitHub organization." + "privacy.section.contact.p1": "If you have questions about this privacy policy, email hello@typewhisper.com or open an issue on our GitHub organization." } diff --git a/src/lib/android-beta.ts b/src/lib/android-beta.ts new file mode 100644 index 00000000..5ae37e71 --- /dev/null +++ b/src/lib/android-beta.ts @@ -0,0 +1,9 @@ +/** + * Links of the Android closed beta on Google Play. Testers join the group + * first; Play only lets group members opt in. + */ +export const androidBeta = { + googleGroupUrl: "https://groups.google.com/g/typewhisper-android-beta", + playOptInUrl: "https://play.google.com/apps/testing/com.typewhisper.android", + feedbackEmail: "hello@typewhisper.com", +} as const; diff --git a/src/pages/[locale]/android.astro b/src/pages/[locale]/android.astro new file mode 100644 index 00000000..a268703c --- /dev/null +++ b/src/pages/[locale]/android.astro @@ -0,0 +1,143 @@ +--- +import "@/styles/utility.css"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import { ArrowUpRight } from "lucide-react"; +import { BarMark, PageHead, WaveRule } from "@/components/site"; +import { t, type Locale } from "@/i18n/index"; +import { canonicalPath, getLocaleStaticPaths } from "@/lib/locale-routes"; +import { androidBeta } from "@/lib/android-beta"; +import { discordUrl } from "@/lib/platform-download"; + +interface Props { + locale: Locale; +} + +export function getStaticPaths() { + return getLocaleStaticPaths(); +} + +const { locale } = Astro.props; + +// Play only lets members of the Google Group opt in, so the group comes first. +const steps = [ + { id: "step1", href: androidBeta.googleGroupUrl }, + { id: "step2", href: androidBeta.playOptInUrl }, +]; + +const feedback = [ + { + label: t(locale, "android.notice.discord"), + text: t(locale, "android.notice.discordText"), + href: discordUrl, + }, + { + label: androidBeta.feedbackEmail, + text: t(locale, "android.notice.emailText"), + href: `mailto:${androidBeta.feedbackEmail}`, + }, +]; +--- + + +
    + + +
    +
    + +
    +
    +

    {t(locale, "android.join.title")}

    +

    {t(locale, "android.join.lede")}

    +
    +
      + {steps.map((step) => ( +
    1. +
      +

      {t(locale, `android.join.${step.id}.title`)}

      +

      {t(locale, `android.join.${step.id}.text`)}

      + +
      +
    2. + ))} +
    +
    +
    +
    + +
    +
    + +
    +
    +

    {t(locale, "android.try.title")}

    +

    {t(locale, "android.try.lede")}

    +
    +
      + {[1, 2, 3, 4, 5, 6, 7].map((item) => ( +
    • + +

      {t(locale, `android.try.item${item}`)}

      +
    • + ))} +
    +
    +
    +
    + +
    +
    + +
    +
    +

    {t(locale, "android.notice.title")}

    +

    {t(locale, "android.notice.p1")}

    +

    {t(locale, "android.notice.p2")}

    +

    {t(locale, "android.notice.p3")}

    +
    +
    + +

    {t(locale, "android.requirements.title")}

    +

    {t(locale, "android.requirements.text")}

    +
    +
    +
    +
    +
    +
    diff --git a/src/pages/[locale]/delete-account.astro b/src/pages/[locale]/delete-account.astro new file mode 100644 index 00000000..4fdc7315 --- /dev/null +++ b/src/pages/[locale]/delete-account.astro @@ -0,0 +1,75 @@ +--- +import "@/styles/utility.css"; +import BaseLayout from "@/layouts/BaseLayout.astro"; +import { PageHead } from "@/components/site"; +import { t, localePath, type Locale } from "@/i18n/index"; +import { canonicalPath, getLocaleStaticPaths } from "@/lib/locale-routes"; + +interface Props { + locale: Locale; +} + +export function getStaticPaths() { + return getLocaleStaticPaths(); +} + +const { locale } = Astro.props; + +const privacyHref = localePath(locale, "/privacy"); + +// Google Play links to this page as the way to delete an account outside the app. +const sections: { id: string; paragraphs: string[]; items: number; after?: string[] }[] = [ + { id: "who", paragraphs: ["p1"], items: 0 }, + { id: "inApp", paragraphs: ["p1"], items: 2 }, + { id: "email", paragraphs: ["p1"], items: 0 }, + { id: "deleted", paragraphs: ["p1"], items: 5, after: ["p2"] }, + { id: "kept", paragraphs: [], items: 3 }, +]; +--- + + +
    + +
    +
    +
    + {sections.map((section) => ( + <> +

    {t(locale, `deleteAccount.section.${section.id}.title`)}

    + {section.paragraphs.map((key) => ( +

    + ))} + {section.items > 0 && ( +

      + {Array.from({ length: section.items }, (_, index) => ( +
    • {t(locale, `deleteAccount.section.${section.id}.item${index + 1}`)}
    • + ))} +
    + )} + {section.after?.map((key) => ( +

    {t(locale, `deleteAccount.section.${section.id}.${key}`)}

    + ))} + + ))} + +

    {t(locale, "deleteAccount.section.privacy.title")}

    +

    + {t(locale, "deleteAccount.section.privacy.beforeLink")} + {t(locale, "deleteAccount.section.privacy.linkText")}. +

    +
    +
    +
    +
    +
    diff --git a/src/pages/[locale]/privacy.astro b/src/pages/[locale]/privacy.astro index c491242d..75180475 100644 --- a/src/pages/[locale]/privacy.astro +++ b/src/pages/[locale]/privacy.astro @@ -16,11 +16,16 @@ export function getStaticPaths() { const { locale } = Astro.props; const securityReviewHref = localePath(locale, "/business/legal-security-review"); +const deleteAccountHref = localePath(locale, "/delete-account"); + +// The Android app gets one section with a short heading per topic. +const androidTopics = ["speech", "writing", "keyboard", "storage", "premium", "tracking"] as const; type PrivacySection = | { id: "overview" | "dataCollection" | "analytics" | "premiumSync" | "contributions" | "network" | "thirdParty" | "openSource" | "contact"; kind: "simple" } | { id: "localStorage"; kind: "list" } - | { id: "securityReview"; kind: "link" }; + | { id: "android"; kind: "topics" } + | { id: "deleteAccount" | "securityReview"; kind: "link" }; const sections: PrivacySection[] = [ { id: "overview", kind: "simple" }, @@ -28,6 +33,8 @@ const sections: PrivacySection[] = [ { id: "analytics", kind: "simple" }, { id: "localStorage", kind: "list" }, { id: "premiumSync", kind: "simple" }, + { id: "android", kind: "topics" }, + { id: "deleteAccount", kind: "link" }, { id: "contributions", kind: "simple" }, { id: "network", kind: "simple" }, { id: "thirdParty", kind: "simple" }, @@ -85,7 +92,29 @@ const sections: PrivacySection[] = [ )} - {section.kind === "link" && ( + {section.kind === "topics" && ( + <> +

    {t(locale, "privacy.section.android.p1")}

    + {androidTopics.map((topic) => ( + <> +

    {t(locale, `privacy.section.android.${topic}.title`)}

    +

    {t(locale, `privacy.section.android.${topic}.p1`)}

    + + ))} + + )} + + {section.id === "deleteAccount" && ( +

    + {t(locale, "privacy.section.deleteAccount.beforeLink")} + + {t(locale, "privacy.section.deleteAccount.linkText")} + + {t(locale, "privacy.section.deleteAccount.afterLink")} +

    + )} + + {section.id === "securityReview" && (

    {t(locale, "privacy.section.securityReview.beforeLink")} diff --git a/src/pages/[locale]/release-status.astro b/src/pages/[locale]/release-status.astro index 75662b48..fdc9fff8 100644 --- a/src/pages/[locale]/release-status.astro +++ b/src/pages/[locale]/release-status.astro @@ -31,9 +31,14 @@ interface StatusLink { } /** One block per edition: stage, what it means, what it contains, where to get it. */ -const editions: { id: "mac" | "win" | "ios"; links: StatusLink[] }[] = [ +const editions: { + id: "mac" | "win" | "ios" | "android"; + stage: "stable" | "beta"; + links: StatusLink[]; +}[] = [ { id: "mac", + stage: "stable", links: [ { label: t(locale, "releaseStatus.mac.download"), @@ -55,6 +60,7 @@ const editions: { id: "mac" | "win" | "ios"; links: StatusLink[] }[] = [ }, { id: "win", + stage: "stable", links: [ { label: t(locale, "releaseStatus.win.download"), @@ -87,6 +93,7 @@ const editions: { id: "mac" | "win" | "ios"; links: StatusLink[] }[] = [ }, { id: "ios", + stage: "stable", links: [ { label: t(locale, "releaseStatus.ios.download"), @@ -107,6 +114,17 @@ const editions: { id: "mac" | "win" | "ios"; links: StatusLink[] }[] = [ }, ], }, + { + id: "android", + stage: "beta", + links: [ + { + label: t(locale, "releaseStatus.android.join"), + href: localePath(locale, "/android"), + kind: "docs", + }, + ], + }, ]; --- @@ -131,7 +149,7 @@ const editions: { id: "mac" | "win" | "ios"; links: StatusLink[] }[] = [

    - {t(locale, "releaseStage.stable")} + {t(locale, `releaseStage.${edition.stage}`)}

    {t(locale, `releaseStatus.${edition.id}.title`)}

    diff --git a/src/pages/android.astro b/src/pages/android.astro new file mode 100644 index 00000000..b7ef5a4f --- /dev/null +++ b/src/pages/android.astro @@ -0,0 +1,4 @@ +--- +import LocalizedRedirect from "@/components/LocalizedRedirect.astro"; +--- + diff --git a/src/pages/delete-account.astro b/src/pages/delete-account.astro new file mode 100644 index 00000000..e5bc1f20 --- /dev/null +++ b/src/pages/delete-account.astro @@ -0,0 +1,4 @@ +--- +import LocalizedRedirect from "@/components/LocalizedRedirect.astro"; +--- + diff --git a/src/styles/utility.css b/src/styles/utility.css index bc9160e8..db1909c3 100644 --- a/src/styles/utility.css +++ b/src/styles/utility.css @@ -678,6 +678,11 @@ font-size: 0.9375rem; } +/* A heading that continues below a block of link rows. */ +.utility-after-rows { + margin-top: clamp(2.5rem, 5vw, 3.5rem); +} + /* Three platforms stay side by side as long as they fit, then stack. */ .utility-platforms { grid-template-columns: repeat(3, minmax(0, 1fr)); diff --git a/tests/android-beta.spec.ts b/tests/android-beta.spec.ts new file mode 100644 index 00000000..1ecc9714 --- /dev/null +++ b/tests/android-beta.spec.ts @@ -0,0 +1,100 @@ +import { expect, test } from "@playwright/test"; + +const groupUrl = "https://groups.google.com/g/typewhisper-android-beta"; +const playUrl = "https://play.google.com/apps/testing/com.typewhisper.android"; + +const copy = { + en: { + title: "TypeWhisper for Android", + deleteTitle: "Delete your TypeWhisper account", + android: "Android Beta", + deleteLink: "Delete account", + }, + de: { + title: "TypeWhisper für Android", + deleteTitle: "TypeWhisper-Konto löschen", + android: "Android-Beta", + deleteLink: "Konto löschen", + }, +} as const; + +for (const locale of ["en", "de"] as const) { + test.describe(`${locale} Android beta and account deletion`, () => { + test("the Android page leads through the group to the Play opt-in", async ({ + page, + }) => { + await page.goto(`/${locale}/android/`); + + await expect( + page.getByRole("heading", { level: 1, name: copy[locale].title }), + ).toBeVisible(); + await expect(page.getByTestId("page-head")).toContainText("Beta"); + await expect(page.getByTestId("android-beta-step1")).toHaveAttribute( + "href", + groupUrl, + ); + await expect(page.getByTestId("android-beta-step2")).toHaveAttribute( + "href", + playUrl, + ); + const notice = page.getByTestId("android-beta-notice"); + await expect(notice).toContainText("14"); + await expect( + notice.locator('a[href="mailto:hello@typewhisper.com"]'), + ).toBeVisible(); + await expect(notice.locator('a[href^="https://discord.gg/"]')).toBeVisible(); + }); + + test("the account deletion page names the app path and the email", async ({ + page, + }) => { + await page.goto(`/${locale}/delete-account/`); + + await expect( + page.getByRole("heading", { level: 1, name: copy[locale].deleteTitle }), + ).toBeVisible(); + const body = page.getByTestId("delete-account"); + await expect(body).toContainText("Premium →"); + await expect(body).toContainText("Delete my TypeWhisper account"); + await expect( + body.locator('a[href^="mailto:hello@typewhisper.com"]'), + ).toBeVisible(); + await expect( + body.locator(`a[href="/${locale}/privacy/"]`), + ).toBeVisible(); + }); + + test("privacy, footer, and release status link to the new pages", async ({ + page, + }) => { + await page.goto(`/${locale}/privacy/`); + await expect(page.locator("#android")).toBeVisible(); + await expect( + page.locator(`.site-prose a[href="/${locale}/delete-account/"]`), + ).toBeVisible(); + + const footer = page.locator("footer"); + await expect( + footer.getByRole("link", { name: copy[locale].android }), + ).toHaveAttribute("href", `/${locale}/android/`); + await expect( + footer.getByRole("link", { name: copy[locale].deleteLink }), + ).toHaveAttribute("href", `/${locale}/delete-account/`); + + await page.goto(`/${locale}/release-status/`); + const android = page.getByTestId("release-status-android"); + await expect(android).toContainText("Beta"); + await expect( + android.locator(`a[href="/${locale}/android/"]`), + ).toBeVisible(); + }); + }); +} + +test("the unprefixed URLs forward to a locale", async ({ page }) => { + await page.goto("/android/"); + await expect(page).toHaveURL(/\/(en|de)\/android\/$/); + + await page.goto("/delete-account/"); + await expect(page).toHaveURL(/\/(en|de)\/delete-account\/$/); +}); diff --git a/tests/android-beta.test.ts b/tests/android-beta.test.ts new file mode 100644 index 00000000..bc3c10de --- /dev/null +++ b/tests/android-beta.test.ts @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import { readFile } from "node:fs/promises"; +import test from "node:test"; + +import { androidBeta } from "../src/lib/android-beta.ts"; + +test("the Android beta links point at the tester group and the Play opt-in", () => { + assert.equal( + androidBeta.googleGroupUrl, + "https://groups.google.com/g/typewhisper-android-beta", + ); + assert.equal( + androidBeta.playOptInUrl, + "https://play.google.com/apps/testing/com.typewhisper.android", + ); +}); + +test("the unprefixed Android and account deletion URLs redirect to a locale", async () => { + for (const name of ["android", "delete-account"]) { + const page = await readFile(`src/pages/${name}.astro`, "utf8"); + assert.match(page, new RegExp(``)); + } +}); + +test("the footer links the Android beta and the account deletion page", async () => { + const footer = await readFile("src/components/layout/footer.astro", "utf8"); + assert.match(footer, /localePath\(locale, "\/android"\)/); + assert.match(footer, /localePath\(locale, "\/delete-account"\)/); +}); diff --git a/tests/client-messages.spec.ts b/tests/client-messages.spec.ts index d36de6cf..da1ae767 100644 --- a/tests/client-messages.spec.ts +++ b/tests/client-messages.spec.ts @@ -205,6 +205,8 @@ for (const locale of ["en", "de"]) { "/release-status/", "/open-source-accessibility/", "/privacy/", + "/android/", + "/delete-account/", "/no-such-page/", ]) await check(page, `/${locale}${path}`, found); From a4e958b9371f72440d0d8e43f7c8a331bb8add1b Mon Sep 17 00:00:00 2001 From: SeoFood Date: Wed, 7 Oct 2026 18:31:21 +0200 Subject: [PATCH 2/7] Delete the account on the web instead of by email The Premium service stores no email address, so a request by email could not be matched to an account. The deletion page and the privacy policy now point to app.typewhisper.com/account/delete, where users sign in with Google or Apple and delete the account themselves. --- src/i18n/locales/de/delete-account.json | 6 +++--- src/i18n/locales/de/privacy.json | 2 +- src/i18n/locales/en/delete-account.json | 6 +++--- src/i18n/locales/en/privacy.json | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/i18n/locales/de/delete-account.json b/src/i18n/locales/de/delete-account.json index b5f0c97c..3c93aec4 100644 --- a/src/i18n/locales/de/delete-account.json +++ b/src/i18n/locales/de/delete-account.json @@ -2,15 +2,15 @@ "deleteAccount.meta.title": "TypeWhisper-Konto löschen - TypeWhisper", "deleteAccount.meta.description": "So löschst du dein TypeWhisper-Premium-Konto auf Android, iPhone, iPad und Mac, und das wird dabei gelöscht.", "deleteAccount.heading": "TypeWhisper-Konto löschen", - "deleteAccount.lede": "Du kannst dein TypeWhisper-Premium-Konto in der App oder per E-Mail löschen.", + "deleteAccount.lede": "Du kannst dein TypeWhisper-Premium-Konto in der App oder im Web löschen.", "deleteAccount.section.who.title": "Für wen diese Seite ist", "deleteAccount.section.who.p1": "Diese Seite gilt für TypeWhisper-Premium-Konten auf Android, iPhone, iPad und Mac. Ein solches Konto hast du, wenn du dich für Premium mit Google oder Apple angemeldet hast. TypeWhisper funktioniert ohne Konto; hast du dich nie angemeldet, gibt es nichts zu löschen.", "deleteAccount.section.inApp.title": "In der App löschen", "deleteAccount.section.inApp.p1": "Das ist der schnellste Weg.", "deleteAccount.section.inApp.item1": "Android: Einstellungen → Premium → Konto löschen.", "deleteAccount.section.inApp.item2": "iPhone, iPad und Mac: in den Premium-Einstellungen der App.", - "deleteAccount.section.email.title": "Per E-Mail löschen", - "deleteAccount.section.email.p1": "Wenn du die App nicht nutzen kannst, schreib eine E-Mail an hello@typewhisper.com mit dem Betreff „Delete my TypeWhisper account“. Schreib von der E-Mail-Adresse des Google- oder Apple-Kontos, mit dem du dich angemeldet hast, und gib an, ob du Google oder Apple genutzt hast.", + "deleteAccount.section.email.title": "Im Web löschen", + "deleteAccount.section.email.p1": "Ohne App öffnest du app.typewhisper.com/account/delete, meldest dich mit dem Google- oder Apple-Konto an, das du für TypeWhisper nutzt, und löschst das Konto dort. Wenn etwas nicht klappt, schreib an hello@typewhisper.com.", "deleteAccount.section.deleted.title": "Was gelöscht wird", "deleteAccount.section.deleted.p1": "Beim Löschen entfernt der TypeWhisper-Premium-Dienst:", "deleteAccount.section.deleted.item1": "Dein TypeWhisper-Konto", diff --git a/src/i18n/locales/de/privacy.json b/src/i18n/locales/de/privacy.json index aa03e2c7..3bdd0180 100644 --- a/src/i18n/locales/de/privacy.json +++ b/src/i18n/locales/de/privacy.json @@ -32,7 +32,7 @@ "privacy.section.android.tracking.title": "Kein Tracking", "privacy.section.android.tracking.p1": "Die Android-App enthält keine SDKs für Werbung, Analyse, Absturzberichte oder Tracking. Feedback erreicht uns nur als E-Mail, die du selbst aus deiner Mail-App sendest. Sie kann App-Version, Android-Version, Gerätemodell und ein kurzes Fehlerprotokoll ohne diktierten Text enthalten.", "privacy.section.deleteAccount.title": "Konto löschen", - "privacy.section.deleteAccount.beforeLink": "Du kannst dein TypeWhisper-Premium-Konto in der App löschen (unter Android: Einstellungen → Premium → Konto löschen) oder per E-Mail. Die ", + "privacy.section.deleteAccount.beforeLink": "Du kannst dein TypeWhisper-Premium-Konto in der App löschen (unter Android: Einstellungen → Premium → Konto löschen) oder im Web. Die ", "privacy.section.deleteAccount.linkText": "Seite zur Kontolöschung", "privacy.section.deleteAccount.afterLink": " erklärt beide Wege und was gelöscht wird.", "privacy.section.contributions.title": "Optionale Korrekturbeiträge", diff --git a/src/i18n/locales/en/delete-account.json b/src/i18n/locales/en/delete-account.json index aa0a0cc0..c3b516aa 100644 --- a/src/i18n/locales/en/delete-account.json +++ b/src/i18n/locales/en/delete-account.json @@ -2,15 +2,15 @@ "deleteAccount.meta.title": "Delete your TypeWhisper account - TypeWhisper", "deleteAccount.meta.description": "How to delete your TypeWhisper Premium account on Android, iPhone, iPad, and Mac, and what is deleted.", "deleteAccount.heading": "Delete your TypeWhisper account", - "deleteAccount.lede": "You can delete your TypeWhisper Premium account in the app or by email.", + "deleteAccount.lede": "You can delete your TypeWhisper Premium account in the app or on the web.", "deleteAccount.section.who.title": "Who this is for", "deleteAccount.section.who.p1": "This page is for TypeWhisper Premium accounts on Android, iPhone, iPad, and Mac. You have such an account if you signed in with Google or Apple to use Premium. TypeWhisper works without an account; if you never signed in, there is nothing to delete.", "deleteAccount.section.inApp.title": "Delete in the app", "deleteAccount.section.inApp.p1": "This is the fastest way.", "deleteAccount.section.inApp.item1": "Android: Settings → Premium → Delete account.", "deleteAccount.section.inApp.item2": "iPhone, iPad, and Mac: in the app's Premium settings.", - "deleteAccount.section.email.title": "Delete by email", - "deleteAccount.section.email.p1": "If you can't use the app, send an email to hello@typewhisper.com with the subject “Delete my TypeWhisper account”. Write from the email address of the Google or Apple account you signed in with, and tell us whether you used Google or Apple.", + "deleteAccount.section.email.title": "Delete on the web", + "deleteAccount.section.email.p1": "Without the app, open app.typewhisper.com/account/delete, sign in with the Google or Apple account you use for TypeWhisper, and delete the account there. If something does not work, write to hello@typewhisper.com.", "deleteAccount.section.deleted.title": "What is deleted", "deleteAccount.section.deleted.p1": "When the account is deleted, the TypeWhisper Premium service removes:", "deleteAccount.section.deleted.item1": "Your TypeWhisper account", diff --git a/src/i18n/locales/en/privacy.json b/src/i18n/locales/en/privacy.json index baf7d08d..a4c01d3a 100644 --- a/src/i18n/locales/en/privacy.json +++ b/src/i18n/locales/en/privacy.json @@ -32,7 +32,7 @@ "privacy.section.android.tracking.title": "No tracking", "privacy.section.android.tracking.p1": "The Android app contains no advertising, analytics, crash reporting, or tracking SDKs. Feedback reaches us only as an email that you send from your own mail app. It can include the app version, Android version, device model, and a short error log without dictated text.", "privacy.section.deleteAccount.title": "Deleting Your Account", - "privacy.section.deleteAccount.beforeLink": "You can delete your TypeWhisper Premium account in the app (on Android: Settings → Premium → Delete account) or by email. The ", + "privacy.section.deleteAccount.beforeLink": "You can delete your TypeWhisper Premium account in the app (on Android: Settings → Premium → Delete account) or on the web. The ", "privacy.section.deleteAccount.linkText": "account deletion page", "privacy.section.deleteAccount.afterLink": " explains both ways and what is deleted.", "privacy.section.contributions.title": "Optional Correction Contributions", From 994439bc0a4fb04ec317e0c19059159de3c23fc8 Mon Sep 17 00:00:00 2001 From: SeoFood Date: Wed, 7 Oct 2026 21:16:05 +0200 Subject: [PATCH 3/7] Expect the web deletion link instead of the email subject in the test --- tests/android-beta.spec.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/android-beta.spec.ts b/tests/android-beta.spec.ts index 1ecc9714..80a728b1 100644 --- a/tests/android-beta.spec.ts +++ b/tests/android-beta.spec.ts @@ -45,7 +45,7 @@ for (const locale of ["en", "de"] as const) { await expect(notice.locator('a[href^="https://discord.gg/"]')).toBeVisible(); }); - test("the account deletion page names the app path and the email", async ({ + test("the account deletion page names the app path and the web page", async ({ page, }) => { await page.goto(`/${locale}/delete-account/`); @@ -55,9 +55,11 @@ for (const locale of ["en", "de"] as const) { ).toBeVisible(); const body = page.getByTestId("delete-account"); await expect(body).toContainText("Premium →"); - await expect(body).toContainText("Delete my TypeWhisper account"); await expect( - body.locator('a[href^="mailto:hello@typewhisper.com"]'), + body.locator('a[href="https://app.typewhisper.com/account/delete"]'), + ).toBeVisible(); + await expect( + body.locator('a[href="mailto:hello@typewhisper.com"]'), ).toBeVisible(); await expect( body.locator(`a[href="/${locale}/privacy/"]`), From 4507909bdb1c98e66d41954774c2407511694a59 Mon Sep 17 00:00:00 2001 From: SeoFood Date: Wed, 7 Oct 2026 21:27:46 +0200 Subject: [PATCH 4/7] Keep the Android beta unlisted until the closed test is live; scope the open-source note - androidBeta.listed (false) hides the footer link and the release status entry; the page itself stays at /android/ for when the test is published. - The open source section names what is on GitHub (macOS, Windows, plugins) instead of claiming the entire source, and the Android section no longer contradicts it. - The web deletion page this PR links is live on app.typewhisper.com. --- src/components/layout/footer.astro | 9 ++++++--- src/i18n/locales/de/privacy.json | 4 ++-- src/i18n/locales/en/privacy.json | 4 ++-- src/lib/android-beta.ts | 5 +++++ src/pages/[locale]/release-status.astro | 27 +++++++++++++++---------- tests/android-beta.spec.ts | 11 ++++------ tests/android-beta.test.ts | 3 ++- 7 files changed, 37 insertions(+), 26 deletions(-) diff --git a/src/components/layout/footer.astro b/src/components/layout/footer.astro index be855546..087e9404 100644 --- a/src/components/layout/footer.astro +++ b/src/components/layout/footer.astro @@ -9,6 +9,7 @@ import { import { mulberry32 } from "@/lib/seeded-random"; import type { Locale } from "@/i18n/index"; import { t, localePath } from "@/i18n/index"; +import { androidBeta } from "@/lib/android-beta"; interface Props { locale?: Locale; @@ -139,9 +140,11 @@ const community = [ data-tracking-placement="footer">{t(locale, "footer.ios")} -
  • - {t(locale, "footer.android")} -
  • + {androidBeta.listed && ( +
  • + {t(locale, "footer.android")} +
  • + )} diff --git a/src/i18n/locales/de/privacy.json b/src/i18n/locales/de/privacy.json index 3bdd0180..d4b86020 100644 --- a/src/i18n/locales/de/privacy.json +++ b/src/i18n/locales/de/privacy.json @@ -18,7 +18,7 @@ "privacy.section.premiumSync.title": "Optionales Premium-Konto und Sync", "privacy.section.premiumSync.p1": "Premium Sync ist optional. Über „Mit Apple anmelden“ erhält der TypeWhisper-Berechtigungsdienst eine stabile Konto-ID. Der Dienst speichert ausschließlich gehashte Konto-, Lizenz-, StoreKit-Transaktions-, Sitzungs- und Gerätereferenzen sowie Berechtigungsstatus, Tarif, Ablaufdatum, Gerätelimit und Prüfzeitpunkt. Polar-Lizenzschlüssel werden nur zur Validierung verwendet und weder im Original gespeichert noch protokolliert. Inhalte aus Wörterbuch und Snippets laufen niemals über den TypeWhisper-Berechtigungsdienst: Sie verbleiben im privaten iCloud-Container oder in dem von dir gewählten Files-/Finder-Ordner. Abmeldung oder eine abgelaufene Berechtigung pausiert den Sync, ohne lokale oder bereits synchronisierte Daten zu löschen. Die Kontolöschung in der App entfernt das TypeWhisper-Konto und seine Berechtigungsverknüpfungen; Cloud-Dateien bleiben bestehen, bis du sie separat löschst.", "privacy.section.android.title": "Android-App (Beta)", - "privacy.section.android.p1": "Die Android-App ist in der Beta und arbeitet ebenso lokal. Dieser Abschnitt beschreibt, was sie mit deinen Daten macht. Ihr Quellcode ist nicht öffentlich.", + "privacy.section.android.p1": "Die Android-App ist in der Beta und arbeitet ebenso lokal. Dieser Abschnitt beschreibt, was sie mit deinen Daten macht.", "privacy.section.android.speech.title": "Spracherkennung", "privacy.section.android.speech.p1": "Standardmäßig wandelt die App Sprache auf deinem Handy in Text um, mit Parakeet, Whisper oder Nemotron. Audio verlässt das Handy nur, wenn du eine Cloud-Spracherkennung wählst und deinen eigenen API-Schlüssel einträgst. Es geht dann direkt von deinem Handy an diesen Anbieter: Groq, OpenAI, ElevenLabs, Soniox, Meta, Reson8, Deepgram, AssemblyAI oder deinen eigenen Server. Modelldateien werden von Hugging Face geladen; die App sendet dabei keine Konto- oder Gerätekennungen mit.", "privacy.section.android.writing.title": "Schreibprofile", @@ -45,7 +45,7 @@ "privacy.section.securityReview.beforeLink": "Für Kanzlei-IT und Datenschutzprüfung gibt es zusätzlich eine ", "privacy.section.securityReview.linkText": "öffentliche Prüfliste zu Datenfluss, Speicherorten und optionalen Cloud-Bausteinen", "privacy.section.openSource.title": "Open Source", - "privacy.section.openSource.p1": "TypeWhisper ist Open Source unter der GPLv3-Lizenz. Du kannst den gesamten Quellcode auf GitHub einsehen.", + "privacy.section.openSource.p1": "TypeWhisper ist Open Source unter der GPLv3-Lizenz. Den Quellcode der macOS- und Windows-App und der Plugins findest du auf GitHub; die Apps für iPhone, iPad und Android sind dort noch nicht veröffentlicht.", "privacy.section.contact.title": "Kontakt", "privacy.section.contact.p1": "Bei Fragen zu dieser Datenschutzerklärung schreib an hello@typewhisper.com oder erstelle ein Issue in unserer GitHub-Organisation." } diff --git a/src/i18n/locales/en/privacy.json b/src/i18n/locales/en/privacy.json index a4c01d3a..92fce6b6 100644 --- a/src/i18n/locales/en/privacy.json +++ b/src/i18n/locales/en/privacy.json @@ -18,7 +18,7 @@ "privacy.section.premiumSync.title": "Optional Premium Account and Sync", "privacy.section.premiumSync.p1": "Premium Sync is optional. Sign in with Apple gives the TypeWhisper entitlement service a stable account identifier. The service stores only hashed account, license, StoreKit transaction, session, and device references plus entitlement status, tier, expiry, device limit, and verification time. Polar license keys are used for validation and are not stored or logged in their original form. Dictionary and snippet contents never pass through the TypeWhisper entitlement service: they remain in the private iCloud container or Files/Finder folder selected by you. Signing out or an expired entitlement pauses sync without deleting local or already synchronized data. In-app account deletion removes the TypeWhisper account and its entitlement links; cloud files remain until you delete them separately.", "privacy.section.android.title": "Android App (Beta)", - "privacy.section.android.p1": "The Android app is in beta and follows the same local-first approach. This section describes what it does with your data. Its source code is not public.", + "privacy.section.android.p1": "The Android app is in beta and follows the same local-first approach. This section describes what it does with your data.", "privacy.section.android.speech.title": "Speech recognition", "privacy.section.android.speech.p1": "By default, the app turns speech into text on your phone, with Parakeet, Whisper, or Nemotron. Audio leaves the phone only if you pick a cloud speech engine and enter your own API key. It then goes directly from your phone to that provider: Groq, OpenAI, ElevenLabs, Soniox, Meta, Reson8, Deepgram, AssemblyAI, or your own server. Model files are downloaded from Hugging Face; the app sends no account or device identifiers with these downloads.", "privacy.section.android.writing.title": "Writing profiles", @@ -45,7 +45,7 @@ "privacy.section.securityReview.beforeLink": "For firm IT and privacy review, there is also a ", "privacy.section.securityReview.linkText": "public checklist covering data flow, storage locations, and optional cloud building blocks", "privacy.section.openSource.title": "Open Source", - "privacy.section.openSource.p1": "TypeWhisper is open source under the GPLv3 license. You can audit the entire source code on GitHub.", + "privacy.section.openSource.p1": "TypeWhisper is open source under the GPLv3 license. The source code of the macOS and Windows apps and of the plugins is on GitHub; the iPhone, iPad, and Android apps are not published there yet.", "privacy.section.contact.title": "Contact", "privacy.section.contact.p1": "If you have questions about this privacy policy, email hello@typewhisper.com or open an issue on our GitHub organization." } diff --git a/src/lib/android-beta.ts b/src/lib/android-beta.ts index 5ae37e71..0ab02cba 100644 --- a/src/lib/android-beta.ts +++ b/src/lib/android-beta.ts @@ -6,4 +6,9 @@ export const androidBeta = { googleGroupUrl: "https://groups.google.com/g/typewhisper-android-beta", playOptInUrl: "https://play.google.com/apps/testing/com.typewhisper.android", feedbackEmail: "hello@typewhisper.com", + /** + * Whether the footer and the release status link the beta page. Stays false until the closed + * test is published on Google Play; before that the opt-in link leads nowhere. + */ + listed: false, } as const; diff --git a/src/pages/[locale]/release-status.astro b/src/pages/[locale]/release-status.astro index fdc9fff8..d09d13b5 100644 --- a/src/pages/[locale]/release-status.astro +++ b/src/pages/[locale]/release-status.astro @@ -4,6 +4,7 @@ import BaseLayout from "@/layouts/BaseLayout.astro"; import { ArrowRight, ArrowUpRight, Download } from "lucide-react"; import { BarMark, PageHead } from "@/components/site"; import { t, localePath, type Locale } from "@/i18n/index"; +import { androidBeta } from "@/lib/android-beta"; import { canonicalPath, getLocaleStaticPaths } from "@/lib/locale-routes"; import { getWindowsStoreUrl, @@ -114,17 +115,21 @@ const editions: { }, ], }, - { - id: "android", - stage: "beta", - links: [ - { - label: t(locale, "releaseStatus.android.join"), - href: localePath(locale, "/android"), - kind: "docs", - }, - ], - }, + ...(androidBeta.listed + ? [ + { + id: "android" as const, + stage: "beta" as const, + links: [ + { + label: t(locale, "releaseStatus.android.join"), + href: localePath(locale, "/android"), + kind: "docs" as const, + }, + ], + }, + ] + : []), ]; --- diff --git a/tests/android-beta.spec.ts b/tests/android-beta.spec.ts index 80a728b1..9993c07b 100644 --- a/tests/android-beta.spec.ts +++ b/tests/android-beta.spec.ts @@ -66,7 +66,7 @@ for (const locale of ["en", "de"] as const) { ).toBeVisible(); }); - test("privacy, footer, and release status link to the new pages", async ({ + test("privacy and footer link the deletion page; the beta stays unlisted", async ({ page, }) => { await page.goto(`/${locale}/privacy/`); @@ -75,20 +75,17 @@ for (const locale of ["en", "de"] as const) { page.locator(`.site-prose a[href="/${locale}/delete-account/"]`), ).toBeVisible(); + // The beta page is not linked until the closed test is published (androidBeta.listed). const footer = page.locator("footer"); await expect( footer.getByRole("link", { name: copy[locale].android }), - ).toHaveAttribute("href", `/${locale}/android/`); + ).toHaveCount(0); await expect( footer.getByRole("link", { name: copy[locale].deleteLink }), ).toHaveAttribute("href", `/${locale}/delete-account/`); await page.goto(`/${locale}/release-status/`); - const android = page.getByTestId("release-status-android"); - await expect(android).toContainText("Beta"); - await expect( - android.locator(`a[href="/${locale}/android/"]`), - ).toBeVisible(); + await expect(page.getByTestId("release-status-android")).toHaveCount(0); }); }); } diff --git a/tests/android-beta.test.ts b/tests/android-beta.test.ts index bc3c10de..df7b9052 100644 --- a/tests/android-beta.test.ts +++ b/tests/android-beta.test.ts @@ -22,8 +22,9 @@ test("the unprefixed Android and account deletion URLs redirect to a locale", as } }); -test("the footer links the Android beta and the account deletion page", async () => { +test("the footer links the Android beta only once it is listed, and the account deletion page", async () => { const footer = await readFile("src/components/layout/footer.astro", "utf8"); + assert.match(footer, /androidBeta\.listed && \(/); assert.match(footer, /localePath\(locale, "\/android"\)/); assert.match(footer, /localePath\(locale, "\/delete-account"\)/); }); From 4b6c0508099215a161ebac8d8b49945cbafa9e9a Mon Sep 17 00:00:00 2001 From: SeoFood Date: Wed, 7 Oct 2026 21:37:58 +0200 Subject: [PATCH 5/7] Do not announce the Android beta before it opens; name purchase references - Release status copy no longer calls Android a current beta. - The beta page offers the Google Group, but shows a note instead of the Play opt-in until androidBeta.listed is set. - The deletion page says entitlements include the hashed App Store, Google Play and Polar purchase references. - The privacy policy says Discord's policy applies to feedback posted there. --- src/i18n/locales/de/android.json | 1 + src/i18n/locales/de/common.json | 4 ++-- src/i18n/locales/de/delete-account.json | 2 +- src/i18n/locales/de/privacy.json | 2 +- src/i18n/locales/en/android.json | 1 + src/i18n/locales/en/common.json | 4 ++-- src/i18n/locales/en/delete-account.json | 2 +- src/i18n/locales/en/privacy.json | 2 +- src/pages/[locale]/android.astro | 14 +++++++++----- tests/android-beta.spec.ts | 8 ++++---- 10 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/i18n/locales/de/android.json b/src/i18n/locales/de/android.json index 4e229bf0..692a0f47 100644 --- a/src/i18n/locales/de/android.json +++ b/src/i18n/locales/de/android.json @@ -14,6 +14,7 @@ "android.join.step2.title": "Melde dich an und installiere aus Google Play", "android.join.step2.text": "Öffne die Testseite mit dem Google-Konto, mit dem du der Gruppe beigetreten bist, tippe auf Tester werden und installiere TypeWhisper dann aus Google Play. Nach dem Beitritt zur Gruppe kann es ein paar Minuten dauern, bis die Seite dich hereinlässt.", "android.join.step2.button": "Bei Google Play anmelden", + "android.join.step2.soon": "Die Testseite bei Google Play öffnet, sobald die Beta startet. Tritt der Gruppe schon jetzt bei, dann kannst du sofort einsteigen.", "android.try.label": "Was du ausprobieren kannst", "android.try.title": "Was du ausprobieren kannst", "android.try.lede": "Alles hier ist in der Beta enthalten. Probier aus, was du jeden Tag nutzen würdest.", diff --git a/src/i18n/locales/de/common.json b/src/i18n/locales/de/common.json index 14c05197..f318bbfa 100644 --- a/src/i18n/locales/de/common.json +++ b/src/i18n/locales/de/common.json @@ -69,8 +69,8 @@ "changelog.title": "Changelog - TypeWhisper", "changelog.description": "Release Notes und Updates für TypeWhisper.", "releaseStatus.title": "Release-Status", - "releaseStatus.meta.description": "Aktuelle Release-Stufen für TypeWhisper auf macOS, Windows, iOS und Android.", - "releaseStatus.subtitle": "TypeWhisper bietet stabile Editionen für macOS, Windows, iPhone und iPad. Android ist in der Beta.", + "releaseStatus.meta.description": "Aktuelle Release-Stufen für TypeWhisper auf macOS, Windows und iOS.", + "releaseStatus.subtitle": "TypeWhisper bietet stabile Editionen für macOS, Windows, iPhone und iPad.", "releaseStatus.mac.intro": "macOS {macSeries} ist das unterstützte stabile Release.", "releaseStatus.mac.detail": "Dies ist der offizielle stabile Pfad für täglichen Gebrauch, Dokumentation, Support und Launch-Kommunikation.", "releaseStatus.win.intro": "Windows ist ein stabiles Desktop-Release.", diff --git a/src/i18n/locales/de/delete-account.json b/src/i18n/locales/de/delete-account.json index 3c93aec4..1b636273 100644 --- a/src/i18n/locales/de/delete-account.json +++ b/src/i18n/locales/de/delete-account.json @@ -15,7 +15,7 @@ "deleteAccount.section.deleted.p1": "Beim Löschen entfernt der TypeWhisper-Premium-Dienst:", "deleteAccount.section.deleted.item1": "Dein TypeWhisper-Konto", "deleteAccount.section.deleted.item2": "Die damit verbundenen Geräte", - "deleteAccount.section.deleted.item3": "Deine Premium-Berechtigungen", + "deleteAccount.section.deleted.item3": "Deine Premium-Berechtigungen, samt den gehashten Verweisen auf deine Käufe im App Store, bei Google Play und bei Polar", "deleteAccount.section.deleted.item4": "Die Einträge zu deinen Käufen über Google Play", "deleteAccount.section.deleted.item5": "Deine Anmeldesitzungen", "deleteAccount.section.deleted.p2": "Ist eine Polar-Lizenz von typewhisper.com mit dem Konto verknüpft, werden ihre Aktivierungen für dieses Konto deaktiviert.", diff --git a/src/i18n/locales/de/privacy.json b/src/i18n/locales/de/privacy.json index d4b86020..39204dc8 100644 --- a/src/i18n/locales/de/privacy.json +++ b/src/i18n/locales/de/privacy.json @@ -30,7 +30,7 @@ "privacy.section.android.premium.title": "Premium unter Android", "privacy.section.android.premium.p1": "Premium kaufst du über Google Play Billing. Um Premium an ein Konto zu binden, meldest du dich mit Google oder Apple an. Der TypeWhisper-Premium-Dienst (app.typewhisper.com, gehostet bei Cloudflare) speichert nur eine gehashte Konto-ID, eine gehashte zufällige Installations-ID, gehashte Google-Play-Kauftokens und deine Berechtigung. Name und E-Mail-Adresse speichert er nicht. Eine auf typewhisper.com gekaufte Polar-Lizenz lässt sich im Web mit dem Konto verknüpfen.", "privacy.section.android.tracking.title": "Kein Tracking", - "privacy.section.android.tracking.p1": "Die Android-App enthält keine SDKs für Werbung, Analyse, Absturzberichte oder Tracking. Feedback erreicht uns nur als E-Mail, die du selbst aus deiner Mail-App sendest. Sie kann App-Version, Android-Version, Gerätemodell und ein kurzes Fehlerprotokoll ohne diktierten Text enthalten.", + "privacy.section.android.tracking.p1": "Die Android-App enthält keine SDKs für Werbung, Analyse, Absturzberichte oder Tracking. Feedback aus der App erreicht uns nur als E-Mail, die du selbst aus deiner Mail-App sendest. Sie kann App-Version, Android-Version, Gerätemodell und ein kurzes Fehlerprotokoll ohne diktierten Text enthalten. Schreibst du uns stattdessen auf Discord, gilt für deine Beiträge dort die Datenschutzerklärung von Discord.", "privacy.section.deleteAccount.title": "Konto löschen", "privacy.section.deleteAccount.beforeLink": "Du kannst dein TypeWhisper-Premium-Konto in der App löschen (unter Android: Einstellungen → Premium → Konto löschen) oder im Web. Die ", "privacy.section.deleteAccount.linkText": "Seite zur Kontolöschung", diff --git a/src/i18n/locales/en/android.json b/src/i18n/locales/en/android.json index 5e0e9851..6ae69156 100644 --- a/src/i18n/locales/en/android.json +++ b/src/i18n/locales/en/android.json @@ -14,6 +14,7 @@ "android.join.step2.title": "Opt in and install from Google Play", "android.join.step2.text": "Open the testing page with the Google account that joined the group, tap Become a tester, then install TypeWhisper from Google Play. After joining the group, it can take a few minutes until the page lets you in.", "android.join.step2.button": "Opt in on Google Play", + "android.join.step2.soon": "The Google Play testing page opens once the beta starts. Join the group now so you can opt in right away.", "android.try.label": "What to try", "android.try.title": "What to try", "android.try.lede": "Everything below is in the beta. Try what you would use every day.", diff --git a/src/i18n/locales/en/common.json b/src/i18n/locales/en/common.json index 947bcfa3..27d05fa6 100644 --- a/src/i18n/locales/en/common.json +++ b/src/i18n/locales/en/common.json @@ -69,8 +69,8 @@ "changelog.title": "Changelog - TypeWhisper", "changelog.description": "Release notes and updates for TypeWhisper.", "releaseStatus.title": "Release Status", - "releaseStatus.meta.description": "Current release stages for TypeWhisper on macOS, Windows, iOS, and Android.", - "releaseStatus.subtitle": "TypeWhisper ships stable editions for macOS, Windows, iPhone, and iPad. Android is in beta.", + "releaseStatus.meta.description": "Current release stages for TypeWhisper on macOS, Windows, and iOS.", + "releaseStatus.subtitle": "TypeWhisper ships stable editions for macOS, Windows, iPhone, and iPad.", "releaseStatus.mac.intro": "macOS {macSeries} is the supported stable release.", "releaseStatus.mac.detail": "This is the official stable path for daily use, documentation, support, and launch messaging.", "releaseStatus.win.intro": "Windows is a stable desktop release.", diff --git a/src/i18n/locales/en/delete-account.json b/src/i18n/locales/en/delete-account.json index c3b516aa..5a381016 100644 --- a/src/i18n/locales/en/delete-account.json +++ b/src/i18n/locales/en/delete-account.json @@ -15,7 +15,7 @@ "deleteAccount.section.deleted.p1": "When the account is deleted, the TypeWhisper Premium service removes:", "deleteAccount.section.deleted.item1": "Your TypeWhisper account", "deleteAccount.section.deleted.item2": "The devices registered to it", - "deleteAccount.section.deleted.item3": "Your Premium entitlements", + "deleteAccount.section.deleted.item3": "Your Premium entitlements, including the hashed references to your App Store, Google Play, and Polar purchases", "deleteAccount.section.deleted.item4": "The records of your Google Play purchases", "deleteAccount.section.deleted.item5": "Your sign-in sessions", "deleteAccount.section.deleted.p2": "If a Polar license from typewhisper.com is linked to the account, its activations for the account are deactivated.", diff --git a/src/i18n/locales/en/privacy.json b/src/i18n/locales/en/privacy.json index 92fce6b6..dfbfa05f 100644 --- a/src/i18n/locales/en/privacy.json +++ b/src/i18n/locales/en/privacy.json @@ -30,7 +30,7 @@ "privacy.section.android.premium.title": "Premium on Android", "privacy.section.android.premium.p1": "Premium is bought through Google Play Billing. To tie Premium to an account, you sign in with Google or Apple. The TypeWhisper Premium service (app.typewhisper.com, hosted on Cloudflare) stores only a hashed account identifier, a hashed random install ID, hashed Google Play purchase tokens, and your entitlement. It does not store your name or email address. A Polar license bought on typewhisper.com can be linked to the account on the web.", "privacy.section.android.tracking.title": "No tracking", - "privacy.section.android.tracking.p1": "The Android app contains no advertising, analytics, crash reporting, or tracking SDKs. Feedback reaches us only as an email that you send from your own mail app. It can include the app version, Android version, device model, and a short error log without dictated text.", + "privacy.section.android.tracking.p1": "The Android app contains no advertising, analytics, crash reporting, or tracking SDKs. Feedback from the app reaches us only as an email that you send from your own mail app. It can include the app version, Android version, device model, and a short error log without dictated text. If you write to us on Discord instead, Discord's privacy policy applies to what you post there.", "privacy.section.deleteAccount.title": "Deleting Your Account", "privacy.section.deleteAccount.beforeLink": "You can delete your TypeWhisper Premium account in the app (on Android: Settings → Premium → Delete account) or on the web. The ", "privacy.section.deleteAccount.linkText": "account deletion page", diff --git a/src/pages/[locale]/android.astro b/src/pages/[locale]/android.astro index a268703c..9b31d293 100644 --- a/src/pages/[locale]/android.astro +++ b/src/pages/[locale]/android.astro @@ -18,10 +18,11 @@ export function getStaticPaths() { const { locale } = Astro.props; -// Play only lets members of the Google Group opt in, so the group comes first. +// Play only lets members of the Google Group opt in, so the group comes first. Until the closed +// test is published (androidBeta.listed), the Play opt-in leads nowhere and is not offered. const steps = [ - { id: "step1", href: androidBeta.googleGroupUrl }, - { id: "step2", href: androidBeta.playOptInUrl }, + { id: "step1", href: androidBeta.googleGroupUrl, open: true }, + { id: "step2", href: androidBeta.playOptInUrl, open: androidBeta.listed }, ]; const feedback = [ @@ -66,7 +67,10 @@ const feedback = [

    {t(locale, `android.join.${step.id}.title`)}

    {t(locale, `android.join.${step.id}.text`)}

    -
    + {!step.open && ( +

    {t(locale, `android.join.${step.id}.soon`)}

    + )} + {step.open &&
    -
    +
    }
    ))} diff --git a/tests/android-beta.spec.ts b/tests/android-beta.spec.ts index 9993c07b..a0e29a35 100644 --- a/tests/android-beta.spec.ts +++ b/tests/android-beta.spec.ts @@ -33,10 +33,10 @@ for (const locale of ["en", "de"] as const) { "href", groupUrl, ); - await expect(page.getByTestId("android-beta-step2")).toHaveAttribute( - "href", - playUrl, - ); + // The Play opt-in is only offered once the closed test is published (androidBeta.listed). + await expect(page.getByTestId("android-beta-step2")).toHaveCount(0); + await expect(page.getByTestId("android-beta-step2-soon")).toBeVisible(); + await expect(page.locator(`a[href="${playUrl}"]`)).toHaveCount(0); const notice = page.getByTestId("android-beta-notice"); await expect(notice).toContainText("14"); await expect( From 0174d1e640a1c0502fd241a0b9a2f38a9e08bbe0 Mon Sep 17 00:00:00 2001 From: SeoFood Date: Wed, 7 Oct 2026 21:46:33 +0200 Subject: [PATCH 6/7] Keep the unlisted Android beta page out of search and the sitemap The page sets noindex while androidBeta.listed is false, and the sitemap filter now also drops pages that ask not to be indexed. --- scripts/sitemap-filter.mjs | 9 ++++++++- src/pages/[locale]/android.astro | 1 + tests/sitemap-filter.test.mjs | 6 ++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/scripts/sitemap-filter.mjs b/scripts/sitemap-filter.mjs index 398edbbf..d3b5763b 100644 --- a/scripts/sitemap-filter.mjs +++ b/scripts/sitemap-filter.mjs @@ -2,12 +2,18 @@ import { existsSync, readFileSync } from "node:fs"; import { fileURLToPath } from "node:url"; const META_REFRESH_RE = /]+http-equiv=["']?refresh/i; +const NOINDEX_RE = /]+name=["']?robots["']?[^>]+content=["'][^"']*noindex/i; /** A page that only forwards the visitor carries a meta refresh. */ export function isRedirectHtml(html) { return META_REFRESH_RE.test(html); } +/** A page that asks search engines not to index it, e.g. a page that is not live yet. */ +export function isNoIndexHtml(html) { + return NOINDEX_RE.test(html); +} + /** * Filter for @astrojs/sitemap. The sitemap is written after the pages, so the * built HTML tells whether a URL is a redirect page: the unprefixed URLs that @@ -18,6 +24,7 @@ export function createSitemapFilter(distDir) { const pathname = decodeURIComponent(new URL(page).pathname); const file = fileURLToPath(new URL(`.${pathname}index.html`, distDir)); if (!existsSync(file)) return true; - return !isRedirectHtml(readFileSync(file, "utf8")); + const html = readFileSync(file, "utf8"); + return !isRedirectHtml(html) && !isNoIndexHtml(html); }; } diff --git a/src/pages/[locale]/android.astro b/src/pages/[locale]/android.astro index 9b31d293..28d53054 100644 --- a/src/pages/[locale]/android.astro +++ b/src/pages/[locale]/android.astro @@ -44,6 +44,7 @@ const feedback = [ description={t(locale, "android.meta.description")} canonicalPath={canonicalPath(locale, "/android")} locale={locale} + noIndex={!androidBeta.listed} >
    { assert.equal(isRedirectHtml(pageHtml), false); }); +test("isNoIndexHtml recognizes a page that asks not to be indexed", () => { + assert.equal(isNoIndexHtml(''), true); + assert.equal(isNoIndexHtml(pageHtml), false); +}); + test("createSitemapFilter drops redirect pages and keeps content pages", () => { const dist = mkdtempSync(join(tmpdir(), "sitemap-filter-")); try { From fb3da07c8f618c7bc8aa16da1663e22dd619ea2d Mon Sep 17 00:00:00 2001 From: SeoFood Date: Wed, 7 Oct 2026 22:00:15 +0200 Subject: [PATCH 7/7] List every hashed Premium record in the Android privacy section --- src/i18n/locales/de/privacy.json | 2 +- src/i18n/locales/en/privacy.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/i18n/locales/de/privacy.json b/src/i18n/locales/de/privacy.json index 39204dc8..5f68d5df 100644 --- a/src/i18n/locales/de/privacy.json +++ b/src/i18n/locales/de/privacy.json @@ -28,7 +28,7 @@ "privacy.section.android.storage.title": "Speicherung auf deinem Handy", "privacy.section.android.storage.p1": "Verlauf, Wörterbuch, Profile, Snippets und behaltene Aufnahmen bleiben im Speicher der App auf deinem Handy. Die Android-Sicherung ist für die App ausgeschaltet. Stimmprofile für die Sprechererkennung verlassen das Handy nie. API-Schlüssel werden mit dem Android Keystore verschlüsselt gespeichert. Unter Einstellungen → Daten und Sicherung kannst du deine Daten exportieren oder löschen.", "privacy.section.android.premium.title": "Premium unter Android", - "privacy.section.android.premium.p1": "Premium kaufst du über Google Play Billing. Um Premium an ein Konto zu binden, meldest du dich mit Google oder Apple an. Der TypeWhisper-Premium-Dienst (app.typewhisper.com, gehostet bei Cloudflare) speichert nur eine gehashte Konto-ID, eine gehashte zufällige Installations-ID, gehashte Google-Play-Kauftokens und deine Berechtigung. Name und E-Mail-Adresse speichert er nicht. Eine auf typewhisper.com gekaufte Polar-Lizenz lässt sich im Web mit dem Konto verknüpfen.", + "privacy.section.android.premium.p1": "Premium kaufst du über Google Play Billing. Um Premium an ein Konto zu binden, meldest du dich mit Google oder Apple an. Der TypeWhisper-Premium-Dienst (app.typewhisper.com, gehostet bei Cloudflare) speichert nur gehashte Verweise: deine Konto-ID, eine zufällige Installations-ID pro Gerät, deine Anmeldesitzungen, deine Google-Play-Kauftokens und, falls du eine verknüpfst, deine Polar-Lizenz; dazu den Status deiner Berechtigung. Name und E-Mail-Adresse speichert er nicht. Eine auf typewhisper.com gekaufte Polar-Lizenz lässt sich im Web mit dem Konto verknüpfen.", "privacy.section.android.tracking.title": "Kein Tracking", "privacy.section.android.tracking.p1": "Die Android-App enthält keine SDKs für Werbung, Analyse, Absturzberichte oder Tracking. Feedback aus der App erreicht uns nur als E-Mail, die du selbst aus deiner Mail-App sendest. Sie kann App-Version, Android-Version, Gerätemodell und ein kurzes Fehlerprotokoll ohne diktierten Text enthalten. Schreibst du uns stattdessen auf Discord, gilt für deine Beiträge dort die Datenschutzerklärung von Discord.", "privacy.section.deleteAccount.title": "Konto löschen", diff --git a/src/i18n/locales/en/privacy.json b/src/i18n/locales/en/privacy.json index dfbfa05f..6dd848d3 100644 --- a/src/i18n/locales/en/privacy.json +++ b/src/i18n/locales/en/privacy.json @@ -28,7 +28,7 @@ "privacy.section.android.storage.title": "Storage on your phone", "privacy.section.android.storage.p1": "History, dictionary, profiles, snippets, and kept recordings stay in the app's storage on your phone. Android backup is turned off for the app. Voice profiles for speaker detection never leave the phone. API keys are stored encrypted with the Android Keystore. You can export or delete your data in Settings → Data and backup.", "privacy.section.android.premium.title": "Premium on Android", - "privacy.section.android.premium.p1": "Premium is bought through Google Play Billing. To tie Premium to an account, you sign in with Google or Apple. The TypeWhisper Premium service (app.typewhisper.com, hosted on Cloudflare) stores only a hashed account identifier, a hashed random install ID, hashed Google Play purchase tokens, and your entitlement. It does not store your name or email address. A Polar license bought on typewhisper.com can be linked to the account on the web.", + "privacy.section.android.premium.p1": "Premium is bought through Google Play Billing. To tie Premium to an account, you sign in with Google or Apple. The TypeWhisper Premium service (app.typewhisper.com, hosted on Cloudflare) stores only hashed references: your account identifier, a random install ID per device, your sign-in sessions, your Google Play purchase tokens and, if you link one, your Polar license; plus your entitlement status. It does not store your name or email address. A Polar license bought on typewhisper.com can be linked to the account on the web.", "privacy.section.android.tracking.title": "No tracking", "privacy.section.android.tracking.p1": "The Android app contains no advertising, analytics, crash reporting, or tracking SDKs. Feedback from the app reaches us only as an email that you send from your own mail app. It can include the app version, Android version, device model, and a short error log without dictated text. If you write to us on Discord instead, Discord's privacy policy applies to what you post there.", "privacy.section.deleteAccount.title": "Deleting Your Account",