feat: data foundation (models, secure storage, Retrofit API, repository, L10n)

- java.time via core library desugaring (minSdk 24)
- Domain models: CalEvent, AppSettings, account/calendar models
- CredentialStore (EncryptedSharedPreferences) + SettingsStore
- Retrofit CalendarrApi covering all server endpoints
- CalendarRepository: auth, settings, accounts, events (org.json parsing
  for mixed-type payloads), writable calendars, visibility toggles
- Hilt DI (Moshi), dynamic-baseUrl ApiProvider, AuthInterceptor
- L10n (de/en) ported from iOS

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Guido Schmit
2026-05-31 11:53:34 +02:00
parent a015a45265
commit 676b7ee2c6
17 changed files with 1450 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
package com.scarriffle.calendarr.ui
import java.util.Locale
/**
* In-app localization mirroring the iOS `L10n`. The stored language may be
* "de", "en" or "system" (resolve to device language).
*/
object L10n {
fun resolved(stored: String): String {
if (stored == "de" || stored == "en") return stored
val pref = Locale.getDefault().language.lowercase()
return if (pref.startsWith("de")) "de" else "en"
}
fun locale(stored: String): Locale = Locale(resolved(stored))
fun t(key: String, stored: String): String {
val lang = resolved(stored)
return strings[lang]?.get(key) ?: strings["en"]?.get(key) ?: key
}
fun t(key: String, stored: String, vararg args: Any): String {
val template = t(key, stored)
// Support both %@ (iOS) and %s / %d (Java) placeholders.
val javaTemplate = template.replace("%@", "%s")
return runCatching { String.format(locale(stored), javaTemplate, *args) }
.getOrDefault(template)
}
private val strings: Map<String, Map<String, String>> = mapOf(
"de" to mapOf(
"nav.today" to "Heute", "nav.menu" to "Menü", "nav.done" to "Fertig",
"view.month" to "Monat", "view.week" to "Woche", "view.day" to "Tag",
"view.quarter" to "Quartal", "view.agenda" to "Termine", "view.change" to "Ansicht",
"cal.cw" to "KW", "cal.allday" to "Ganztägig",
"cal.no_events_title" to "Keine Termine",
"cal.no_events_body" to "In den nächsten 90 Tagen sind keine Termine vorhanden.",
"cal.loading_more" to "Lade weitere Wochen…", "cal.new_event" to "Neues Ereignis",
"menu.section.settings" to "Einstellungen", "menu.profile" to "Profil",
"menu.appearance" to "Darstellung", "menu.accounts" to "Konten & Kalender",
"menu.server" to "Server", "menu.logout" to "Abmelden", "menu.admin" to "Admin",
"menu.sync" to "Mit Server synchronisieren", "menu.sync.section" to "Synchronisierung",
"settings.title" to "Darstellung", "settings.loading" to "Lade Einstellungen…",
"settings.save" to "Speichern", "settings.saved" to "Gespeichert",
"settings.cache.title" to "Vorladen", "settings.cache.range" to "Zeitraum",
"settings.cache.1m" to "±1 Monat", "settings.cache.3m" to "±3 Monate",
"settings.cache.6m" to "±6 Monate", "settings.cache.1y" to "±1 Jahr",
"settings.language" to "Sprache", "lang.system" to "Systemstandard",
"lang.german" to "Deutsch", "lang.english" to "English",
"settings.colors" to "Farben", "settings.color.primary" to "Primärfarbe",
"settings.color.accent" to "Akzentfarbe", "settings.color.today" to "Heutige-Tag-Farbe",
"settings.color.divider" to "Monatswechsel-Linie", "settings.color.label" to "Monatskürzel",
"settings.textcontrast" to "Schriftkontrast", "settings.linecontrast" to "Linienkontrast",
"settings.calview" to "Kalenderansicht", "settings.defaultview" to "Standardansicht",
"settings.firstweekday" to "Erster Wochentag", "settings.monday" to "Montag",
"settings.sunday" to "Sonntag", "settings.dimpast" to "Vergangene Termine ausgrauen",
"settings.hourheight" to "Stundenhöhe",
"common.cancel" to "Abbrechen", "common.close" to "Schliessen",
"common.ok" to "OK", "common.error" to "Fehler", "common.delete" to "Löschen",
"common.save" to "Sichern", "common.retry" to "Erneut versuchen",
"server.title" to "Server", "server.connected" to "Verbundener Server",
"server.switch" to "Server wechseln", "server.logout_title" to "Abmelden",
"server.version" to "Version",
"profile.title" to "Profil", "profile.loading" to "Lade Profil…",
"profile.account" to "Konto", "profile.username" to "Benutzername",
"profile.role" to "Rolle", "profile.role.admin" to "Administrator",
"profile.role.user" to "Benutzer", "profile.email" to "E-Mail",
"profile.no_email" to "Keine E-Mail", "profile.save_email" to "E-Mail speichern",
"profile.email_saved" to "E-Mail gespeichert", "profile.change_password" to "Passwort ändern",
"profile.current_password" to "Aktuelles Passwort", "profile.new_password" to "Neues Passwort",
"profile.new_password_repeat" to "Neues Passwort wiederholen",
"profile.password_mismatch" to "Passwörter stimmen nicht überein",
"profile.password_changed" to "Passwort geändert",
"profile.twofa" to "Zwei-Faktor-Authentifizierung",
"profile.twofa.active" to "2FA ist aktiviert", "profile.twofa.inactive" to "2FA ist deaktiviert",
"profile.twofa.enable" to "2FA einrichten", "profile.twofa.disable" to "2FA deaktivieren",
"twofa.setup_title" to "2FA einrichten",
"twofa.scan_hint" to "Scanne den QR-Code mit deiner Authenticator-App.",
"twofa.code_placeholder" to "6-stelliger Code", "twofa.activate" to "Aktivieren",
"twofa.disable_title" to "2FA deaktivieren", "twofa.password_placeholder" to "Passwort",
"twofa.disable" to "Deaktivieren",
"event.title_placeholder" to "Titel", "event.allday" to "Ganztägig",
"event.start" to "Start", "event.end" to "Ende", "event.location" to "Ort",
"event.description" to "Beschreibung", "event.calendar_section" to "Kalender",
"event.no_writable" to "Keine beschreibbaren Kalender vorhanden",
"event.calendar_picker" to "Kalender", "event.color_section" to "Farbe",
"event.color" to "Terminfarbe", "event.reset_color" to "Zurücksetzen",
"event.edit_title" to "Termin bearbeiten", "event.new_title" to "Neuer Termin",
"event.save" to "Sichern", "event.add" to "Hinzufügen",
"event.delete_confirm" to "Diesen Termin löschen?",
"accounts.title" to "Konten", "accounts.loading" to "Lade Konten…",
"accounts.caldav.header" to "CalDAV-Konten", "accounts.caldav.empty" to "Keine CalDAV-Konten",
"accounts.caldav.add" to "CalDAV hinzufügen", "accounts.local.header" to "Lokale Kalender",
"accounts.local.empty" to "Keine lokalen Kalender", "accounts.local.add" to "Lokalen Kalender erstellen",
"accounts.ical.header" to "iCal-Abonnements", "accounts.ical.empty" to "Keine Abonnements",
"accounts.ical.add" to "iCal-URL abonnieren", "accounts.google.header" to "Google-Konten",
"accounts.google.empty" to "Keine Google-Konten",
"accounts.google.hint" to "Google-Konten werden über den Browser verknüpft",
"accounts.ha.header" to "Home Assistant", "accounts.ha.empty" to "Keine Home Assistant-Konten",
"accounts.ha.add" to "Home Assistant hinzufügen",
"filter.title" to "Kalender", "filter.empty" to "Keine Kalender vorhanden",
"filter.show_all" to "Alle anzeigen", "filter.hide_all" to "Alle ausblenden",
"filter.button" to "Kalender ein-/ausblenden",
"caldav.display_name" to "Anzeigename", "caldav.url" to "CalDAV-URL",
"caldav.username" to "Benutzername", "caldav.password" to "Passwort",
"caldav.color" to "Farbe", "caldav.connect" to "Verbinden", "caldav.title" to "CalDAV-Konto",
"local.title" to "Lokaler Kalender", "local.name" to "Name", "local.color" to "Farbe",
"local.create" to "Erstellen",
"ical.title" to "iCal abonnieren", "ical.name" to "Name", "ical.url" to "iCal-URL",
"ical.color" to "Farbe", "ical.interval" to "Intervall", "ical.subscribe" to "Abonnieren",
"ical.refresh.15m" to "Alle 15 Min.", "ical.refresh.30m" to "Alle 30 Min.",
"ical.refresh.1h" to "Stündlich", "ical.refresh.6h" to "Alle 6 Std.", "ical.refresh.1d" to "Täglich",
"ha.display_name" to "Anzeigename", "ha.url_placeholder" to "URL (z.B. http://homeassistant.local:8123)",
"ha.token" to "Long-Lived Access Token", "ha.connect" to "Verbinden",
// Auth screens
"auth.server_title" to "Server verbinden", "auth.server_url" to "Server-URL",
"auth.server_hint" to "Gib die Adresse deines Calendarr-Servers ein.",
"auth.continue" to "Weiter", "auth.checking" to "Verbinde…",
"auth.login_title" to "Anmelden", "auth.username" to "Benutzername",
"auth.password" to "Passwort", "auth.login" to "Anmelden",
"auth.totp" to "2FA-Code", "auth.remember" to "Angemeldet bleiben",
"auth.back" to "Zurück",
),
"en" to mapOf(
"nav.today" to "Today", "nav.menu" to "Menu", "nav.done" to "Done",
"view.month" to "Month", "view.week" to "Week", "view.day" to "Day",
"view.quarter" to "Quarter", "view.agenda" to "Agenda", "view.change" to "View",
"cal.cw" to "W", "cal.allday" to "All-day",
"cal.no_events_title" to "No events",
"cal.no_events_body" to "No events in the next 90 days.",
"cal.loading_more" to "Loading more weeks…", "cal.new_event" to "New event",
"menu.section.settings" to "Settings", "menu.profile" to "Profile",
"menu.appearance" to "Appearance", "menu.accounts" to "Accounts & Calendars",
"menu.server" to "Server", "menu.logout" to "Sign out", "menu.admin" to "Admin",
"menu.sync" to "Sync with server", "menu.sync.section" to "Synchronization",
"settings.title" to "Appearance", "settings.loading" to "Loading settings…",
"settings.save" to "Save", "settings.saved" to "Saved",
"settings.cache.title" to "Preloading", "settings.cache.range" to "Range",
"settings.cache.1m" to "±1 month", "settings.cache.3m" to "±3 months",
"settings.cache.6m" to "±6 months", "settings.cache.1y" to "±1 year",
"settings.language" to "Language", "lang.system" to "System default",
"lang.german" to "Deutsch", "lang.english" to "English",
"settings.colors" to "Colors", "settings.color.primary" to "Primary color",
"settings.color.accent" to "Accent color", "settings.color.today" to "Today color",
"settings.color.divider" to "Month divider line", "settings.color.label" to "Month abbreviation",
"settings.textcontrast" to "Text contrast", "settings.linecontrast" to "Line contrast",
"settings.calview" to "Calendar view", "settings.defaultview" to "Default view",
"settings.firstweekday" to "First day of week", "settings.monday" to "Monday",
"settings.sunday" to "Sunday", "settings.dimpast" to "Dim past events",
"settings.hourheight" to "Hour height",
"common.cancel" to "Cancel", "common.close" to "Close",
"common.ok" to "OK", "common.error" to "Error", "common.delete" to "Delete",
"common.save" to "Save", "common.retry" to "Retry",
"server.title" to "Server", "server.connected" to "Connected server",
"server.switch" to "Switch server", "server.logout_title" to "Sign out",
"server.version" to "Version",
"profile.title" to "Profile", "profile.loading" to "Loading profile…",
"profile.account" to "Account", "profile.username" to "Username",
"profile.role" to "Role", "profile.role.admin" to "Administrator",
"profile.role.user" to "User", "profile.email" to "Email",
"profile.no_email" to "No email", "profile.save_email" to "Save email",
"profile.email_saved" to "Email saved", "profile.change_password" to "Change password",
"profile.current_password" to "Current password", "profile.new_password" to "New password",
"profile.new_password_repeat" to "Repeat new password",
"profile.password_mismatch" to "Passwords don't match",
"profile.password_changed" to "Password changed",
"profile.twofa" to "Two-factor authentication",
"profile.twofa.active" to "2FA is enabled", "profile.twofa.inactive" to "2FA is disabled",
"profile.twofa.enable" to "Set up 2FA", "profile.twofa.disable" to "Disable 2FA",
"twofa.setup_title" to "Set up 2FA",
"twofa.scan_hint" to "Scan the QR code with your authenticator app.",
"twofa.code_placeholder" to "6-digit code", "twofa.activate" to "Activate",
"twofa.disable_title" to "Disable 2FA", "twofa.password_placeholder" to "Password",
"twofa.disable" to "Disable",
"event.title_placeholder" to "Title", "event.allday" to "All-day",
"event.start" to "Start", "event.end" to "End", "event.location" to "Location",
"event.description" to "Description", "event.calendar_section" to "Calendar",
"event.no_writable" to "No writable calendars available",
"event.calendar_picker" to "Calendar", "event.color_section" to "Color",
"event.color" to "Event color", "event.reset_color" to "Reset",
"event.edit_title" to "Edit event", "event.new_title" to "New event",
"event.save" to "Save", "event.add" to "Add",
"event.delete_confirm" to "Delete this event?",
"accounts.title" to "Accounts", "accounts.loading" to "Loading accounts…",
"accounts.caldav.header" to "CalDAV accounts", "accounts.caldav.empty" to "No CalDAV accounts",
"accounts.caldav.add" to "Add CalDAV", "accounts.local.header" to "Local calendars",
"accounts.local.empty" to "No local calendars", "accounts.local.add" to "Create local calendar",
"accounts.ical.header" to "iCal subscriptions", "accounts.ical.empty" to "No subscriptions",
"accounts.ical.add" to "Subscribe to iCal URL", "accounts.google.header" to "Google accounts",
"accounts.google.empty" to "No Google accounts",
"accounts.google.hint" to "Google accounts are linked via the browser",
"accounts.ha.header" to "Home Assistant", "accounts.ha.empty" to "No Home Assistant accounts",
"accounts.ha.add" to "Add Home Assistant",
"filter.title" to "Calendars", "filter.empty" to "No calendars available",
"filter.show_all" to "Show all", "filter.hide_all" to "Hide all",
"filter.button" to "Show/hide calendars",
"caldav.display_name" to "Display name", "caldav.url" to "CalDAV URL",
"caldav.username" to "Username", "caldav.password" to "Password",
"caldav.color" to "Color", "caldav.connect" to "Connect", "caldav.title" to "CalDAV account",
"local.title" to "Local calendar", "local.name" to "Name", "local.color" to "Color",
"local.create" to "Create",
"ical.title" to "Subscribe to iCal", "ical.name" to "Name", "ical.url" to "iCal URL",
"ical.color" to "Color", "ical.interval" to "Interval", "ical.subscribe" to "Subscribe",
"ical.refresh.15m" to "Every 15 min", "ical.refresh.30m" to "Every 30 min",
"ical.refresh.1h" to "Hourly", "ical.refresh.6h" to "Every 6 hours", "ical.refresh.1d" to "Daily",
"ha.display_name" to "Display name", "ha.url_placeholder" to "URL (e.g. http://homeassistant.local:8123)",
"ha.token" to "Long-Lived Access Token", "ha.connect" to "Connect",
// Auth screens
"auth.server_title" to "Connect server", "auth.server_url" to "Server URL",
"auth.server_hint" to "Enter the address of your Calendarr server.",
"auth.continue" to "Continue", "auth.checking" to "Connecting…",
"auth.login_title" to "Sign in", "auth.username" to "Username",
"auth.password" to "Password", "auth.login" to "Sign in",
"auth.totp" to "2FA code", "auth.remember" to "Stay signed in",
"auth.back" to "Back",
),
)
}