feat: Erste Stufe der iOS-App - Anmeldung und Kontenliste

Neuer Ordner ios/ mit dem Geruest: Netzwerkschicht, Modelle, Offline-Ablage,
Texte in Deutsch und Englisch, Navigation, der komplette Anmeldeweg (Server
waehlen, anmelden, zweiter Faktor, Pflicht-Passwortwechsel) und als erste
Fachansicht die Kontenliste.

Die Anmeldung laeuft einmal ueber das Sitzungs-Cookie, holt sich dabei eine
App-Marke und legt sie in den Schluesselbund. Danach traegt nur noch der
Header. Kein Passwort auf dem Geraet, kein Ablauf nach 30 Tagen.

Der Code ist nie uebersetzt worden - hier gibt es kein Xcode. Geprueft ist
dafuer jedes Modellfeld gegen echte Antworten des laufenden Backends; dabei
kam heraus, dass created_at ohne Zeitzone und mit Mikrosekunden kommt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-20 16:20:03 +02:00
co-authored by Claude Opus 5
parent 8a7545bf5b
commit 6b86a6bf91
25 changed files with 2908 additions and 0 deletions
+104
View File
@@ -0,0 +1,104 @@
/* Certain for iOS – English
Keys mirror de.lproj line for line. Keep them that way: in the web app
de.json and en.json are the same length, and that is why nothing is ever
missing there. */
/* --- General --- */
"allgemein.nochmal" = "Try again";
"allgemein.abbrechen" = "Cancel";
"allgemein.abmelden" = "Sign out";
/* --- Tabs --- */
"reiter.konten" = "Accounts";
"reiter.einstellungen" = "Settings";
/* --- Errors --- */
"fehler.titel" = "That did not work";
"fehler.unbekannt" = "Unknown error.";
"fehler.keineVerbindung" = "Cannot reach the server: %@";
"fehler.keinCertainServer" = "Something answers at this address, but it is not Certain.";
"fehler.nichtAngemeldet" = "This device is no longer signed in. It was probably revoked on the desktop.";
"fehler.passwortWechseln" = "The password has to be changed before anything else.";
"fehler.zweiterFaktor" = "This account needs a second factor.";
"fehler.nichtGefunden" = "Does not exist.";
"fehler.server" = "The server reports an error (%d).";
"fehler.antwortUnlesbar" = "The server's answer does not match what the app expects: %@";
/* --- Offline --- */
"stand.hinweis" = "As of %@ – the server cannot be reached right now.";
/* --- Server address --- */
"server.titel" = "Connect to Certain";
"server.ueberschrift" = "Address";
"server.platzhalter" = "budget.example.com";
"server.erklaerung" = "The same address you open Certain with in your browser. Leaving off https is fine.";
"server.verbinden" = "Connect";
"server.keineAdresse" = "That does not look like an address.";
/* --- Sign in --- */
"anmeldung.titel" = "Sign in";
"anmeldung.benutzer" = "Username";
"anmeldung.passwort" = "Password";
"anmeldung.anmelden" = "Sign in";
"anmeldung.erklaerung" = "Just this once. Afterwards the device keeps a key of its own, and your password is stored nowhere.";
"anmeldung.andererServer" = "Choose a different server";
"anmeldung.aktuellerServer" = "Connected to %@";
"anmeldung.markeOhneSchluessel" = "The server returned no key. Please try again.";
/* --- Second factor --- */
"zweifaktor.titel" = "Second factor";
"zweifaktor.ueberschrift" = "Code";
"zweifaktor.platzhalterCode" = "123456";
"zweifaktor.platzhalterWiederherstellung" = "ABCDE-FGHIJ";
"zweifaktor.erklaerungCode" = "The six digits from your authenticator app.";
"zweifaktor.erklaerungWiederherstellung" = "One of the codes you got when setting this up. Each works once.";
"zweifaktor.bestaetigen" = "Confirm";
"zweifaktor.dochWiederherstellung" = "Use a recovery code instead";
"zweifaktor.dochCode" = "Use the code from the app instead";
"zweifaktor.abbrechen" = "Cancel sign-in";
"zweifaktor.verbraucht" = "Too many wrong attempts. Please start over.";
/* --- Second factor missing --- */
"zweifaktorEinrichten.titel" = "Second factor required";
"zweifaktorEinrichten.erklaerung" = "The policy requires a second factor for this account. Setting one up still only works in the browser; the app will get it later.";
"zweifaktorEinrichten.imBrowser" = "Open in browser";
/* --- Password change --- */
"passwort.titel" = "Change password";
"passwort.bisher" = "Current password";
"passwort.neu" = "New password";
"passwort.wiederholen" = "Once more";
"passwort.erklaerung" = "This account has to change its password. The server allows nothing else until then.";
"passwort.stimmtNichtUeberein" = "These two do not match.";
"passwort.speichern" = "Change password";
/* --- Accounts --- */
"konten.titel" = "Accounts";
"konten.leerTitel" = "No accounts yet";
"konten.leerErklaerung" = "Create your first account in the browser; the app will get this in a later stage.";
"konten.archivierteZeigen" = "Show archived";
"konten.archiviert" = "archived";
"konten.gebunden" = "Money set aside";
"konten.gebundenSumme" = "Of which set aside";
"konten.verfuegbar" = "Available";
"konten.bankWeichtAb" = "The bank reports a different balance";
/* --- Account types --- */
"kontoart.girokonto" = "Current account";
"kontoart.sparkonto" = "Savings account";
"kontoart.bargeld" = "Cash";
"kontoart.kreditkarte" = "Credit card";
"kontoart.depot" = "Securities account";
"kontoart.sonstiges" = "Other";
/* --- Settings --- */
"einstellungen.zugang" = "Access";
"einstellungen.angemeldetAls" = "Signed in as";
"einstellungen.server" = "Server";
"einstellungen.markeErklaerung" = "This device holds a key of its own. You can revoke it any time on the desktop under Settings → Device keys; the app signs out immediately.";
"einstellungen.ausDemServer" = "From the server";
"einstellungen.hauptwaehrung" = "Main currency";
"einstellungen.budgetStarttag" = "Budget starts on";
"einstellungen.abmeldenFrage" = "Sign out on this device?";
"einstellungen.abmeldenErklaerung" = "Signing out also removes this device's key on the server.";