feat(v1.5.0): Complete i18n implementation + Language Selector feature

- Added comprehensive English (strings.xml) and German (strings-de.xml) localization with 400+ strings
- Created new LanguageSettingsScreen with System Default, English, and German options
- Fixed hardcoded German notification toasts in MainActivity and ComposeMainActivity
- Integrated Language selector in Settings as top-level menu item
- Changed ComposeSettingsActivity from ComponentActivity to AppCompatActivity for AppCompatDelegate compatibility
- Added locales_config.xml for Android 13+ Per-App Language support
- Updated Extensions.kt with i18n-aware timestamp formatting (toReadableTime with context)
- Translated all UI strings including settings, toasts, notifications, and error messages
- Added dynamic language display in SettingsMainScreen showing current language

Fixes:
- Notification permission toast now respects system language setting
- Activity correctly restarts when language is changed
- All string formatting with parameters properly localized

Migration:
- MainViewModel: All toast messages now use getString()
- SettingsViewModel: All toast and dialog messages localized
- NotificationHelper: Notification titles and messages translated
- UrlValidator: Error messages now accept Context parameter for translation
- NoteCard, DeleteConfirmationDialog, SyncStatusBanner: All strings externalized

Testing completed on device with both EN and DE locale switching.
Closes #5
This commit is contained in:
inventory69
2026-01-16 10:33:38 +01:00
parent 3ada6c966d
commit 3af99f31b8
32 changed files with 1261 additions and 356 deletions

View File

@@ -22,6 +22,7 @@
android:fullBackupContent="@xml/backup_rules" android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.SimpleNotes" android:theme="@style/Theme.SimpleNotes"

View File

@@ -467,10 +467,10 @@ class MainActivity : AppCompatActivity() {
val checkboxAlways = dialogView.findViewById<CheckBox>(R.id.checkboxAlwaysDeleteFromServer) val checkboxAlways = dialogView.findViewById<CheckBox>(R.id.checkboxAlwaysDeleteFromServer)
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setTitle("Notiz löschen") .setTitle(getString(R.string.legacy_delete_dialog_title))
.setMessage("\"${note.title}\" wird lokal gelöscht.\n\nAuch vom Server löschen?") .setMessage(getString(R.string.legacy_delete_dialog_message, note.title))
.setView(dialogView) .setView(dialogView)
.setNeutralButton("Abbrechen") { _, _ -> .setNeutralButton(getString(R.string.cancel)) { _, _ ->
// RESTORE: Re-submit original list (note is NOT deleted from storage) // RESTORE: Re-submit original list (note is NOT deleted from storage)
adapter.submitList(originalList) adapter.submitList(originalList)
} }
@@ -485,7 +485,7 @@ class MainActivity : AppCompatActivity() {
// NOW actually delete from storage // NOW actually delete from storage
deleteNoteLocally(note, deleteFromServer = false) deleteNoteLocally(note, deleteFromServer = false)
} }
.setNegativeButton("Vom Server löschen") { _, _ -> .setNegativeButton(getString(R.string.legacy_delete_from_server)) { _, _ ->
if (checkboxAlways.isChecked) { if (checkboxAlways.isChecked) {
prefs.edit().putBoolean(Constants.KEY_ALWAYS_DELETE_FROM_SERVER, true).apply() prefs.edit().putBoolean(Constants.KEY_ALWAYS_DELETE_FROM_SERVER, true).apply()
} }
@@ -507,13 +507,13 @@ class MainActivity : AppCompatActivity() {
// Show Snackbar with UNDO option // Show Snackbar with UNDO option
val message = if (deleteFromServer) { val message = if (deleteFromServer) {
"\"${note.title}\" wird lokal und vom Server gelöscht" getString(R.string.legacy_delete_with_server, note.title)
} else { } else {
"\"${note.title}\" lokal gelöscht (Server bleibt)" getString(R.string.legacy_delete_local_only, note.title)
} }
Snackbar.make(recyclerViewNotes, message, Snackbar.LENGTH_LONG) Snackbar.make(recyclerViewNotes, message, Snackbar.LENGTH_LONG)
.setAction("RÜCKGÄNGIG") { .setAction(getString(R.string.snackbar_undo)) {
// UNDO: Restore note // UNDO: Restore note
storage.saveNote(note) storage.saveNote(note)
pendingDeletions.remove(note.id) pendingDeletions.remove(note.id)
@@ -535,7 +535,7 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
Toast.makeText( Toast.makeText(
this@MainActivity, this@MainActivity,
"Vom Server gelöscht", getString(R.string.snackbar_deleted_from_server),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -543,7 +543,7 @@ class MainActivity : AppCompatActivity() {
runOnUiThread { runOnUiThread {
Toast.makeText( Toast.makeText(
this@MainActivity, this@MainActivity,
"Server-Löschung fehlgeschlagen", getString(R.string.snackbar_server_delete_failed),
Toast.LENGTH_LONG Toast.LENGTH_LONG
).show() ).show()
} }
@@ -800,10 +800,9 @@ class MainActivity : AppCompatActivity() {
REQUEST_NOTIFICATION_PERMISSION -> { REQUEST_NOTIFICATION_PERMISSION -> {
if (grantResults.isNotEmpty() && if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) { grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showToast("Benachrichtigungen aktiviert") showToast(getString(R.string.toast_notifications_enabled))
} else { } else {
showToast("Benachrichtigungen deaktiviert. " + showToast(getString(R.string.toast_notifications_disabled))
"Du kannst sie in den Einstellungen aktivieren.")
} }
} }
} }

View File

@@ -227,9 +227,9 @@ class SettingsActivity : AppCompatActivity() {
*/ */
private fun updateProtocolHint() { private fun updateProtocolHint() {
protocolHintText.text = if (radioHttp.isChecked) { protocolHintText.text = if (radioHttp.isChecked) {
"HTTP nur für lokale Netzwerke (z.B. 192.168.x.x, 10.x.x.x)" getString(R.string.server_connection_http_hint)
} else { } else {
"HTTPS für sichere Verbindungen über das Internet" getString(R.string.server_connection_https_hint)
} }
} }
@@ -359,7 +359,7 @@ class SettingsActivity : AppCompatActivity() {
60L -> "60 Minuten" 60L -> "60 Minuten"
else -> "$newInterval Minuten" else -> "$newInterval Minuten"
} }
showToast("⏱️ Sync-Intervall auf $intervalText geändert") showToast(getString(R.string.toast_sync_interval_changed, intervalText))
Logger.i(TAG, "Sync interval changed to $newInterval minutes, restarted periodic sync") Logger.i(TAG, "Sync interval changed to $newInterval minutes, restarted periodic sync")
} else { } else {
showToast("⏱️ Sync-Intervall gespeichert (Auto-Sync ist deaktiviert)") showToast("⏱️ Sync-Intervall gespeichert (Auto-Sync ist deaktiviert)")
@@ -379,7 +379,7 @@ class SettingsActivity : AppCompatActivity() {
textViewAppVersion.text = "Version $versionName ($versionCode)" textViewAppVersion.text = "Version $versionName ($versionCode)"
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(TAG, "Failed to load version info", e) Logger.e(TAG, "Failed to load version info", e)
textViewAppVersion.text = "Version nicht verfügbar" textViewAppVersion.text = getString(R.string.version_not_available)
} }
// GitHub Repository Card // GitHub Repository Card
@@ -475,12 +475,12 @@ class SettingsActivity : AppCompatActivity() {
*/ */
private fun showClearLogsConfirmation() { private fun showClearLogsConfirmation() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle("Logs löschen?") .setTitle(getString(R.string.debug_delete_logs_title))
.setMessage("Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht.") .setMessage(getString(R.string.debug_delete_logs_message))
.setPositiveButton("Löschen") { _, _ -> .setPositiveButton(getString(R.string.delete)) { _, _ ->
clearLogs() clearLogs()
} }
.setNegativeButton("Abbrechen", null) .setNegativeButton(getString(R.string.cancel), null)
.show() .show()
} }
@@ -491,13 +491,13 @@ class SettingsActivity : AppCompatActivity() {
try { try {
val cleared = Logger.clearLogFile(this) val cleared = Logger.clearLogFile(this)
if (cleared) { if (cleared) {
showToast("🗑️ Logs gelöscht") showToast(getString(R.string.toast_logs_deleted))
} else { } else {
showToast("📭 Keine Logs zum Löschen") showToast(getString(R.string.toast_no_logs_to_delete))
} }
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(TAG, "Failed to clear logs", e) Logger.e(TAG, "Failed to clear logs", e)
showToast("❌ Fehler beim Löschen: ${e.message}") showToast(getString(R.string.toast_logs_delete_error, e.message ?: ""))
} }
} }
@@ -510,7 +510,7 @@ class SettingsActivity : AppCompatActivity() {
startActivity(intent) startActivity(intent)
} catch (e: Exception) { } catch (e: Exception) {
Logger.e(TAG, "Failed to open URL: $url", e) Logger.e(TAG, "Failed to open URL: $url", e)
showToast("❌ Fehler beim Öffnen des Links") showToast(getString(R.string.toast_link_error))
} }
} }
@@ -524,7 +524,7 @@ class SettingsActivity : AppCompatActivity() {
// 🔥 v1.1.2: Validate HTTP URL (only allow for local networks) // 🔥 v1.1.2: Validate HTTP URL (only allow for local networks)
if (fullUrl.isNotEmpty()) { if (fullUrl.isNotEmpty()) {
val (isValid, errorMessage) = UrlValidator.validateHttpUrl(fullUrl) val (isValid, errorMessage) = UrlValidator.validateHttpUrl(this, fullUrl)
if (!isValid) { if (!isValid) {
// Only show error in TextField (no Toast) // Only show error in TextField (no Toast)
textInputLayoutServerUrl.isErrorEnabled = true textInputLayoutServerUrl.isErrorEnabled = true
@@ -552,7 +552,7 @@ class SettingsActivity : AppCompatActivity() {
// 🔥 v1.1.2: Validate before testing // 🔥 v1.1.2: Validate before testing
if (fullUrl.isNotEmpty()) { if (fullUrl.isNotEmpty()) {
val (isValid, errorMessage) = UrlValidator.validateHttpUrl(fullUrl) val (isValid, errorMessage) = UrlValidator.validateHttpUrl(this, fullUrl)
if (!isValid) { if (!isValid) {
// Only show error in TextField (no Toast) // Only show error in TextField (no Toast)
textInputLayoutServerUrl.isErrorEnabled = true textInputLayoutServerUrl.isErrorEnabled = true
@@ -646,7 +646,7 @@ class SettingsActivity : AppCompatActivity() {
return return
} }
textViewServerStatus.text = "🔍 Prüfe..." textViewServerStatus.text = getString(R.string.status_checking)
textViewServerStatus.setTextColor(getColor(android.R.color.darker_gray)) textViewServerStatus.setTextColor(getColor(android.R.color.darker_gray))
lifecycleScope.launch { lifecycleScope.launch {
@@ -803,12 +803,12 @@ class SettingsActivity : AppCompatActivity() {
.setMessage( .setMessage(
"Damit die App im Hintergrund synchronisieren kann, " + "Damit die App im Hintergrund synchronisieren kann, " +
"muss die Akku-Optimierung deaktiviert werden.\n\n" + "muss die Akku-Optimierung deaktiviert werden.\n\n" +
"Bitte wähle 'Nicht optimieren' für Simple Notes." getString(R.string.battery_optimization_dialog_message)
) )
.setPositiveButton("Einstellungen öffnen") { _, _ -> .setPositiveButton(getString(R.string.battery_optimization_open_settings)) { _, _ ->
openBatteryOptimizationSettings() openBatteryOptimizationSettings()
} }
.setNegativeButton("Später") { dialog, _ -> .setNegativeButton(getString(R.string.battery_optimization_later)) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
} }
.setCancelable(false) .setCancelable(false)
@@ -915,20 +915,20 @@ class SettingsActivity : AppCompatActivity() {
// Radio Buttons erstellen // Radio Buttons erstellen
val radioMerge = android.widget.RadioButton(this).apply { val radioMerge = android.widget.RadioButton(this).apply {
text = "⚪ Zusammenführen (Standard)\n → Neue hinzufügen, Bestehende behalten" text = getString(R.string.backup_mode_merge_full)
id = android.view.View.generateViewId() id = android.view.View.generateViewId()
isChecked = true isChecked = true
setPadding(10, 10, 10, 10) setPadding(10, 10, 10, 10)
} }
val radioReplace = android.widget.RadioButton(this).apply { val radioReplace = android.widget.RadioButton(this).apply {
text = "⚪ Ersetzen\n → Alle löschen & Backup importieren" text = getString(R.string.backup_mode_replace_full)
id = android.view.View.generateViewId() id = android.view.View.generateViewId()
setPadding(10, 10, 10, 10) setPadding(10, 10, 10, 10)
} }
val radioOverwrite = android.widget.RadioButton(this).apply { val radioOverwrite = android.widget.RadioButton(this).apply {
text = "⚪ Duplikate überschreiben\n → Backup gewinnt bei Konflikten" text = getString(R.string.backup_mode_overwrite_full)
id = android.view.View.generateViewId() id = android.view.View.generateViewId()
setPadding(10, 10, 10, 10) setPadding(10, 10, 10, 10)
} }
@@ -978,7 +978,7 @@ class SettingsActivity : AppCompatActivity() {
RestoreSource.WEBDAV_SERVER -> performRestoreFromServer(selectedMode) RestoreSource.WEBDAV_SERVER -> performRestoreFromServer(selectedMode)
} }
} }
.setNegativeButton("Abbrechen", null) .setNegativeButton(getString(R.string.cancel), null)
.show() .show()
} }

View File

@@ -5,6 +5,7 @@ import android.net.Uri
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import dev.dettmer.simplenotes.BuildConfig import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.storage.NotesStorage import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.utils.Logger import dev.dettmer.simplenotes.utils.Logger
@@ -144,7 +145,7 @@ class BackupManager(private val context: Context) {
if (!validationResult.isValid) { if (!validationResult.isValid) {
return@withContext RestoreResult( return@withContext RestoreResult(
success = false, success = false,
error = validationResult.errorMessage ?: "Ungültige Backup-Datei" error = validationResult.errorMessage ?: context.getString(R.string.error_invalid_backup_file)
) )
} }
@@ -171,7 +172,7 @@ class BackupManager(private val context: Context) {
Logger.e(TAG, "Failed to restore backup", e) Logger.e(TAG, "Failed to restore backup", e)
RestoreResult( RestoreResult(
success = false, success = false,
error = "Wiederherstellung fehlgeschlagen: ${e.message}" error = context.getString(R.string.error_restore_failed, e.message ?: "")
) )
} }
} }
@@ -187,8 +188,7 @@ class BackupManager(private val context: Context) {
if (backupData.backupVersion > BACKUP_VERSION) { if (backupData.backupVersion > BACKUP_VERSION) {
return ValidationResult( return ValidationResult(
isValid = false, isValid = false,
errorMessage = "Backup-Version nicht unterstützt " + errorMessage = context.getString(R.string.error_backup_version_unsupported, backupData.backupVersion, BACKUP_VERSION)
"(v${backupData.backupVersion} benötigt v${BACKUP_VERSION}+)"
) )
} }
@@ -196,7 +196,7 @@ class BackupManager(private val context: Context) {
if (backupData.notes.isEmpty()) { if (backupData.notes.isEmpty()) {
return ValidationResult( return ValidationResult(
isValid = false, isValid = false,
errorMessage = "Backup enthält keine Notizen" errorMessage = context.getString(R.string.error_backup_empty)
) )
} }
@@ -208,7 +208,7 @@ class BackupManager(private val context: Context) {
if (invalidNotes.isNotEmpty()) { if (invalidNotes.isNotEmpty()) {
return ValidationResult( return ValidationResult(
isValid = false, isValid = false,
errorMessage = "Backup enthält ${invalidNotes.size} ungültige Notizen" errorMessage = context.getString(R.string.error_backup_invalid_notes, invalidNotes.size)
) )
} }
@@ -217,7 +217,7 @@ class BackupManager(private val context: Context) {
} catch (e: Exception) { } catch (e: Exception) {
ValidationResult( ValidationResult(
isValid = false, isValid = false,
errorMessage = "Backup-Datei beschädigt oder ungültig: ${e.message}" errorMessage = context.getString(R.string.error_backup_corrupt, e.message ?: "")
) )
} }
} }
@@ -241,7 +241,7 @@ class BackupManager(private val context: Context) {
success = true, success = true,
importedNotes = newNotes.size, importedNotes = newNotes.size,
skippedNotes = skippedNotes, skippedNotes = skippedNotes,
message = "${newNotes.size} neue Notizen importiert, $skippedNotes übersprungen" message = context.getString(R.string.restore_merge_result, newNotes.size, skippedNotes)
) )
} }
@@ -262,10 +262,10 @@ class BackupManager(private val context: Context) {
success = true, success = true,
importedNotes = backupNotes.size, importedNotes = backupNotes.size,
skippedNotes = 0, skippedNotes = 0,
message = "Alle Notizen ersetzt: ${backupNotes.size} importiert" message = context.getString(R.string.restore_replace_result, backupNotes.size)
) )
} }
/** /**
* Restore-Modus: OVERWRITE_DUPLICATES * Restore-Modus: OVERWRITE_DUPLICATES
* Backup überschreibt bei ID-Konflikten * Backup überschreibt bei ID-Konflikten
@@ -287,7 +287,7 @@ class BackupManager(private val context: Context) {
importedNotes = newNotes.size, importedNotes = newNotes.size,
skippedNotes = 0, skippedNotes = 0,
overwrittenNotes = overwrittenNotes.size, overwrittenNotes = overwrittenNotes.size,
message = "${newNotes.size} neu, ${overwrittenNotes.size} überschrieben" message = context.getString(R.string.restore_overwrite_result, newNotes.size, overwrittenNotes.size)
) )
} }

View File

@@ -6,6 +6,7 @@ import android.net.NetworkCapabilities
import com.thegrizzlylabs.sardineandroid.Sardine import com.thegrizzlylabs.sardineandroid.Sardine
import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine import com.thegrizzlylabs.sardineandroid.impl.OkHttpSardine
import dev.dettmer.simplenotes.BuildConfig import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.DeletionTracker import dev.dettmer.simplenotes.models.DeletionTracker
import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.SyncStatus import dev.dettmer.simplenotes.models.SyncStatus
@@ -1852,15 +1853,15 @@ class WebDavSyncService(private val context: Context) {
suspend fun manualMarkdownSync(): ManualMarkdownSyncResult = withContext(Dispatchers.IO) { suspend fun manualMarkdownSync(): ManualMarkdownSyncResult = withContext(Dispatchers.IO) {
return@withContext try { return@withContext try {
val sardine = getOrCreateSardine() val sardine = getOrCreateSardine()
?: throw SyncException("Sardine client konnte nicht erstellt werden") ?: throw SyncException(context.getString(R.string.error_sardine_client_failed))
val serverUrl = getServerUrl() val serverUrl = getServerUrl()
?: throw SyncException("Server-URL nicht konfiguriert") ?: throw SyncException(context.getString(R.string.error_server_url_not_configured))
val username = prefs.getString(Constants.KEY_USERNAME, "") ?: "" val username = prefs.getString(Constants.KEY_USERNAME, "") ?: ""
val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: "" val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: ""
if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) { if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) {
throw SyncException("WebDAV-Server nicht vollständig konfiguriert") throw SyncException(context.getString(R.string.error_server_not_configured))
} }
Logger.d(TAG, "🔄 Manual Markdown Sync START") Logger.d(TAG, "🔄 Manual Markdown Sync START")

View File

@@ -24,10 +24,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
import dev.dettmer.simplenotes.models.NoteType import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.models.SyncStatus import dev.dettmer.simplenotes.models.SyncStatus
@@ -339,10 +341,10 @@ class ComposeMainActivity : ComponentActivity() {
REQUEST_NOTIFICATION_PERMISSION -> { REQUEST_NOTIFICATION_PERMISSION -> {
if (grantResults.isNotEmpty() && if (grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) { grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Benachrichtigungen aktiviert", Toast.LENGTH_SHORT).show() Toast.makeText(this, getString(R.string.toast_notifications_enabled), Toast.LENGTH_SHORT).show()
} else { } else {
Toast.makeText(this, Toast.makeText(this,
"Benachrichtigungen deaktiviert. Du kannst sie in den Einstellungen aktivieren.", getString(R.string.toast_notifications_disabled),
Toast.LENGTH_SHORT Toast.LENGTH_SHORT
).show() ).show()
} }
@@ -363,21 +365,21 @@ private fun DeleteConfirmationDialog(
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("Notiz löschen") }, title = { Text(stringResource(R.string.legacy_delete_dialog_title)) },
text = { text = {
Text("\"$noteTitle\" wird lokal gelöscht.\n\nAuch vom Server löschen?") Text(stringResource(R.string.legacy_delete_dialog_message, noteTitle))
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Abbrechen") Text(stringResource(R.string.cancel))
} }
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onDeleteLocal) { TextButton(onClick = onDeleteLocal) {
Text("Nur lokal") Text(stringResource(R.string.delete_local_only))
} }
TextButton(onClick = onDeleteFromServer) { TextButton(onClick = onDeleteFromServer) {
Text("Vom Server löschen") Text(stringResource(R.string.legacy_delete_from_server))
} }
} }
) )

View File

@@ -42,8 +42,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.NoteType import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.sync.SyncStateManager import dev.dettmer.simplenotes.sync.SyncStateManager
import dev.dettmer.simplenotes.ui.main.components.DeleteConfirmationDialog import dev.dettmer.simplenotes.ui.main.components.DeleteConfirmationDialog
@@ -231,7 +233,7 @@ private fun MainTopBar(
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
text = "Simple Notes", text = stringResource(R.string.main_title),
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
}, },
@@ -242,13 +244,13 @@ private fun MainTopBar(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Refresh, imageVector = Icons.Default.Refresh,
contentDescription = "Synchronisieren" contentDescription = stringResource(R.string.action_sync)
) )
} }
IconButton(onClick = onSettingsClick) { IconButton(onClick = onSettingsClick) {
Icon( Icon(
imageVector = Icons.Default.Settings, imageVector = Icons.Default.Settings,
contentDescription = "Einstellungen" contentDescription = stringResource(R.string.action_settings)
) )
} }
}, },
@@ -276,13 +278,13 @@ private fun SelectionTopBar(
IconButton(onClick = onCloseSelection) { IconButton(onClick = onCloseSelection) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = "Auswahl beenden" contentDescription = stringResource(R.string.action_close_selection)
) )
} }
}, },
title = { title = {
Text( Text(
text = "$selectedCount ausgewählt", text = stringResource(R.string.selection_count, selectedCount),
style = MaterialTheme.typography.titleLarge style = MaterialTheme.typography.titleLarge
) )
}, },
@@ -292,7 +294,7 @@ private fun SelectionTopBar(
IconButton(onClick = onSelectAll) { IconButton(onClick = onSelectAll) {
Icon( Icon(
imageVector = Icons.Default.SelectAll, imageVector = Icons.Default.SelectAll,
contentDescription = "Alle auswählen" contentDescription = stringResource(R.string.action_select_all)
) )
} }
} }
@@ -303,7 +305,7 @@ private fun SelectionTopBar(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = "Ausgewählte löschen", contentDescription = stringResource(R.string.action_delete_selected),
tint = if (selectedCount > 0) { tint = if (selectedCount > 0) {
MaterialTheme.colorScheme.error MaterialTheme.colorScheme.error
} else { } else {

View File

@@ -5,6 +5,7 @@ import android.content.Context
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.storage.NotesStorage import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.sync.SyncStateManager import dev.dettmer.simplenotes.sync.SyncStateManager
import dev.dettmer.simplenotes.sync.WebDavSyncService import dev.dettmer.simplenotes.sync.WebDavSyncService
@@ -236,15 +237,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Show snackbar with undo // Show snackbar with undo
val count = selectedNotes.size val count = selectedNotes.size
val message = if (deleteFromServer) { val message = if (deleteFromServer) {
"$count Notiz${if (count > 1) "en" else ""} werden vom Server gelöscht" getString(R.string.snackbar_notes_deleted_server, count)
} else { } else {
"$count Notiz${if (count > 1) "en" else ""} lokal gelöscht" getString(R.string.snackbar_notes_deleted_local, count)
} }
viewModelScope.launch { viewModelScope.launch {
_showSnackbar.emit(SnackbarData( _showSnackbar.emit(SnackbarData(
message = message, message = message,
actionLabel = "RÜCKGÄNGIG", actionLabel = getString(R.string.snackbar_undo),
onAction = { onAction = {
undoDeleteMultiple(selectedNotes) undoDeleteMultiple(selectedNotes)
} }
@@ -336,15 +337,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Show snackbar with undo // Show snackbar with undo
val message = if (deleteFromServer) { val message = if (deleteFromServer) {
"\"${note.title}\" wird vom Server gelöscht" getString(R.string.snackbar_note_deleted_server, note.title)
} else { } else {
"\"${note.title}\" lokal gelöscht" getString(R.string.snackbar_note_deleted_local, note.title)
} }
viewModelScope.launch { viewModelScope.launch {
_showSnackbar.emit(SnackbarData( _showSnackbar.emit(SnackbarData(
message = message, message = message,
actionLabel = "RÜCKGÄNGIG", actionLabel = getString(R.string.snackbar_undo),
onAction = { onAction = {
undoDelete(note) undoDelete(note)
} }
@@ -390,12 +391,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
if (success) { if (success) {
_showToast.emit("Vom Server gelöscht") _showToast.emit(getString(R.string.snackbar_deleted_from_server))
} else { } else {
_showToast.emit("Server-Löschung fehlgeschlagen") _showToast.emit(getString(R.string.snackbar_server_delete_failed))
} }
} catch (e: Exception) { } catch (e: Exception) {
_showToast.emit("Server-Fehler: ${e.message}") _showToast.emit(getString(R.string.snackbar_server_error, e.message ?: ""))
} finally { } finally {
// Remove from pending deletions // Remove from pending deletions
_pendingDeletions.value = _pendingDeletions.value - noteId _pendingDeletions.value = _pendingDeletions.value - noteId
@@ -446,7 +447,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (!isReachable) { if (!isReachable) {
Logger.d(TAG, "⏭️ $source Sync: Server not reachable") Logger.d(TAG, "⏭️ $source Sync: Server not reachable")
SyncStateManager.markError("Server nicht erreichbar") SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
return@launch return@launch
} }
@@ -456,7 +457,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
} }
if (result.isSuccess) { if (result.isSuccess) {
SyncStateManager.markCompleted("${result.syncedCount} Notizen") SyncStateManager.markCompleted(getString(R.string.toast_sync_success, result.syncedCount))
loadNotes() loadNotes()
} else { } else {
SyncStateManager.markError(result.errorMessage) SyncStateManager.markError(result.errorMessage)
@@ -524,8 +525,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (result.isSuccess && result.syncedCount > 0) { if (result.isSuccess && result.syncedCount > 0) {
Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes") Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes")
SyncStateManager.markCompleted("${result.syncedCount} Notizen") SyncStateManager.markCompleted(getString(R.string.toast_sync_success, result.syncedCount))
_showToast.emit("✅ Gesynct: ${result.syncedCount} Notizen") _showToast.emit(getString(R.string.snackbar_synced_count, result.syncedCount))
loadNotes() loadNotes()
} else if (result.isSuccess) { } else if (result.isSuccess) {
Logger.d(TAG, " Auto-sync ($source): No changes") Logger.d(TAG, " Auto-sync ($source): No changes")
@@ -559,6 +560,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Helpers // Helpers
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
private fun getString(resId: Int): String = getApplication<android.app.Application>().getString(resId)
private fun getString(resId: Int, vararg formatArgs: Any): String =
getApplication<android.app.Application>().getString(resId, *formatArgs)
fun isServerConfigured(): Boolean { fun isServerConfigured(): Boolean {
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null) val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
return !serverUrl.isNullOrEmpty() && serverUrl != "http://" && serverUrl != "https://" return !serverUrl.isNullOrEmpty() && serverUrl != "http://" && serverUrl != "https://"

View File

@@ -14,7 +14,9 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
/** /**
* Delete confirmation dialog with server/local options * Delete confirmation dialog with server/local options
@@ -28,15 +30,15 @@ fun DeleteConfirmationDialog(
onDeleteEverywhere: () -> Unit onDeleteEverywhere: () -> Unit
) { ) {
val title = if (noteCount == 1) { val title = if (noteCount == 1) {
"Notiz löschen?" stringResource(R.string.delete_note_title)
} else { } else {
"$noteCount Notizen löschen?" stringResource(R.string.delete_notes_title, noteCount)
} }
val message = if (noteCount == 1) { val message = if (noteCount == 1) {
"Wie möchtest du diese Notiz löschen?" stringResource(R.string.delete_note_message)
} else { } else {
"Wie möchtest du diese $noteCount Notizen löschen?" stringResource(R.string.delete_notes_message, noteCount)
} }
AlertDialog( AlertDialog(
@@ -66,7 +68,7 @@ fun DeleteConfirmationDialog(
contentColor = MaterialTheme.colorScheme.error contentColor = MaterialTheme.colorScheme.error
) )
) { ) {
Text("Überall löschen (auch Server)") Text(stringResource(R.string.delete_everywhere))
} }
// Delete local only // Delete local only
@@ -74,7 +76,7 @@ fun DeleteConfirmationDialog(
onClick = onDeleteLocal, onClick = onDeleteLocal,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text("Nur lokal löschen") Text(stringResource(R.string.delete_local_only))
} }
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -84,7 +86,7 @@ fun DeleteConfirmationDialog(
onClick = onDismiss, onClick = onDismiss,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Text("Abbrechen") Text(stringResource(R.string.cancel))
} }
} }
}, },

View File

@@ -14,9 +14,11 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.dettmer.simplenotes.R
/** /**
* Empty state card shown when no notes exist * Empty state card shown when no notes exist
@@ -52,7 +54,7 @@ fun EmptyState(
// Title // Title
Text( Text(
text = "Noch keine Notizen", text = stringResource(R.string.empty_state_title),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center textAlign = TextAlign.Center
@@ -62,7 +64,7 @@ fun EmptyState(
// Message // Message
Text( Text(
text = "Tippe + um eine neue Notiz zu erstellen", text = stringResource(R.string.empty_state_message),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center textAlign = TextAlign.Center

View File

@@ -39,8 +39,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.Note import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.NoteType import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.models.SyncStatus import dev.dettmer.simplenotes.models.SyncStatus
@@ -66,6 +70,7 @@ fun NoteCard(
onClick: () -> Unit, onClick: () -> Unit,
onLongClick: () -> Unit onLongClick: () -> Unit
) { ) {
val context = LocalContext.current
val borderColor = if (isSelected) { val borderColor = if (isSelected) {
MaterialTheme.colorScheme.primary MaterialTheme.colorScheme.primary
} else { } else {
@@ -137,7 +142,7 @@ fun NoteCard(
// Title // Title
Text( Text(
text = note.title.ifEmpty { "Ohne Titel" }, text = note.title.ifEmpty { stringResource(R.string.untitled) },
style = MaterialTheme.typography.titleMedium, style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
maxLines = 2, maxLines = 2,
@@ -154,7 +159,7 @@ fun NoteCard(
NoteType.TEXT -> note.content.take(100) NoteType.TEXT -> note.content.take(100)
NoteType.CHECKLIST -> { NoteType.CHECKLIST -> {
val items = note.checklistItems ?: emptyList() val items = note.checklistItems ?: emptyList()
"${items.count { it.isChecked }}/${items.size} erledigt" stringResource(R.string.checklist_progress, items.count { it.isChecked }, items.size)
} }
}, },
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
@@ -171,7 +176,7 @@ fun NoteCard(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( Text(
text = note.updatedAt.toReadableTime(), text = note.updatedAt.toReadableTime(context),
style = MaterialTheme.typography.labelSmall, style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline, color = MaterialTheme.colorScheme.outline,
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
@@ -231,7 +236,7 @@ fun NoteCard(
if (isSelected) { if (isSelected) {
Icon( Icon(
imageVector = Icons.Default.Check, imageVector = Icons.Default.Check,
contentDescription = "Ausgewählt", contentDescription = stringResource(R.string.selection_count, 1),
tint = MaterialTheme.colorScheme.onPrimary, tint = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.size(16.dp) modifier = Modifier.size(16.dp)
) )

View File

@@ -16,6 +16,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.NoteType import dev.dettmer.simplenotes.models.NoteType
/** /**
@@ -42,7 +44,7 @@ fun NoteTypeFAB(
) { ) {
Icon( Icon(
imageVector = Icons.Default.Add, imageVector = Icons.Default.Add,
contentDescription = "Neue Notiz" contentDescription = stringResource(R.string.fab_new_note)
) )
// Dropdown inside FAB - renders as popup overlay // Dropdown inside FAB - renders as popup overlay
@@ -51,7 +53,7 @@ fun NoteTypeFAB(
onDismissRequest = { expanded = false } onDismissRequest = { expanded = false }
) { ) {
DropdownMenuItem( DropdownMenuItem(
text = { Text("Text-Notiz") }, text = { Text(stringResource(R.string.fab_text_note)) },
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = Icons.Outlined.Description, imageVector = Icons.Outlined.Description,
@@ -65,7 +67,7 @@ fun NoteTypeFAB(
} }
) )
DropdownMenuItem( DropdownMenuItem(
text = { Text("Checkliste") }, text = { Text(stringResource(R.string.fab_checklist)) },
leadingIcon = { leadingIcon = {
Icon( Icon(
imageVector = Icons.AutoMirrored.Outlined.List, imageVector = Icons.AutoMirrored.Outlined.List,

View File

@@ -16,7 +16,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.sync.SyncStateManager import dev.dettmer.simplenotes.sync.SyncStateManager
/** /**
@@ -60,10 +62,10 @@ fun SyncStatusBanner(
Text( Text(
text = when (syncState) { text = when (syncState) {
SyncStateManager.SyncState.SYNCING -> "Synchronisiere..." SyncStateManager.SyncState.SYNCING -> stringResource(R.string.sync_status_syncing)
SyncStateManager.SyncState.SYNCING_SILENT -> "" // v1.5.0: Wird nicht angezeigt (isVisible = false) SyncStateManager.SyncState.SYNCING_SILENT -> "" // v1.5.0: Wird nicht angezeigt (isVisible = false)
SyncStateManager.SyncState.COMPLETED -> message ?: "Synchronisiert" SyncStateManager.SyncState.COMPLETED -> message ?: stringResource(R.string.sync_status_completed)
SyncStateManager.SyncState.ERROR -> message ?: "Fehler" SyncStateManager.SyncState.ERROR -> message ?: stringResource(R.string.sync_status_error)
SyncStateManager.SyncState.IDLE -> "" SyncStateManager.SyncState.IDLE -> ""
}, },
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,

View File

@@ -7,16 +7,17 @@ import android.os.Bundle
import android.os.PowerManager import android.os.PowerManager
import android.provider.Settings import android.provider.Settings
import android.widget.Toast import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.OnBackPressedCallback import androidx.activity.OnBackPressedCallback
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.google.android.material.color.DynamicColors import com.google.android.material.color.DynamicColors
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.SimpleNotesApplication import dev.dettmer.simplenotes.SimpleNotesApplication
import dev.dettmer.simplenotes.ui.theme.SimpleNotesTheme import dev.dettmer.simplenotes.ui.theme.SimpleNotesTheme
import dev.dettmer.simplenotes.utils.Logger import dev.dettmer.simplenotes.utils.Logger
@@ -34,7 +35,7 @@ import kotlinx.coroutines.launch
* - Navigation with back button in each screen * - Navigation with back button in each screen
* - Clean separation of concerns with SettingsViewModel * - Clean separation of concerns with SettingsViewModel
*/ */
class ComposeSettingsActivity : ComponentActivity() { class ComposeSettingsActivity : AppCompatActivity() {
companion object { companion object {
private const val TAG = "ComposeSettingsActivity" private const val TAG = "ComposeSettingsActivity"
@@ -133,16 +134,12 @@ class ComposeSettingsActivity : ComponentActivity() {
*/ */
private fun showBatteryOptimizationDialog() { private fun showBatteryOptimizationDialog() {
AlertDialog.Builder(this) AlertDialog.Builder(this)
.setTitle("Hintergrund-Synchronisation") .setTitle(getString(R.string.battery_optimization_dialog_title))
.setMessage( .setMessage(getString(R.string.battery_optimization_dialog_full_message))
"Damit die App im Hintergrund synchronisieren kann, " + .setPositiveButton(getString(R.string.battery_optimization_open_settings)) { _, _ ->
"muss die Akku-Optimierung deaktiviert werden.\n\n" +
"Bitte wähle 'Nicht optimieren' für Simple Notes."
)
.setPositiveButton("Einstellungen öffnen") { _, _ ->
openBatteryOptimizationSettings() openBatteryOptimizationSettings()
} }
.setNegativeButton("Später") { dialog, _ -> .setNegativeButton(getString(R.string.battery_optimization_later)) { dialog, _ ->
dialog.dismiss() dialog.dismiss()
} }
.setCancelable(false) .setCancelable(false)

View File

@@ -7,6 +7,7 @@ import androidx.navigation.compose.composable
import dev.dettmer.simplenotes.ui.settings.screens.AboutScreen import dev.dettmer.simplenotes.ui.settings.screens.AboutScreen
import dev.dettmer.simplenotes.ui.settings.screens.BackupSettingsScreen import dev.dettmer.simplenotes.ui.settings.screens.BackupSettingsScreen
import dev.dettmer.simplenotes.ui.settings.screens.DebugSettingsScreen import dev.dettmer.simplenotes.ui.settings.screens.DebugSettingsScreen
import dev.dettmer.simplenotes.ui.settings.screens.LanguageSettingsScreen
import dev.dettmer.simplenotes.ui.settings.screens.MarkdownSettingsScreen import dev.dettmer.simplenotes.ui.settings.screens.MarkdownSettingsScreen
import dev.dettmer.simplenotes.ui.settings.screens.ServerSettingsScreen import dev.dettmer.simplenotes.ui.settings.screens.ServerSettingsScreen
import dev.dettmer.simplenotes.ui.settings.screens.SettingsMainScreen import dev.dettmer.simplenotes.ui.settings.screens.SettingsMainScreen
@@ -35,6 +36,13 @@ fun SettingsNavHost(
) )
} }
// Language Settings
composable(SettingsRoute.Language.route) {
LanguageSettingsScreen(
onBack = { navController.popBackStack() }
)
}
// Server Settings // Server Settings
composable(SettingsRoute.Server.route) { composable(SettingsRoute.Server.route) {
ServerSettingsScreen( ServerSettingsScreen(

View File

@@ -6,6 +6,7 @@ package dev.dettmer.simplenotes.ui.settings
*/ */
sealed class SettingsRoute(val route: String) { sealed class SettingsRoute(val route: String) {
data object Main : SettingsRoute("settings_main") data object Main : SettingsRoute("settings_main")
data object Language : SettingsRoute("settings_language")
data object Server : SettingsRoute("settings_server") data object Server : SettingsRoute("settings_server")
data object Sync : SettingsRoute("settings_sync") data object Sync : SettingsRoute("settings_sync")
data object Markdown : SettingsRoute("settings_markdown") data object Markdown : SettingsRoute("settings_markdown")

View File

@@ -6,6 +6,7 @@ import android.net.Uri
import android.util.Log import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.backup.BackupManager import dev.dettmer.simplenotes.backup.BackupManager
import dev.dettmer.simplenotes.backup.RestoreMode import dev.dettmer.simplenotes.backup.RestoreMode
import dev.dettmer.simplenotes.sync.WebDavSyncService import dev.dettmer.simplenotes.sync.WebDavSyncService
@@ -184,10 +185,10 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
} else { } else {
ServerStatus.Unreachable(result.errorMessage) ServerStatus.Unreachable(result.errorMessage)
} }
emitToast(if (result.isSuccess) "✅ Verbindung erfolgreich!" else "${result.errorMessage}") emitToast(if (result.isSuccess) getString(R.string.toast_connection_success) else getString(R.string.toast_connection_failed, result.errorMessage ?: ""))
} catch (e: Exception) { } catch (e: Exception) {
_serverStatus.value = ServerStatus.Unreachable(e.message) _serverStatus.value = ServerStatus.Unreachable(e.message)
emitToast("❌ Fehler: ${e.message}") emitToast(getString(R.string.toast_error, e.message ?: ""))
} }
} }
} }
@@ -225,22 +226,22 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
viewModelScope.launch { viewModelScope.launch {
_isSyncing.value = true _isSyncing.value = true
try { try {
emitToast("🔄 Synchronisiere...") emitToast(getString(R.string.toast_syncing))
val syncService = WebDavSyncService(getApplication()) val syncService = WebDavSyncService(getApplication())
if (!syncService.hasUnsyncedChanges()) { if (!syncService.hasUnsyncedChanges()) {
emitToast("✅ Bereits synchronisiert") emitToast(getString(R.string.toast_already_synced))
return@launch return@launch
} }
val result = syncService.syncNotes() val result = syncService.syncNotes()
if (result.isSuccess) { if (result.isSuccess) {
emitToast("${result.syncedCount} Notizen synchronisiert") emitToast(getString(R.string.toast_sync_success, result.syncedCount))
} else { } else {
emitToast("${result.errorMessage}") emitToast(getString(R.string.toast_sync_failed, result.errorMessage ?: ""))
} }
} catch (e: Exception) { } catch (e: Exception) {
emitToast("❌ Fehler: ${e.message}") emitToast(getString(R.string.toast_error, e.message ?: ""))
} finally { } finally {
_isSyncing.value = false _isSyncing.value = false
} }
@@ -260,10 +261,10 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
// v1.5.0 Fix: Trigger battery optimization check and network monitor restart // v1.5.0 Fix: Trigger battery optimization check and network monitor restart
_events.emit(SettingsEvent.RequestBatteryOptimization) _events.emit(SettingsEvent.RequestBatteryOptimization)
_events.emit(SettingsEvent.RestartNetworkMonitor) _events.emit(SettingsEvent.RestartNetworkMonitor)
emitToast("✅ Auto-Sync aktiviert") emitToast(getString(R.string.toast_auto_sync_enabled))
} else { } else {
_events.emit(SettingsEvent.RestartNetworkMonitor) _events.emit(SettingsEvent.RestartNetworkMonitor)
emitToast("Auto-Sync deaktiviert") emitToast(getString(R.string.toast_auto_sync_disabled))
} }
} }
} }
@@ -273,11 +274,11 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
prefs.edit().putLong(Constants.PREF_SYNC_INTERVAL_MINUTES, minutes).apply() prefs.edit().putLong(Constants.PREF_SYNC_INTERVAL_MINUTES, minutes).apply()
viewModelScope.launch { viewModelScope.launch {
val text = when (minutes) { val text = when (minutes) {
15L -> "15 Minuten" 15L -> getString(R.string.toast_sync_interval_15min)
60L -> "60 Minuten" 60L -> getString(R.string.toast_sync_interval_60min)
else -> "30 Minuten" else -> getString(R.string.toast_sync_interval_30min)
} }
emitToast("⏱️ Sync-Intervall: $text") emitToast(getString(R.string.toast_sync_interval, text))
} }
} }
@@ -296,7 +297,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: "" val password = prefs.getString(Constants.KEY_PASSWORD, "") ?: ""
if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) { if (serverUrl.isBlank() || username.isBlank() || password.isBlank()) {
emitToast("⚠️ Bitte zuerst WebDAV-Server konfigurieren") emitToast(getString(R.string.toast_configure_server_first))
// Don't enable - revert state // Don't enable - revert state
return@launch return@launch
} }
@@ -329,7 +330,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
.apply() .apply()
_markdownExportProgress.value = MarkdownExportProgress(noteCount, noteCount, isComplete = true) _markdownExportProgress.value = MarkdownExportProgress(noteCount, noteCount, isComplete = true)
emitToast("$exportedCount Notizen nach Markdown exportiert") emitToast(getString(R.string.toast_markdown_exported, exportedCount))
// Clear progress after short delay // Clear progress after short delay
kotlinx.coroutines.delay(500) kotlinx.coroutines.delay(500)
@@ -342,12 +343,12 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
.putBoolean(Constants.KEY_MARKDOWN_EXPORT, true) .putBoolean(Constants.KEY_MARKDOWN_EXPORT, true)
.putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true) .putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true)
.apply() .apply()
emitToast("📝 Markdown Auto-Sync aktiviert") emitToast(getString(R.string.toast_markdown_enabled))
} }
} catch (e: Exception) { } catch (e: Exception) {
_markdownExportProgress.value = null _markdownExportProgress.value = null
emitToast("❌ Export fehlgeschlagen: ${e.message}") emitToast(getString(R.string.toast_export_failed, e.message ?: ""))
// Don't enable on error // Don't enable on error
} }
} }
@@ -359,7 +360,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
.putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false) .putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false)
.apply() .apply()
viewModelScope.launch { viewModelScope.launch {
emitToast("📝 Markdown Auto-Sync deaktiviert") emitToast(getString(R.string.toast_markdown_disabled))
} }
} }
} }
@@ -367,12 +368,12 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
fun performManualMarkdownSync() { fun performManualMarkdownSync() {
viewModelScope.launch { viewModelScope.launch {
try { try {
emitToast("📝 Markdown-Sync läuft...") emitToast(getString(R.string.toast_markdown_syncing))
val syncService = WebDavSyncService(getApplication()) val syncService = WebDavSyncService(getApplication())
val result = syncService.manualMarkdownSync() val result = syncService.manualMarkdownSync()
emitToast("✅ Export: ${result.exportedCount} • Import: ${result.importedCount}") emitToast(getString(R.string.toast_markdown_result, result.exportedCount, result.importedCount))
} catch (e: Exception) { } catch (e: Exception) {
emitToast("❌ Fehler: ${e.message}") emitToast(getString(R.string.toast_error, e.message ?: ""))
} }
} }
} }
@@ -386,9 +387,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
_isBackupInProgress.value = true _isBackupInProgress.value = true
try { try {
val result = backupManager.createBackup(uri) val result = backupManager.createBackup(uri)
emitToast(if (result.success) "${result.message}" else "${result.error}") emitToast(if (result.success) getString(R.string.toast_backup_success, result.message ?: "") else getString(R.string.toast_backup_failed, result.error ?: ""))
} catch (e: Exception) { } catch (e: Exception) {
emitToast("❌ Backup fehlgeschlagen: ${e.message}") emitToast(getString(R.string.toast_backup_failed, e.message ?: ""))
} finally { } finally {
_isBackupInProgress.value = false _isBackupInProgress.value = false
} }
@@ -400,9 +401,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
_isBackupInProgress.value = true _isBackupInProgress.value = true
try { try {
val result = backupManager.restoreBackup(uri, mode) val result = backupManager.restoreBackup(uri, mode)
emitToast(if (result.success) "${result.importedNotes} Notizen wiederhergestellt" else "${result.error}") emitToast(if (result.success) getString(R.string.toast_restore_success, result.importedNotes) else getString(R.string.toast_restore_failed, result.error ?: ""))
} catch (e: Exception) { } catch (e: Exception) {
emitToast("❌ Wiederherstellung fehlgeschlagen: ${e.message}") emitToast(getString(R.string.toast_restore_failed, e.message ?: ""))
} finally { } finally {
_isBackupInProgress.value = false _isBackupInProgress.value = false
} }
@@ -413,14 +414,14 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
viewModelScope.launch { viewModelScope.launch {
_isBackupInProgress.value = true _isBackupInProgress.value = true
try { try {
emitToast("📥 Lade vom Server...") emitToast(getString(R.string.restore_progress))
val syncService = WebDavSyncService(getApplication()) val syncService = WebDavSyncService(getApplication())
val result = withContext(Dispatchers.IO) { val result = withContext(Dispatchers.IO) {
syncService.restoreFromServer(mode) syncService.restoreFromServer(mode)
} }
emitToast(if (result.isSuccess) "${result.restoredCount} Notizen wiederhergestellt" else "${result.errorMessage}") emitToast(if (result.isSuccess) getString(R.string.toast_restore_success, result.restoredCount) else getString(R.string.toast_restore_failed, result.errorMessage ?: ""))
} catch (e: Exception) { } catch (e: Exception) {
emitToast("❌ Fehler: ${e.message}") emitToast(getString(R.string.toast_error, e.message ?: ""))
} finally { } finally {
_isBackupInProgress.value = false _isBackupInProgress.value = false
} }
@@ -436,7 +437,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
prefs.edit().putBoolean(Constants.KEY_FILE_LOGGING_ENABLED, enabled).apply() prefs.edit().putBoolean(Constants.KEY_FILE_LOGGING_ENABLED, enabled).apply()
Logger.setFileLoggingEnabled(enabled) Logger.setFileLoggingEnabled(enabled)
viewModelScope.launch { viewModelScope.launch {
emitToast(if (enabled) "📝 Datei-Logging aktiviert" else "📝 Datei-Logging deaktiviert") emitToast(if (enabled) getString(R.string.toast_file_logging_enabled) else getString(R.string.toast_file_logging_disabled))
} }
} }
@@ -444,9 +445,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
viewModelScope.launch { viewModelScope.launch {
try { try {
val cleared = Logger.clearLogFile(getApplication()) val cleared = Logger.clearLogFile(getApplication())
emitToast(if (cleared) "🗑️ Logs gelöscht" else "📭 Keine Logs zum Löschen") emitToast(if (cleared) getString(R.string.toast_logs_deleted) else getString(R.string.toast_logs_deleted))
} catch (e: Exception) { } catch (e: Exception) {
emitToast("❌ Fehler: ${e.message}") emitToast(getString(R.string.toast_error, e.message ?: ""))
} }
} }
} }
@@ -457,6 +458,11 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
// Helper // Helper
// ═══════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════
private fun getString(resId: Int): String = getApplication<android.app.Application>().getString(resId)
private fun getString(resId: Int, vararg formatArgs: Any): String =
getApplication<android.app.Application>().getString(resId, *formatArgs)
private suspend fun emitToast(message: String) { private suspend fun emitToast(message: String) {
_showToast.emit(message) _showToast.emit(message)
} }

View File

@@ -13,6 +13,8 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import dev.dettmer.simplenotes.R
/** /**
* Reusable Scaffold with back-navigation TopAppBar * Reusable Scaffold with back-navigation TopAppBar
@@ -40,7 +42,7 @@ fun SettingsScaffold(
IconButton(onClick = onBack) { IconButton(onClick = onBack) {
Icon( Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack, imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Zurück" contentDescription = stringResource(R.string.content_description_back)
) )
} }
}, },

View File

@@ -37,9 +37,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import dev.dettmer.simplenotes.BuildConfig import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader import dev.dettmer.simplenotes.ui.settings.components.SettingsSectionHeader
@@ -59,7 +61,7 @@ fun AboutScreen(
val licenseUrl = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE" val licenseUrl = "https://github.com/inventory69/simple-notes-sync/blob/main/LICENSE"
SettingsScaffold( SettingsScaffold(
title = "Über diese App", title = stringResource(R.string.about_settings_title),
onBack = onBack onBack = onBack
) { paddingValues -> ) { paddingValues ->
Column( Column(
@@ -110,7 +112,7 @@ fun AboutScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "Simple Notes Sync", text = stringResource(R.string.about_app_name),
style = MaterialTheme.typography.headlineSmall, style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer color = MaterialTheme.colorScheme.onPrimaryContainer
) )
@@ -118,7 +120,7 @@ fun AboutScreen(
Spacer(modifier = Modifier.height(4.dp)) Spacer(modifier = Modifier.height(4.dp))
Text( Text(
text = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", text = stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f) color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
) )
@@ -127,13 +129,13 @@ fun AboutScreen(
Spacer(modifier = Modifier.height(24.dp)) Spacer(modifier = Modifier.height(24.dp))
SettingsSectionHeader(text = "Links") SettingsSectionHeader(text = stringResource(R.string.about_links_section))
// GitHub Repository // GitHub Repository
AboutLinkItem( AboutLinkItem(
icon = Icons.Default.Code, icon = Icons.Default.Code,
title = "GitHub Repository", title = stringResource(R.string.about_github_title),
subtitle = "Quellcode, Issues & Dokumentation", subtitle = stringResource(R.string.about_github_subtitle),
onClick = { onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubRepoUrl)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubRepoUrl))
context.startActivity(intent) context.startActivity(intent)
@@ -143,8 +145,8 @@ fun AboutScreen(
// Developer // Developer
AboutLinkItem( AboutLinkItem(
icon = Icons.Default.Person, icon = Icons.Default.Person,
title = "Entwickler", title = stringResource(R.string.about_developer_title),
subtitle = "GitHub Profil: @inventory69", subtitle = stringResource(R.string.about_developer_subtitle),
onClick = { onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubProfileUrl)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubProfileUrl))
context.startActivity(intent) context.startActivity(intent)
@@ -154,8 +156,8 @@ fun AboutScreen(
// License // License
AboutLinkItem( AboutLinkItem(
icon = Icons.Default.Policy, icon = Icons.Default.Policy,
title = "Lizenz", title = stringResource(R.string.about_license_title),
subtitle = "MIT License - Open Source", subtitle = stringResource(R.string.about_license_subtitle),
onClick = { onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(licenseUrl)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(licenseUrl))
context.startActivity(intent) context.startActivity(intent)
@@ -177,14 +179,12 @@ fun AboutScreen(
modifier = Modifier.padding(16.dp) modifier = Modifier.padding(16.dp)
) { ) {
Text( Text(
text = "🔒 Datenschutz", text = stringResource(R.string.about_privacy_title),
style = MaterialTheme.typography.titleSmall style = MaterialTheme.typography.titleSmall
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = "Diese App sammelt keine Daten. Alle Notizen werden " + text = stringResource(R.string.about_privacy_text),
"nur lokal auf deinem Gerät und auf deinem eigenen " +
"WebDAV-Server gespeichert. Keine Telemetrie, keine Werbung.",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )

View File

@@ -21,7 +21,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.backup.RestoreMode import dev.dettmer.simplenotes.backup.RestoreMode
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.RadioOption import dev.dettmer.simplenotes.ui.settings.components.RadioOption
@@ -71,7 +73,7 @@ fun BackupSettingsScreen(
} }
SettingsScaffold( SettingsScaffold(
title = "Backup & Wiederherstellung", title = stringResource(R.string.backup_settings_title),
onBack = onBack onBack = onBack
) { paddingValues -> ) { paddingValues ->
Column( Column(
@@ -84,19 +86,18 @@ fun BackupSettingsScreen(
// Info Card // Info Card
SettingsInfoCard( SettingsInfoCard(
text = "📦 Bei jeder Wiederherstellung wird automatisch ein " + text = stringResource(R.string.backup_auto_info)
"Sicherheits-Backup erstellt."
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
// Local Backup Section // Local Backup Section
SettingsSectionHeader(text = "Lokales Backup") SettingsSectionHeader(text = stringResource(R.string.backup_local_section))
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
SettingsButton( SettingsButton(
text = "💾 Backup erstellen", text = stringResource(R.string.backup_create),
onClick = { onClick = {
val timestamp = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US) val timestamp = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
.format(Date()) .format(Date())
@@ -110,7 +111,7 @@ fun BackupSettingsScreen(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
SettingsOutlinedButton( SettingsOutlinedButton(
text = "📂 Aus Datei wiederherstellen", text = stringResource(R.string.backup_restore_file),
onClick = { onClick = {
restoreFileLauncher.launch(arrayOf("application/json")) restoreFileLauncher.launch(arrayOf("application/json"))
}, },
@@ -121,12 +122,12 @@ fun BackupSettingsScreen(
SettingsDivider() SettingsDivider()
// Server Backup Section // Server Backup Section
SettingsSectionHeader(text = "Server-Backup") SettingsSectionHeader(text = stringResource(R.string.backup_server_section))
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
SettingsOutlinedButton( SettingsOutlinedButton(
text = "☁️ Vom Server wiederherstellen", text = stringResource(R.string.backup_restore_server),
onClick = { onClick = {
restoreSource = RestoreSource.Server restoreSource = RestoreSource.Server
showRestoreDialog = true showRestoreDialog = true
@@ -186,42 +187,42 @@ private fun RestoreModeDialog(
onDismiss: () -> Unit onDismiss: () -> Unit
) { ) {
val sourceText = when (source) { val sourceText = when (source) {
RestoreSource.LocalFile -> "Lokale Datei" RestoreSource.LocalFile -> stringResource(R.string.backup_restore_source_file)
RestoreSource.Server -> "WebDAV Server" RestoreSource.Server -> stringResource(R.string.backup_restore_source_server)
} }
val modeOptions = listOf( val modeOptions = listOf(
RadioOption( RadioOption(
value = RestoreMode.MERGE, value = RestoreMode.MERGE,
title = "⚪ Zusammenführen (Standard)", title = stringResource(R.string.backup_mode_merge_title),
subtitle = "Neue hinzufügen, Bestehende behalten" subtitle = stringResource(R.string.backup_mode_merge_subtitle)
), ),
RadioOption( RadioOption(
value = RestoreMode.REPLACE, value = RestoreMode.REPLACE,
title = "⚪ Ersetzen", title = stringResource(R.string.backup_mode_replace_title),
subtitle = "Alle löschen & Backup importieren" subtitle = stringResource(R.string.backup_mode_replace_subtitle)
), ),
RadioOption( RadioOption(
value = RestoreMode.OVERWRITE_DUPLICATES, value = RestoreMode.OVERWRITE_DUPLICATES,
title = "⚪ Duplikate überschreiben", title = stringResource(R.string.backup_mode_overwrite_title),
subtitle = "Backup gewinnt bei Konflikten" subtitle = stringResource(R.string.backup_mode_overwrite_subtitle)
) )
) )
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = { Text("⚠️ Backup wiederherstellen?") }, title = { Text(stringResource(R.string.backup_restore_dialog_title)) },
text = { text = {
Column { Column {
Text( Text(
text = "Quelle: $sourceText", text = stringResource(R.string.backup_restore_source, sourceText),
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
Spacer(modifier = Modifier.height(16.dp)) Spacer(modifier = Modifier.height(16.dp))
Text( Text(
text = "Wiederherstellungs-Modus:", text = stringResource(R.string.backup_restore_mode_label),
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.labelLarge
) )
@@ -236,7 +237,7 @@ private fun RestoreModeDialog(
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
Text( Text(
text = " Ein Sicherheits-Backup wird vor dem Wiederherstellen automatisch erstellt.", text = stringResource(R.string.backup_restore_info),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant color = MaterialTheme.colorScheme.onSurfaceVariant
) )
@@ -244,12 +245,12 @@ private fun RestoreModeDialog(
}, },
confirmButton = { confirmButton = {
TextButton(onClick = onConfirm) { TextButton(onClick = onConfirm) {
Text("Wiederherstellen") Text(stringResource(R.string.backup_restore_button))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = onDismiss) { TextButton(onClick = onDismiss) {
Text("Abbrechen") Text(stringResource(R.string.cancel))
} }
} }
) )

View File

@@ -21,9 +21,11 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import dev.dettmer.simplenotes.BuildConfig import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.SettingsButton import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
import dev.dettmer.simplenotes.ui.settings.components.SettingsDangerButton import dev.dettmer.simplenotes.ui.settings.components.SettingsDangerButton
@@ -48,7 +50,7 @@ fun DebugSettingsScreen(
var showClearLogsDialog by remember { mutableStateOf(false) } var showClearLogsDialog by remember { mutableStateOf(false) }
SettingsScaffold( SettingsScaffold(
title = "Debug & Diagnose", title = stringResource(R.string.debug_settings_title),
onBack = onBack onBack = onBack
) { paddingValues -> ) { paddingValues ->
Column( Column(
@@ -61,8 +63,8 @@ fun DebugSettingsScreen(
// File Logging Toggle // File Logging Toggle
SettingsSwitch( SettingsSwitch(
title = "Datei-Logging", title = stringResource(R.string.debug_file_logging_title),
subtitle = "Sync-Logs in Datei speichern", subtitle = stringResource(R.string.debug_file_logging_subtitle),
checked = fileLoggingEnabled, checked = fileLoggingEnabled,
onCheckedChange = { viewModel.setFileLogging(it) }, onCheckedChange = { viewModel.setFileLogging(it) },
icon = Icons.AutoMirrored.Filled.Notes icon = Icons.AutoMirrored.Filled.Notes
@@ -70,21 +72,18 @@ fun DebugSettingsScreen(
// Privacy Info // Privacy Info
SettingsInfoCard( SettingsInfoCard(
text = "🔒 Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert " + text = stringResource(R.string.debug_privacy_info)
"und niemals an externe Server gesendet. Die Logs enthalten " +
"Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen " +
"oder exportieren."
) )
SettingsDivider() SettingsDivider()
SettingsSectionHeader(text = "Log-Aktionen") SettingsSectionHeader(text = stringResource(R.string.debug_log_actions_section))
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// Export Logs Button // Export Logs Button
SettingsButton( SettingsButton(
text = "📤 Logs exportieren & teilen", text = stringResource(R.string.debug_export_logs),
onClick = { onClick = {
val logFile = viewModel.getLogFile() val logFile = viewModel.getLogFile()
if (logFile != null && logFile.exists() && logFile.length() > 0L) { if (logFile != null && logFile.exists() && logFile.length() > 0L) {
@@ -97,11 +96,11 @@ fun DebugSettingsScreen(
val shareIntent = Intent(Intent.ACTION_SEND).apply { val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain" type = "text/plain"
putExtra(Intent.EXTRA_STREAM, logUri) putExtra(Intent.EXTRA_STREAM, logUri)
putExtra(Intent.EXTRA_SUBJECT, "SimpleNotes Sync Logs") putExtra(Intent.EXTRA_SUBJECT, context.getString(R.string.debug_logs_subject))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
} }
context.startActivity(Intent.createChooser(shareIntent, "Logs teilen via...")) context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.debug_logs_share_via)))
} }
}, },
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
@@ -111,7 +110,7 @@ fun DebugSettingsScreen(
// Clear Logs Button // Clear Logs Button
SettingsDangerButton( SettingsDangerButton(
text = "🗑️ Logs löschen", text = stringResource(R.string.debug_delete_logs),
onClick = { showClearLogsDialog = true }, onClick = { showClearLogsDialog = true },
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
) )
@@ -124,9 +123,9 @@ fun DebugSettingsScreen(
if (showClearLogsDialog) { if (showClearLogsDialog) {
AlertDialog( AlertDialog(
onDismissRequest = { showClearLogsDialog = false }, onDismissRequest = { showClearLogsDialog = false },
title = { Text("Logs löschen?") }, title = { Text(stringResource(R.string.debug_delete_logs_title)) },
text = { text = {
Text("Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht.") Text(stringResource(R.string.debug_delete_logs_message))
}, },
confirmButton = { confirmButton = {
TextButton( TextButton(
@@ -135,12 +134,12 @@ fun DebugSettingsScreen(
viewModel.clearLogs() viewModel.clearLogs()
} }
) { ) {
Text("Löschen") Text(stringResource(R.string.delete))
} }
}, },
dismissButton = { dismissButton = {
TextButton(onClick = { showClearLogsDialog = false }) { TextButton(onClick = { showClearLogsDialog = false }) {
Text("Abbrechen") Text(stringResource(R.string.cancel))
} }
} }
) )

View File

@@ -0,0 +1,120 @@
package dev.dettmer.simplenotes.ui.settings.screens
import android.app.Activity
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.components.RadioOption
import dev.dettmer.simplenotes.ui.settings.components.SettingsInfoCard
import dev.dettmer.simplenotes.ui.settings.components.SettingsRadioGroup
import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
/**
* Language selection settings screen
* v1.5.0: Internationalization feature
*
* Uses Android's Per-App Language API (Android 13+) with AppCompat fallback
*/
@Composable
fun LanguageSettingsScreen(
onBack: () -> Unit
) {
val context = LocalContext.current
// Get current app locale - fresh value each time (no remember, always reads current state)
val currentLocale = AppCompatDelegate.getApplicationLocales()
val currentLanguageCode = if (currentLocale.isEmpty) {
"" // System default
} else {
currentLocale.get(0)?.language ?: ""
}
var selectedLanguage by remember(currentLanguageCode) { mutableStateOf(currentLanguageCode) }
// Language options
val languageOptions = listOf(
RadioOption(
value = "",
title = stringResource(R.string.language_system_default),
subtitle = null
),
RadioOption(
value = "en",
title = stringResource(R.string.language_english),
subtitle = "English"
),
RadioOption(
value = "de",
title = stringResource(R.string.language_german),
subtitle = "German"
)
)
SettingsScaffold(
title = stringResource(R.string.language_settings_title),
onBack = onBack
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
) {
Spacer(modifier = Modifier.height(8.dp))
// Info card
SettingsInfoCard(
text = stringResource(R.string.language_info)
)
Spacer(modifier = Modifier.height(8.dp))
// Language selection radio group
SettingsRadioGroup(
options = languageOptions,
selectedValue = selectedLanguage,
onValueSelected = { newLanguage ->
if (newLanguage != selectedLanguage) {
selectedLanguage = newLanguage
setAppLanguage(newLanguage, context as Activity)
}
}
)
}
}
}
/**
* Set app language using AppCompatDelegate
* Works on Android 13+ natively, falls back to AppCompat on older versions
*/
private fun setAppLanguage(languageCode: String, activity: Activity) {
val localeList = if (languageCode.isEmpty()) {
LocaleListCompat.getEmptyLocaleList()
} else {
LocaleListCompat.forLanguageTags(languageCode)
}
AppCompatDelegate.setApplicationLocales(localeList)
// Restart the activity to apply the change
// On Android 13+ the system handles this automatically for some apps,
// but we need to recreate to ensure our Compose UI recomposes with new locale
activity.recreate()
}

View File

@@ -20,7 +20,9 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.SettingsButton import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
@@ -44,7 +46,7 @@ fun MarkdownSettingsScreen(
exportProgress?.let { progress -> exportProgress?.let { progress ->
AlertDialog( AlertDialog(
onDismissRequest = { /* Not dismissable */ }, onDismissRequest = { /* Not dismissable */ },
title = { Text("Markdown Auto-Sync") }, title = { Text(stringResource(R.string.markdown_dialog_title)) },
text = { text = {
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
@@ -53,9 +55,9 @@ fun MarkdownSettingsScreen(
) { ) {
Text( Text(
text = if (progress.isComplete) { text = if (progress.isComplete) {
"✅ Export abgeschlossen" stringResource(R.string.markdown_export_complete)
} else { } else {
"Exportiere ${progress.current}/${progress.total} Notizen..." stringResource(R.string.markdown_export_progress, progress.current, progress.total)
}, },
style = MaterialTheme.typography.bodyMedium style = MaterialTheme.typography.bodyMedium
) )
@@ -75,7 +77,7 @@ fun MarkdownSettingsScreen(
} }
SettingsScaffold( SettingsScaffold(
title = "Markdown Desktop-Integration", title = stringResource(R.string.markdown_settings_title),
onBack = onBack onBack = onBack
) { paddingValues -> ) { paddingValues ->
Column( Column(
@@ -88,17 +90,15 @@ fun MarkdownSettingsScreen(
// Info Card // Info Card
SettingsInfoCard( SettingsInfoCard(
text = "📝 Exportiert Notizen zusätzlich als .md-Dateien. Mounte " + text = stringResource(R.string.markdown_info)
"WebDAV als Netzlaufwerk um mit VS Code, Typora oder jedem " +
"Markdown-Editor zu bearbeiten. JSON-Sync bleibt primäres Format."
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// Markdown Auto-Sync Toggle // Markdown Auto-Sync Toggle
SettingsSwitch( SettingsSwitch(
title = "Markdown Auto-Sync", title = stringResource(R.string.markdown_auto_sync_title),
subtitle = "Synchronisiert Notizen automatisch als .md-Dateien (Upload + Download bei jedem Sync)", subtitle = stringResource(R.string.markdown_auto_sync_subtitle),
checked = markdownAutoSync, checked = markdownAutoSync,
onCheckedChange = { viewModel.setMarkdownAutoSync(it) }, onCheckedChange = { viewModel.setMarkdownAutoSync(it) },
icon = Icons.Default.Description icon = Icons.Default.Description
@@ -109,14 +109,13 @@ fun MarkdownSettingsScreen(
SettingsDivider() SettingsDivider()
SettingsInfoCard( SettingsInfoCard(
text = "Manueller Sync exportiert alle Notizen als .md-Dateien und " + text = stringResource(R.string.markdown_manual_sync_info)
"importiert .md-Dateien vom Server. Nützlich für einmalige Synchronisation."
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
SettingsButton( SettingsButton(
text = "📝 Manueller Markdown-Sync", text = stringResource(R.string.markdown_manual_sync_button),
onClick = { viewModel.performManualMarkdownSync() }, onClick = { viewModel.performManualMarkdownSync() },
modifier = Modifier.padding(horizontal = 16.dp) modifier = Modifier.padding(horizontal = 16.dp)
) )

View File

@@ -41,9 +41,11 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
@@ -71,7 +73,7 @@ fun ServerSettingsScreen(
} }
SettingsScaffold( SettingsScaffold(
title = "Server-Einstellungen", title = stringResource(R.string.server_settings_title),
onBack = onBack onBack = onBack
) { paddingValues -> ) { paddingValues ->
Column( Column(
@@ -83,7 +85,7 @@ fun ServerSettingsScreen(
) { ) {
// Verbindungstyp // Verbindungstyp
Text( Text(
text = "Verbindungstyp", text = stringResource(R.string.server_connection_type),
style = MaterialTheme.typography.labelLarge, style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(bottom = 8.dp) modifier = Modifier.padding(bottom = 8.dp)
) )
@@ -95,22 +97,22 @@ fun ServerSettingsScreen(
FilterChip( FilterChip(
selected = !isHttps, selected = !isHttps,
onClick = { viewModel.updateProtocol(false) }, onClick = { viewModel.updateProtocol(false) },
label = { Text("🏠 Intern (HTTP)") }, label = { Text(stringResource(R.string.server_connection_http)) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
FilterChip( FilterChip(
selected = isHttps, selected = isHttps,
onClick = { viewModel.updateProtocol(true) }, onClick = { viewModel.updateProtocol(true) },
label = { Text("🌐 Extern (HTTPS)") }, label = { Text(stringResource(R.string.server_connection_https)) },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) )
} }
Text( Text(
text = if (!isHttps) { text = if (!isHttps) {
"HTTP nur für lokale Netzwerke (z.B. 192.168.x.x, 10.x.x.x)" stringResource(R.string.server_connection_http_hint)
} else { } else {
"HTTPS für sichere Verbindungen über das Internet" stringResource(R.string.server_connection_https_hint)
}, },
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -121,8 +123,8 @@ fun ServerSettingsScreen(
OutlinedTextField( OutlinedTextField(
value = serverUrl, value = serverUrl,
onValueChange = { viewModel.updateServerUrl(it) }, onValueChange = { viewModel.updateServerUrl(it) },
label = { Text("Server-Adresse") }, label = { Text(stringResource(R.string.server_address)) },
supportingText = { Text("z.B. http://192.168.0.188:8080/notes") }, supportingText = { Text(stringResource(R.string.server_address_hint)) },
leadingIcon = { Icon(Icons.Default.Language, null) }, leadingIcon = { Icon(Icons.Default.Language, null) },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
@@ -135,7 +137,7 @@ fun ServerSettingsScreen(
OutlinedTextField( OutlinedTextField(
value = username, value = username,
onValueChange = { viewModel.updateUsername(it) }, onValueChange = { viewModel.updateUsername(it) },
label = { Text("Benutzername") }, label = { Text(stringResource(R.string.username)) },
leadingIcon = { Icon(Icons.Default.Person, null) }, leadingIcon = { Icon(Icons.Default.Person, null) },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true singleLine = true
@@ -147,7 +149,7 @@ fun ServerSettingsScreen(
OutlinedTextField( OutlinedTextField(
value = password, value = password,
onValueChange = { viewModel.updatePassword(it) }, onValueChange = { viewModel.updatePassword(it) },
label = { Text("Passwort") }, label = { Text(stringResource(R.string.password)) },
leadingIcon = { Icon(Icons.Default.Lock, null) }, leadingIcon = { Icon(Icons.Default.Lock, null) },
trailingIcon = { trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) { IconButton(onClick = { passwordVisible = !passwordVisible }) {
@@ -157,7 +159,7 @@ fun ServerSettingsScreen(
} else { } else {
Icons.Default.Visibility Icons.Default.Visibility
}, },
contentDescription = if (passwordVisible) "Verstecken" else "Anzeigen" contentDescription = if (passwordVisible) stringResource(R.string.server_password_hide) else stringResource(R.string.server_password_show)
) )
} }
}, },
@@ -187,14 +189,14 @@ fun ServerSettingsScreen(
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text("Server-Status:", style = MaterialTheme.typography.labelLarge) Text(stringResource(R.string.server_status_label), style = MaterialTheme.typography.labelLarge)
Text( Text(
text = when (serverStatus) { text = when (serverStatus) {
is SettingsViewModel.ServerStatus.Reachable -> "✅ Erreichbar" is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.server_status_reachable)
is SettingsViewModel.ServerStatus.Unreachable -> "❌ Nicht erreichbar" is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.server_status_unreachable)
is SettingsViewModel.ServerStatus.Checking -> "🔍 Prüfe..." is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.server_status_checking)
is SettingsViewModel.ServerStatus.NotConfigured -> "⚠️ Nicht konfiguriert" is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.server_status_not_configured)
else -> "❓ Unbekannt" else -> stringResource(R.string.server_status_unknown)
}, },
color = when (serverStatus) { color = when (serverStatus) {
is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50) is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50)
@@ -217,7 +219,7 @@ fun ServerSettingsScreen(
onClick = { viewModel.testConnection() }, onClick = { viewModel.testConnection() },
modifier = Modifier.weight(1f) modifier = Modifier.weight(1f)
) { ) {
Text("Verbindung testen") Text(stringResource(R.string.test_connection))
} }
Button( Button(
@@ -233,7 +235,7 @@ fun ServerSettingsScreen(
) )
Spacer(modifier = Modifier.width(8.dp)) Spacer(modifier = Modifier.width(8.dp))
} }
Text("Jetzt synchronisieren") Text(stringResource(R.string.sync_now))
} }
} }
} }

View File

@@ -1,5 +1,6 @@
package dev.dettmer.simplenotes.ui.settings.screens package dev.dettmer.simplenotes.ui.settings.screens
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -10,6 +11,7 @@ import androidx.compose.material.icons.filled.BugReport
import androidx.compose.material.icons.filled.Cloud import androidx.compose.material.icons.filled.Cloud
import androidx.compose.material.icons.filled.Description import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Sync import androidx.compose.material.icons.filled.Sync
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@@ -17,8 +19,10 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.BuildConfig import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsRoute import dev.dettmer.simplenotes.ui.settings.SettingsRoute
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.SettingsCard import dev.dettmer.simplenotes.ui.settings.components.SettingsCard
@@ -46,8 +50,18 @@ fun SettingsMainScreen(
viewModel.checkServerStatus() viewModel.checkServerStatus()
} }
// Get current language for display (no remember - always fresh value after activity recreate)
val locales = AppCompatDelegate.getApplicationLocales()
val currentLanguageName = if (locales.isEmpty) {
null // System default
} else {
locales[0]?.displayLanguage?.replaceFirstChar { it.uppercase() }
}
val systemDefaultText = stringResource(R.string.language_system_default)
val languageSubtitle = currentLanguageName ?: systemDefaultText
SettingsScaffold( SettingsScaffold(
title = "Einstellungen", title = stringResource(R.string.settings_title),
onBack = onBack onBack = onBack
) { paddingValues -> ) { paddingValues ->
LazyColumn( LazyColumn(
@@ -56,6 +70,16 @@ fun SettingsMainScreen(
.padding(paddingValues), .padding(paddingValues),
contentPadding = PaddingValues(vertical = 8.dp) contentPadding = PaddingValues(vertical = 8.dp)
) { ) {
// Language Settings
item {
SettingsCard(
icon = Icons.Default.Language,
title = stringResource(R.string.settings_language),
subtitle = languageSubtitle,
onClick = { onNavigate(SettingsRoute.Language) }
)
}
// Server-Einstellungen // Server-Einstellungen
item { item {
// v1.5.0 Fix: Nur Prefix-URLs gelten als "nicht konfiguriert" // v1.5.0 Fix: Nur Prefix-URLs gelten als "nicht konfiguriert"
@@ -65,13 +89,13 @@ fun SettingsMainScreen(
SettingsCard( SettingsCard(
icon = Icons.Default.Cloud, icon = Icons.Default.Cloud,
title = "Server-Einstellungen", title = stringResource(R.string.settings_server),
subtitle = if (isConfigured) serverUrl else null, subtitle = if (isConfigured) serverUrl else null,
statusText = when (serverStatus) { statusText = when (serverStatus) {
is SettingsViewModel.ServerStatus.Reachable -> "✅ Erreichbar" is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.settings_server_status_reachable)
is SettingsViewModel.ServerStatus.Unreachable -> "❌ Nicht erreichbar" is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.settings_server_status_unreachable)
is SettingsViewModel.ServerStatus.Checking -> "🔍 Prüfe..." is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.settings_server_status_checking)
is SettingsViewModel.ServerStatus.NotConfigured -> "⚠️ Nicht konfiguriert" is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.settings_server_status_not_configured)
else -> null else -> null
}, },
statusColor = when (serverStatus) { statusColor = when (serverStatus) {
@@ -87,14 +111,14 @@ fun SettingsMainScreen(
// Sync-Einstellungen // Sync-Einstellungen
item { item {
val intervalText = when (syncInterval) { val intervalText = when (syncInterval) {
15L -> "15 Min" 15L -> stringResource(R.string.settings_interval_15min)
60L -> "60 Min" 60L -> stringResource(R.string.settings_interval_60min)
else -> "30 Min" else -> stringResource(R.string.settings_interval_30min)
} }
SettingsCard( SettingsCard(
icon = Icons.Default.Sync, icon = Icons.Default.Sync,
title = "Sync-Einstellungen", title = stringResource(R.string.settings_sync),
subtitle = if (autoSyncEnabled) "Auto-Sync: An • $intervalText" else "Auto-Sync: Aus", subtitle = if (autoSyncEnabled) stringResource(R.string.settings_sync_auto_on, intervalText) else stringResource(R.string.settings_sync_auto_off),
onClick = { onNavigate(SettingsRoute.Sync) } onClick = { onNavigate(SettingsRoute.Sync) }
) )
} }
@@ -103,8 +127,8 @@ fun SettingsMainScreen(
item { item {
SettingsCard( SettingsCard(
icon = Icons.Default.Description, icon = Icons.Default.Description,
title = "Markdown Desktop-Integration", title = stringResource(R.string.settings_markdown),
subtitle = if (markdownAutoSync) "Auto-Sync: An" else "Auto-Sync: Aus", subtitle = if (markdownAutoSync) stringResource(R.string.settings_markdown_auto_on) else stringResource(R.string.settings_markdown_auto_off),
onClick = { onNavigate(SettingsRoute.Markdown) } onClick = { onNavigate(SettingsRoute.Markdown) }
) )
} }
@@ -113,8 +137,8 @@ fun SettingsMainScreen(
item { item {
SettingsCard( SettingsCard(
icon = Icons.Default.Backup, icon = Icons.Default.Backup,
title = "Backup & Wiederherstellung", title = stringResource(R.string.settings_backup),
subtitle = "Lokales oder Server-Backup", subtitle = stringResource(R.string.settings_backup_subtitle),
onClick = { onNavigate(SettingsRoute.Backup) } onClick = { onNavigate(SettingsRoute.Backup) }
) )
} }
@@ -123,8 +147,8 @@ fun SettingsMainScreen(
item { item {
SettingsCard( SettingsCard(
icon = Icons.Default.Info, icon = Icons.Default.Info,
title = "Über diese App", title = stringResource(R.string.settings_about),
subtitle = "Version ${BuildConfig.VERSION_NAME}", subtitle = stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
onClick = { onNavigate(SettingsRoute.About) } onClick = { onNavigate(SettingsRoute.About) }
) )
} }
@@ -133,8 +157,8 @@ fun SettingsMainScreen(
item { item {
SettingsCard( SettingsCard(
icon = Icons.Default.BugReport, icon = Icons.Default.BugReport,
title = "Debug & Diagnose", title = stringResource(R.string.settings_debug),
subtitle = if (fileLoggingEnabled) "Logging: An" else "Logging: Aus", subtitle = if (fileLoggingEnabled) stringResource(R.string.settings_debug_logging_on) else stringResource(R.string.settings_debug_logging_off),
onClick = { onNavigate(SettingsRoute.Debug) } onClick = { onNavigate(SettingsRoute.Debug) }
) )
} }

View File

@@ -13,7 +13,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.RadioOption import dev.dettmer.simplenotes.ui.settings.components.RadioOption
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
@@ -36,7 +38,7 @@ fun SyncSettingsScreen(
val syncInterval by viewModel.syncInterval.collectAsState() val syncInterval by viewModel.syncInterval.collectAsState()
SettingsScaffold( SettingsScaffold(
title = "Sync-Einstellungen", title = stringResource(R.string.sync_settings_title),
onBack = onBack onBack = onBack
) { paddingValues -> ) { paddingValues ->
Column( Column(
@@ -49,18 +51,14 @@ fun SyncSettingsScreen(
// Auto-Sync Info // Auto-Sync Info
SettingsInfoCard( SettingsInfoCard(
text = "🔄 Auto-Sync:\n" + text = stringResource(R.string.sync_auto_sync_info)
"• Prüft alle 30 Min. ob Server erreichbar\n" +
"• Funktioniert bei jeder WiFi-Verbindung\n" +
"• Läuft auch im Hintergrund\n" +
"• Minimaler Akkuverbrauch (~0.4%/Tag)"
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
// Auto-Sync Toggle // Auto-Sync Toggle
SettingsSwitch( SettingsSwitch(
title = "Auto-Sync aktiviert", title = stringResource(R.string.sync_auto_sync_enabled),
checked = autoSyncEnabled, checked = autoSyncEnabled,
onCheckedChange = { viewModel.setAutoSync(it) }, onCheckedChange = { viewModel.setAutoSync(it) },
icon = Icons.Default.Sync icon = Icons.Default.Sync
@@ -69,14 +67,10 @@ fun SyncSettingsScreen(
SettingsDivider() SettingsDivider()
// Sync Interval Section // Sync Interval Section
SettingsSectionHeader(text = "Sync-Intervall") SettingsSectionHeader(text = stringResource(R.string.sync_interval_section))
SettingsInfoCard( SettingsInfoCard(
text = "Legt fest, wie oft die App im Hintergrund synchronisiert. " + text = stringResource(R.string.sync_interval_info)
"Kürzere Intervalle bedeuten aktuellere Daten, verbrauchen aber etwas mehr Akku.\n\n" +
"⏱️ Hinweis: Wenn dein Smartphone im Standby ist, kann Android die " +
"Synchronisation verzögern (bis zu 60 Min.), um Akku zu sparen. " +
"Das ist normal und betrifft alle Hintergrund-Apps."
) )
Spacer(modifier = Modifier.height(8.dp)) Spacer(modifier = Modifier.height(8.dp))
@@ -85,18 +79,18 @@ fun SyncSettingsScreen(
val intervalOptions = listOf( val intervalOptions = listOf(
RadioOption( RadioOption(
value = 15L, value = 15L,
title = "⚡ Alle 15 Minuten", title = stringResource(R.string.sync_interval_15min_title),
subtitle = "Schnellste Synchronisation • ~0.8% Akku/Tag (~23 mAh)" subtitle = stringResource(R.string.sync_interval_15min_subtitle)
), ),
RadioOption( RadioOption(
value = 30L, value = 30L,
title = "✓ Alle 30 Minuten (Empfohlen)", title = stringResource(R.string.sync_interval_30min_title),
subtitle = "Ausgewogenes Verhältnis • ~0.4% Akku/Tag (~12 mAh)" subtitle = stringResource(R.string.sync_interval_30min_subtitle)
), ),
RadioOption( RadioOption(
value = 60L, value = 60L,
title = "🔋 Alle 60 Minuten", title = stringResource(R.string.sync_interval_60min_title),
subtitle = "Maximale Akkulaufzeit • ~0.2% Akku/Tag (~6 mAh geschätzt)" subtitle = stringResource(R.string.sync_interval_60min_subtitle)
) )
) )

View File

@@ -2,6 +2,7 @@ package dev.dettmer.simplenotes.utils
import android.content.Context import android.content.Context
import android.widget.Toast import android.widget.Toast
import dev.dettmer.simplenotes.R
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@@ -15,7 +16,7 @@ fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show() Toast.makeText(this, message, duration).show()
} }
// Timestamp to readable format // Timestamp to readable format (legacy - without context, uses German)
fun Long.toReadableTime(): String { fun Long.toReadableTime(): String {
val now = System.currentTimeMillis() val now = System.currentTimeMillis()
val diff = now - this val diff = now - this
@@ -41,6 +42,32 @@ fun Long.toReadableTime(): String {
} }
} }
// Timestamp to readable format (with context for i18n)
fun Long.toReadableTime(context: Context): String {
val now = System.currentTimeMillis()
val diff = now - this
return when {
diff < TimeUnit.MINUTES.toMillis(1) -> context.getString(R.string.time_just_now)
diff < TimeUnit.HOURS.toMillis(1) -> {
val minutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt()
context.getString(R.string.time_minutes_ago, minutes)
}
diff < TimeUnit.DAYS.toMillis(1) -> {
val hours = TimeUnit.MILLISECONDS.toHours(diff).toInt()
context.getString(R.string.time_hours_ago, hours)
}
diff < TimeUnit.DAYS.toMillis(DAYS_THRESHOLD) -> {
val days = TimeUnit.MILLISECONDS.toDays(diff).toInt()
context.getString(R.string.time_days_ago, days)
}
else -> {
val sdf = SimpleDateFormat("dd.MM.yyyy", Locale.getDefault())
sdf.format(Date(this))
}
}
}
// Truncate long strings // Truncate long strings
fun String.truncate(maxLength: Int): String { fun String.truncate(maxLength: Int): String {
return if (length > maxLength) { return if (length > maxLength) {

View File

@@ -2,6 +2,7 @@ package dev.dettmer.simplenotes.utils
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import dev.dettmer.simplenotes.R
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@@ -16,8 +17,6 @@ object NotificationHelper {
private const val TAG = "NotificationHelper" private const val TAG = "NotificationHelper"
private const val CHANNEL_ID = "notes_sync_channel" private const val CHANNEL_ID = "notes_sync_channel"
private const val CHANNEL_NAME = "Notizen Synchronisierung"
private const val CHANNEL_DESCRIPTION = "Benachrichtigungen über Sync-Status"
private const val NOTIFICATION_ID = 1001 private const val NOTIFICATION_ID = 1001
private const val SYNC_NOTIFICATION_ID = 2 private const val SYNC_NOTIFICATION_ID = 2
private const val AUTO_CANCEL_TIMEOUT_MS = 30_000L private const val AUTO_CANCEL_TIMEOUT_MS = 30_000L
@@ -29,9 +28,11 @@ object NotificationHelper {
fun createNotificationChannel(context: Context) { fun createNotificationChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val importance = NotificationManager.IMPORTANCE_DEFAULT val importance = NotificationManager.IMPORTANCE_DEFAULT
val channelName = context.getString(R.string.notification_channel_name)
val channelDescription = context.getString(R.string.notification_channel_desc)
val channel = NotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance).apply { val channel = NotificationChannel(CHANNEL_ID, channelName, importance).apply {
description = CHANNEL_DESCRIPTION description = channelDescription
enableVibration(true) enableVibration(true)
enableLights(true) enableLights(true)
} }
@@ -68,8 +69,8 @@ object NotificationHelper {
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_menu_upload) .setSmallIcon(android.R.drawable.ic_menu_upload)
.setContentTitle("Sync erfolgreich") .setContentTitle(context.getString(R.string.notification_sync_success_title))
.setContentText("$syncedCount Notiz(en) synchronisiert") .setContentText(context.getString(R.string.notification_sync_success_message, syncedCount))
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
@@ -96,7 +97,7 @@ object NotificationHelper {
fun showSyncFailureNotification(context: Context, errorMessage: String) { fun showSyncFailureNotification(context: Context, errorMessage: String) {
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_alert) .setSmallIcon(android.R.drawable.ic_dialog_alert)
.setContentTitle("Sync fehlgeschlagen") .setContentTitle(context.getString(R.string.notification_sync_failed_title))
.setContentText(errorMessage) .setContentText(errorMessage)
.setStyle(NotificationCompat.BigTextStyle() .setStyle(NotificationCompat.BigTextStyle()
.bigText(errorMessage)) .bigText(errorMessage))
@@ -125,8 +126,8 @@ object NotificationHelper {
fun showSyncProgressNotification(context: Context): Int { fun showSyncProgressNotification(context: Context): Int {
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_popup_sync) .setSmallIcon(android.R.drawable.ic_popup_sync)
.setContentTitle("Synchronisiere...") .setContentTitle(context.getString(R.string.notification_sync_progress_title))
.setContentText("Notizen werden synchronisiert") .setContentText(context.getString(R.string.notification_sync_progress_message))
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true) .setOngoing(true)
.setProgress(0, 0, true) .setProgress(0, 0, true)
@@ -161,8 +162,8 @@ object NotificationHelper {
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.ic_dialog_info) .setSmallIcon(android.R.drawable.ic_dialog_info)
.setContentTitle("Sync-Konflikt erkannt") .setContentTitle(context.getString(R.string.notification_sync_conflict_title))
.setContentText("$conflictCount Notiz(en) haben Konflikte") .setContentText(context.getString(R.string.notification_sync_conflict_message, conflictCount))
.setPriority(NotificationCompat.PRIORITY_HIGH) .setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)
.setAutoCancel(true) .setAutoCancel(true)
@@ -212,8 +213,8 @@ object NotificationHelper {
fun showSyncInProgress(context: Context) { fun showSyncInProgress(context: Context) {
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_sync) .setSmallIcon(android.R.drawable.stat_notify_sync)
.setContentTitle("Synchronisierung läuft") .setContentTitle(context.getString(R.string.notification_sync_in_progress_title))
.setContentText("Notizen werden synchronisiert...") .setContentText(context.getString(R.string.notification_sync_in_progress_message))
.setPriority(NotificationCompat.PRIORITY_LOW) .setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true) .setOngoing(true)
.build() .build()
@@ -240,8 +241,8 @@ object NotificationHelper {
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_sync) .setSmallIcon(android.R.drawable.stat_notify_sync)
.setContentTitle("Sync erfolgreich") .setContentTitle(context.getString(R.string.notification_sync_success_title))
.setContentText("$count Notizen synchronisiert") .setContentText(context.getString(R.string.notification_sync_success_message, count))
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_STATUS) .setCategory(NotificationCompat.CATEGORY_STATUS)
.setContentIntent(pendingIntent) // Click öffnet App .setContentIntent(pendingIntent) // Click öffnet App
@@ -271,7 +272,7 @@ object NotificationHelper {
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
.setContentTitle("Sync Fehler") .setContentTitle(context.getString(R.string.notification_sync_error_title))
.setContentText(message) .setContentText(message)
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_ERROR) .setCategory(NotificationCompat.CATEGORY_ERROR)
@@ -308,11 +309,10 @@ object NotificationHelper {
val notification = NotificationCompat.Builder(context, CHANNEL_ID) val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setSmallIcon(android.R.drawable.stat_notify_error) .setSmallIcon(android.R.drawable.stat_notify_error)
.setContentTitle("⚠️ Sync-Warnung") .setContentTitle(context.getString(R.string.notification_sync_warning_title))
.setContentText("Server seit ${hoursSinceLastSync}h nicht erreichbar") .setContentText(context.getString(R.string.notification_sync_warning_message, hoursSinceLastSync.toInt()))
.setStyle(NotificationCompat.BigTextStyle() .setStyle(NotificationCompat.BigTextStyle()
.bigText("Der WebDAV-Server ist seit ${hoursSinceLastSync} Stunden nicht erreichbar. " + .bigText(context.getString(R.string.notification_sync_warning_detail, hoursSinceLastSync.toInt())))
"Bitte prüfe deine Netzwerkverbindung oder Server-Einstellungen."))
.setPriority(NotificationCompat.PRIORITY_DEFAULT) .setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setCategory(NotificationCompat.CATEGORY_STATUS) .setCategory(NotificationCompat.CATEGORY_STATUS)
.setContentIntent(pendingIntent) .setContentIntent(pendingIntent)

View File

@@ -1,5 +1,7 @@
package dev.dettmer.simplenotes.utils package dev.dettmer.simplenotes.utils
import android.content.Context
import dev.dettmer.simplenotes.R
import java.net.URL import java.net.URL
/** /**
@@ -91,7 +93,7 @@ object UrlValidator {
* Validiert ob HTTP URL erlaubt ist * Validiert ob HTTP URL erlaubt ist
* @return Pair<Boolean, String?> - (isValid, errorMessage) * @return Pair<Boolean, String?> - (isValid, errorMessage)
*/ */
fun validateHttpUrl(url: String): Pair<Boolean, String?> { fun validateHttpUrl(context: Context, url: String): Pair<Boolean, String?> {
return try { return try {
val parsedUrl = URL(url) val parsedUrl = URL(url)
@@ -107,16 +109,15 @@ object UrlValidator {
} else { } else {
return Pair( return Pair(
false, false,
"HTTP ist nur für lokale Server erlaubt (z.B. 192.168.x.x, 10.x.x.x, nas.local). " + context.getString(R.string.error_http_local_only)
"Für öffentliche Server verwende bitte HTTPS."
) )
} }
} }
// Anderes Protokoll // Anderes Protokoll
Pair(false, "Ungültiges Protokoll: ${parsedUrl.protocol}. Bitte verwende HTTP oder HTTPS.") Pair(false, context.getString(R.string.error_invalid_protocol, parsedUrl.protocol))
} catch (e: Exception) { } catch (e: Exception) {
Pair(false, "Ungültige URL: ${e.message}") Pair(false, context.getString(R.string.error_invalid_url, e.message ?: ""))
} }
} }
} }

View File

@@ -0,0 +1,393 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- ============================= -->
<!-- APP IDENTITY -->
<!-- ============================= -->
<string name="app_name">Simple Notes</string>
<!-- ============================= -->
<!-- MAIN SCREEN -->
<!-- ============================= -->
<string name="main_title">Simple Notes</string>
<string name="no_notes_yet">Noch keine Notizen.\nTippe + um eine zu erstellen.</string>
<string name="add_note">Notiz hinzufügen</string>
<string name="sync">Synchronisieren</string>
<string name="settings">Einstellungen</string>
<string name="action_sync">Synchronisieren</string>
<string name="action_settings">Einstellungen</string>
<string name="action_close_selection">Auswahl beenden</string>
<string name="action_select_all">Alle auswählen</string>
<string name="action_delete_selected">Ausgewählte löschen</string>
<string name="selection_count">%d ausgewählt</string>
<!-- ============================= -->
<!-- EMPTY STATE -->
<!-- ============================= -->
<string name="empty_state_title">Noch keine Notizen</string>
<string name="empty_state_message">Tippe + um eine neue Notiz zu erstellen</string>
<!-- ============================= -->
<!-- FAB MENU -->
<!-- ============================= -->
<string name="fab_new_note">Neue Notiz</string>
<string name="fab_text_note">Text-Notiz</string>
<string name="fab_checklist">Checkliste</string>
<string name="create_text_note">Notiz</string>
<string name="create_checklist">Liste</string>
<!-- ============================= -->
<!-- NOTE CARD -->
<!-- ============================= -->
<string name="note_title_placeholder">Notiz-Titel</string>
<string name="note_content_placeholder">Notiz-Vorschau…</string>
<string name="note_timestamp_placeholder">Vor 2 Std</string>
<string name="untitled">Ohne Titel</string>
<string name="checklist_progress">%1$d/%2$d erledigt</string>
<string name="empty_checklist">Keine Einträge</string>
<!-- ============================= -->
<!-- SYNC STATUS BANNER -->
<!-- ============================= -->
<string name="sync_status">Sync-Status</string>
<string name="sync_syncing">Synchronisiere…</string>
<string name="sync_completed">Synchronisiert</string>
<string name="sync_error">Fehler</string>
<string name="sync_status_syncing">Synchronisiere…</string>
<string name="sync_status_completed">Synchronisierung abgeschlossen</string>
<string name="sync_status_error">Synchronisierung fehlgeschlagen</string>
<string name="sync_already_running">Synchronisierung läuft bereits</string>
<!-- ============================= -->
<!-- DELETE DIALOGS -->
<!-- ============================= -->
<string name="delete_note_title">Notiz löschen?</string>
<string name="delete_notes_title">%d Notizen löschen?</string>
<string name="delete_note_message">Wie möchtest du diese Notiz löschen?</string>
<string name="delete_notes_message">Wie möchtest du diese %d Notizen löschen?</string>
<string name="delete_everywhere">Überall löschen (auch Server)</string>
<string name="delete_local_only">Nur lokal löschen</string>
<string name="delete">Löschen</string>
<string name="cancel">Abbrechen</string>
<string name="ok">OK</string>
<!-- Legacy delete dialogs -->
<string name="legacy_delete_dialog_title">Notiz löschen</string>
<string name="legacy_delete_dialog_message">\"%s\" wird lokal gelöscht.\n\nAuch vom Server löschen?</string>
<string name="legacy_delete_from_server">Vom Server löschen</string>
<string name="legacy_delete_with_server">\"%s\" wird lokal und vom Server gelöscht</string>
<string name="legacy_delete_local_only">\"%s\" lokal gelöscht (Server bleibt)</string>
<!-- ============================= -->
<!-- SNACKBAR MESSAGES -->
<!-- ============================= -->
<string name="snackbar_undo">RÜCKGÄNGIG</string>
<string name="snackbar_note_deleted_local">\"%s\" lokal gelöscht</string>
<string name="snackbar_note_deleted_server">\"%s\" wird vom Server gelöscht</string>
<string name="snackbar_notes_deleted_local">%d Notiz(en) lokal gelöscht</string>
<string name="snackbar_notes_deleted_server">%d Notiz(en) werden vom Server gelöscht</string>
<string name="snackbar_deleted_from_server">Vom Server gelöscht</string>
<string name="snackbar_server_delete_failed">Server-Löschung fehlgeschlagen</string>
<string name="snackbar_server_error">Server-Fehler: %s</string>
<string name="snackbar_already_synced">Bereits synchronisiert</string>
<string name="snackbar_server_unreachable">Server nicht erreichbar</string>
<string name="snackbar_synced_count">✅ Gesynct: %d Notizen</string>
<!-- ============================= -->
<!-- URL VALIDATION ERRORS -->
<!-- ============================= -->
<string name="error_http_local_only">HTTP ist nur für lokale Server erlaubt (z.B. 192.168.x.x, 10.x.x.x, nas.local). Für öffentliche Server verwende bitte HTTPS.</string>
<string name="error_invalid_protocol">Ungültiges Protokoll: %s. Bitte verwende HTTP oder HTTPS.</string>
<string name="error_invalid_url">Ungültige URL: %s</string>
<string name="error_server_not_configured">WebDAV-Server nicht vollständig konfiguriert</string>
<string name="error_sardine_client_failed">Sardine Client konnte nicht erstellt werden</string>
<string name="error_server_url_not_configured">Server-URL nicht konfiguriert</string>
<!-- ============================= -->
<!-- NOTE EDITOR -->
<!-- ============================= -->
<string name="new_note">Neue Notiz</string>
<string name="edit_note">Notiz bearbeiten</string>
<string name="new_checklist">Neue Liste</string>
<string name="edit_checklist">Liste bearbeiten</string>
<string name="title">Titel</string>
<string name="content">Inhalt</string>
<string name="back">Zurück</string>
<string name="save">Speichern</string>
<string name="add_item">Element hinzufügen</string>
<string name="item_placeholder">Neues Element…</string>
<string name="reorder_item">Element verschieben</string>
<string name="drag_to_reorder">Ziehen zum Sortieren</string>
<string name="delete_item">Element löschen</string>
<string name="note_is_empty">Notiz ist leer</string>
<string name="note_saved">Notiz gespeichert</string>
<string name="note_deleted">Notiz gelöscht</string>
<!-- ============================= -->
<!-- SETTINGS - MAIN -->
<!-- ============================= -->
<string name="settings_title">Einstellungen</string>
<string name="settings_language">Sprache</string>
<string name="settings_language_subtitle">%s</string>
<string name="settings_server">Server-Einstellungen</string>
<string name="settings_server_status_reachable">✅ Erreichbar</string>
<string name="settings_server_status_unreachable">❌ Nicht erreichbar</string>
<string name="settings_server_status_checking">🔍 Prüfe…</string>
<string name="settings_server_status_not_configured">⚠️ Nicht konfiguriert</string>
<string name="settings_sync">Sync-Einstellungen</string>
<string name="settings_sync_auto_on">Auto-Sync: An • %s</string>
<string name="settings_sync_auto_off">Auto-Sync: Aus</string>
<string name="settings_interval_15min">15 Min</string>
<string name="settings_interval_30min">30 Min</string>
<string name="settings_interval_60min">60 Min</string>
<string name="settings_markdown">Markdown Desktop-Integration</string>
<string name="settings_markdown_auto_on">Auto-Sync: An</string>
<string name="settings_markdown_auto_off">Auto-Sync: Aus</string>
<string name="settings_backup">Backup &amp; Wiederherstellung</string>
<string name="settings_backup_subtitle">Lokales oder Server-Backup</string>
<string name="settings_about">Über diese App</string>
<string name="settings_debug">Debug &amp; Diagnose</string>
<string name="settings_debug_logging_on">Logging: An</string>
<string name="settings_debug_logging_off">Logging: Aus</string>
<!-- ============================= -->
<!-- SETTINGS - SERVER -->
<!-- ============================= -->
<string name="server_settings">Server-Einstellungen</string>
<string name="server_settings_title">Server-Einstellungen</string>
<string name="server_connection_type">Verbindungstyp</string>
<string name="server_connection_http">🏠 Intern (HTTP)</string>
<string name="server_connection_https">🌐 Extern (HTTPS)</string>
<string name="server_connection_http_hint">HTTP nur für lokale Netzwerke (z.B. 192.168.x.x, 10.x.x.x)</string>
<string name="server_connection_https_hint">HTTPS für sichere Verbindungen über das Internet</string>
<string name="server_address">Server-Adresse</string>
<string name="server_address_hint">z.B. http://192.168.0.188:8080/notes</string>
<string name="server_url">Server URL</string>
<string name="username">Benutzername</string>
<string name="password">Passwort</string>
<string name="server_password_show">Anzeigen</string>
<string name="server_password_hide">Verstecken</string>
<string name="server_status_label">Server-Status:</string>
<string name="server_status_reachable">✅ Erreichbar</string>
<string name="server_status_unreachable">❌ Nicht erreichbar</string>
<string name="server_status_checking">🔍 Prüfe…</string>
<string name="server_status_not_configured">⚠️ Nicht konfiguriert</string>
<string name="server_status_unknown">❓ Unbekannt</string>
<string name="test_connection">Verbindung testen</string>
<string name="sync_now">Jetzt synchronisieren</string>
<!-- ============================= -->
<!-- SETTINGS - SYNC -->
<!-- ============================= -->
<string name="sync_settings">Sync-Einstellungen</string>
<string name="sync_settings_title">Sync-Einstellungen</string>
<string name="auto_sync">Auto-Sync aktiviert</string>
<string name="sync_auto_sync_info">🔄 Auto-Sync:\n• Prüft alle 30 Min. ob Server erreichbar\n• Funktioniert bei jeder WiFi-Verbindung\n• Läuft auch im Hintergrund\n• Minimaler Akkuverbrauch (~0.4%/Tag)</string>
<string name="sync_auto_sync_enabled">Auto-Sync aktiviert</string>
<string name="sync_interval_section">Sync-Intervall</string>
<string name="sync_interval_info">Legt fest, wie oft die App im Hintergrund synchronisiert. Kürzere Intervalle bedeuten aktuellere Daten, verbrauchen aber etwas mehr Akku.\n\n⏱ Hinweis: Wenn dein Smartphone im Standby ist, kann Android die Synchronisation verzögern (bis zu 60 Min.), um Akku zu sparen. Das ist normal und betrifft alle Hintergrund-Apps.</string>
<string name="sync_interval_15min_title">⚡ Alle 15 Minuten</string>
<string name="sync_interval_15min_subtitle">Schnellste Synchronisation • ~0.8% Akku/Tag (~23 mAh)</string>
<string name="sync_interval_30min_title">✓ Alle 30 Minuten (Empfohlen)</string>
<string name="sync_interval_30min_subtitle">Ausgewogenes Verhältnis • ~0.4% Akku/Tag (~12 mAh)</string>
<string name="sync_interval_60min_title">🔋 Alle 60 Minuten</string>
<string name="sync_interval_60min_subtitle">Maximale Akkulaufzeit • ~0.2% Akku/Tag (~6 mAh geschätzt)</string>
<!-- Legacy -->
<string name="auto_sync_info"> Auto-Sync:\n\n• Prüft alle 30 Min ob Server erreichbar\n• Funktioniert bei jeder WiFi-Verbindung\n• Läuft auch im Hintergrund\n• Minimaler Akkuverbrauch (~0.4%%/Tag)</string>
<!-- ============================= -->
<!-- SETTINGS - MARKDOWN -->
<!-- ============================= -->
<string name="markdown_settings_title">Markdown Desktop-Integration</string>
<string name="markdown_dialog_title">Markdown Auto-Sync</string>
<string name="markdown_export_complete">✅ Export abgeschlossen</string>
<string name="markdown_export_progress">Exportiere %1$d/%2$d Notizen…</string>
<string name="markdown_info">📝 Exportiert Notizen zusätzlich als .md-Dateien. Mounte WebDAV als Netzlaufwerk um mit VS Code, Typora oder jedem Markdown-Editor zu bearbeiten. JSON-Sync bleibt primäres Format.</string>
<string name="markdown_auto_sync_title">Markdown Auto-Sync</string>
<string name="markdown_auto_sync_subtitle">Synchronisiert Notizen automatisch als .md-Dateien (Upload + Download bei jedem Sync)</string>
<string name="markdown_manual_sync_info">Manueller Sync exportiert alle Notizen als .md-Dateien und importiert .md-Dateien vom Server. Nützlich für einmalige Synchronisation.</string>
<string name="markdown_manual_sync_button">📝 Manueller Markdown-Sync</string>
<!-- ============================= -->
<!-- SETTINGS - BACKUP -->
<!-- ============================= -->
<string name="backup_settings_title">Backup &amp; Wiederherstellung</string>
<string name="backup_restore_title">Backup &amp; Wiederherstellung</string>
<string name="backup_auto_info">📦 Bei jeder Wiederherstellung wird automatisch ein Sicherheits-Backup erstellt.</string>
<string name="backup_local_section">Lokales Backup</string>
<string name="backup_create">💾 Backup erstellen</string>
<string name="backup_restore_file">📂 Aus Datei wiederherstellen</string>
<string name="backup_server_section">Server-Backup</string>
<string name="backup_restore_server">☁️ Vom Server wiederherstellen</string>
<string name="backup_restore_dialog_title">⚠️ Backup wiederherstellen?</string>
<string name="backup_restore_source">Quelle: %s</string>
<string name="backup_restore_source_file">Lokale Datei</string>
<string name="backup_restore_source_server">WebDAV Server</string>
<string name="backup_restore_mode_label">Wiederherstellungs-Modus:</string>
<string name="backup_mode_merge_title">⚪ Zusammenführen (Standard)</string>
<string name="backup_mode_merge_subtitle">Neue hinzufügen, Bestehende behalten</string>
<string name="backup_mode_merge_full">⚪ Zusammenführen (Standard)\n → Neue hinzufügen, Bestehende behalten</string>
<string name="backup_mode_replace_title">⚪ Ersetzen</string>
<string name="backup_mode_replace_subtitle">Alle löschen &amp; Backup importieren</string>
<string name="backup_mode_replace_full">⚪ Ersetzen\n → Alle löschen &amp; Backup importieren</string>
<string name="backup_mode_overwrite_title">⚪ Duplikate überschreiben</string>
<string name="backup_mode_overwrite_subtitle">Backup gewinnt bei Konflikten</string>
<string name="backup_mode_overwrite_full">⚪ Duplikate überschreiben\n → Backup gewinnt bei Konflikten</string>
<string name="backup_restore_info"> Ein Sicherheits-Backup wird vor dem Wiederherstellen automatisch erstellt.</string>
<string name="backup_restore_button">Wiederherstellen</string>
<!-- Legacy -->
<string name="backup_restore_warning">⚠️ Achtung:\n\nDie Wiederherstellung überschreibt ALLE lokalen Notizen mit den Daten vom Server. Diese Aktion kann nicht rückgängig gemacht werden!</string>
<string name="restore_from_server">Vom Server wiederherstellen</string>
<string name="restore_confirmation_title">⚠️ Vom Server wiederherstellen?</string>
<string name="restore_confirmation_message">WARNUNG: Alle lokalen Notizen werden gelöscht und durch die Notizen vom Server ersetzt.\n\nDieser Vorgang kann nicht rückgängig gemacht werden!</string>
<string name="restore_button">Wiederherstellen</string>
<string name="restore_progress">Stelle Notizen wieder her…</string>
<string name="restore_success">✓ %d Notizen wiederhergestellt</string>
<string name="restore_error">Fehler: %s</string>
<!-- ============================= -->
<!-- SETTINGS - DEBUG -->
<!-- ============================= -->
<string name="debug_settings_title">Debug &amp; Diagnose</string>
<string name="debug_file_logging_title">Datei-Logging</string>
<string name="debug_file_logging_subtitle">Sync-Logs in Datei speichern</string>
<string name="debug_privacy_info">🔒 Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren.</string>
<string name="debug_log_actions_section">Log-Aktionen</string>
<string name="debug_export_logs">📤 Logs exportieren &amp; teilen</string>
<string name="debug_logs_subject">SimpleNotes Sync Logs</string>
<string name="debug_logs_share_via">Logs teilen via…</string>
<string name="debug_delete_logs">🗑️ Logs löschen</string>
<string name="debug_delete_logs_title">Logs löschen?</string>
<string name="debug_delete_logs_message">Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht.</string>
<!-- Legacy -->
<string name="file_logging_privacy_notice"> Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren.</string>
<!-- ============================= -->
<!-- SETTINGS - LANGUAGE -->
<!-- ============================= -->
<string name="language_settings_title">Sprache</string>
<string name="language_system_default">Systemstandard</string>
<string name="language_english">English</string>
<string name="language_german">Deutsch</string>
<string name="language_info"> Wähle deine bevorzugte Sprache. Die App wird neu gestartet, um die Änderung anzuwenden.</string>
<string name="language_changed_restart">Sprache geändert. Neustart…</string>
<!-- ============================= -->
<!-- SETTINGS - ABOUT -->
<!-- ============================= -->
<string name="about_settings_title">Über diese App</string>
<string name="about_app_name">Simple Notes Sync</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_links_section">Links</string>
<string name="about_github_title">GitHub Repository</string>
<string name="about_github_subtitle">Quellcode, Issues &amp; Dokumentation</string>
<string name="about_developer_title">Entwickler</string>
<string name="about_developer_subtitle">GitHub Profil: @inventory69</string>
<string name="about_license_title">Lizenz</string>
<string name="about_license_subtitle">MIT License - Open Source</string>
<string name="about_privacy_title">🔒 Datenschutz</string>
<string name="about_privacy_text">Diese App sammelt keine Daten. Alle Notizen werden nur lokal auf deinem Gerät und auf deinem eigenen WebDAV-Server gespeichert. Keine Telemetrie, keine Werbung.</string>
<!-- ============================= -->
<!-- TOAST MESSAGES -->
<!-- ============================= -->
<string name="toast_connection_success">✅ Verbindung erfolgreich!</string>
<string name="toast_connection_failed">❌ %s</string>
<string name="toast_error">❌ Fehler: %s</string>
<string name="toast_syncing">🔄 Synchronisiere…</string>
<string name="toast_already_synced">✅ Bereits synchronisiert</string>
<string name="toast_sync_success">✅ %d Notizen synchronisiert</string>
<string name="toast_sync_failed">❌ %s</string>
<string name="toast_auto_sync_enabled">✅ Auto-Sync aktiviert</string>
<string name="toast_auto_sync_disabled">Auto-Sync deaktiviert</string>
<string name="toast_sync_interval">⏱️ Sync-Intervall: %s</string>
<string name="toast_sync_interval_15min">15 Minuten</string>
<string name="toast_sync_interval_30min">30 Minuten</string>
<string name="toast_sync_interval_60min">60 Minuten</string>
<string name="toast_configure_server_first">⚠️ Bitte zuerst WebDAV-Server konfigurieren</string>
<string name="toast_markdown_exported">✅ %d Notizen nach Markdown exportiert</string>
<string name="toast_markdown_enabled">📝 Markdown Auto-Sync aktiviert</string>
<string name="toast_markdown_disabled">📝 Markdown Auto-Sync deaktiviert</string>
<string name="toast_markdown_syncing">📝 Markdown-Sync läuft…</string>
<string name="toast_markdown_result">✅ Export: %1$d • Import: %2$d</string>
<string name="toast_export_failed">❌ Export fehlgeschlagen: %s</string>
<string name="toast_backup_success">✅ %s</string>
<string name="toast_backup_failed">❌ Backup fehlgeschlagen: %s</string>
<string name="toast_restore_success">✅ %d Notizen wiederhergestellt</string>
<string name="toast_restore_failed">❌ Wiederherstellung fehlgeschlagen: %s</string>
<string name="toast_notifications_enabled">Benachrichtigungen aktiviert</string>
<string name="toast_notifications_disabled">Benachrichtigungen deaktiviert. Du kannst sie in den Einstellungen aktivieren.</string>
<string name="toast_battery_optimization">Bitte Akku-Optimierung manuell deaktivieren</string>
<string name="toast_logs_deleted">🗑️ Logs gelöscht</string>
<string name="toast_no_logs_to_delete">📭 Keine Logs zum Löschen</string>
<string name="toast_logs_delete_error">❌ Fehler beim Löschen: %s</string>
<string name="toast_link_error">❌ Fehler beim Öffnen des Links</string>
<string name="toast_file_logging_enabled">📝 Datei-Logging aktiviert</string>
<string name="toast_file_logging_disabled">📝 Datei-Logging deaktiviert</string>
<string name="toast_sync_interval_changed">⏱️ Sync-Intervall auf %s geändert</string>
<string name="version_not_available">Version nicht verfügbar</string>
<string name="status_checking">🔍 Prüfe…</string>
<string name="battery_optimization_dialog_message">Bitte wähle \'Nicht optimieren\' für Simple Notes.</string>
<string name="battery_optimization_dialog_title">Hintergrund-Synchronisation</string>
<string name="battery_optimization_dialog_full_message">Damit die App im Hintergrund synchronisieren kann, muss die Akku-Optimierung deaktiviert werden.\n\nBitte wähle \'Nicht optimieren\' für Simple Notes.</string>
<string name="battery_optimization_open_settings">Einstellungen öffnen</string>
<string name="battery_optimization_later">Später</string>
<string name="content_description_back">Zurück</string>
<string name="error_invalid_backup_file">Ungültige Backup-Datei</string>
<string name="error_backup_version_unsupported">Backup-Version nicht unterstützt (v%1$d benötigt v%2$d+)</string>
<string name="error_backup_empty">Backup enthält keine Notizen</string>
<string name="error_backup_invalid_notes">Backup enthält %d ungültige Notizen</string>
<string name="error_backup_corrupt">Backup-Datei beschädigt oder ungültig: %s</string>
<string name="error_restore_failed">Wiederherstellung fehlgeschlagen: %s</string>
<string name="restore_merge_result">%1$d neue Notizen importiert, %2$d übersprungen</string>
<string name="restore_overwrite_result">%1$d neu, %2$d überschrieben</string>
<string name="restore_replace_result">Alle Notizen ersetzt: %d importiert</string>
<!-- ============================= -->
<!-- RELATIVE TIME -->
<!-- ============================= -->
<string name="time_just_now">Gerade eben</string>
<string name="time_minutes_ago">Vor %d Min</string>
<string name="time_hours_ago">Vor %d Std</string>
<string name="time_days_ago">Vor %d Tagen</string>
<!-- ============================= -->
<!-- NOTIFICATIONS -->
<!-- ============================= -->
<string name="notification_channel_name">Notizen Synchronisierung</string>
<string name="notification_channel_desc">Benachrichtigungen über Sync-Status</string>
<string name="notification_sync_success_title">Sync erfolgreich</string>
<string name="notification_sync_success_message">%d Notiz(en) synchronisiert</string>
<string name="notification_sync_failed_title">Sync fehlgeschlagen</string>
<string name="notification_sync_progress_title">Synchronisiere…</string>
<string name="notification_sync_progress_message">Notizen werden synchronisiert</string>
<string name="notification_sync_conflict_title">Sync-Konflikt erkannt</string>
<string name="notification_sync_conflict_message">%d Notiz(en) haben Konflikte</string>
<string name="notification_sync_warning_title">⚠️ Sync-Warnung</string>
<string name="notification_sync_warning_message">Server seit %dh nicht erreichbar</string>
<string name="notification_sync_warning_detail">Der WebDAV-Server ist seit %d Stunden nicht erreichbar. Bitte prüfe deine Netzwerkverbindung oder Server-Einstellungen.</string>
<string name="notification_sync_in_progress_title">Synchronisierung läuft</string>
<string name="notification_sync_in_progress_message">Notizen werden synchronisiert…</string>
<string name="notification_sync_error_title">Sync Fehler</string>
<!-- ============================= -->
<!-- PLURALS -->
<!-- ============================= -->
<plurals name="notes_count">
<item quantity="one">%d Notiz</item>
<item quantity="other">%d Notizen</item>
</plurals>
<plurals name="notes_deleted_local">
<item quantity="one">%d Notiz lokal gelöscht</item>
<item quantity="other">%d Notizen lokal gelöscht</item>
</plurals>
<plurals name="notes_deleted_server">
<item quantity="one">%d Notiz wird vom Server gelöscht</item>
<item quantity="other">%d Notizen werden vom Server gelöscht</item>
</plurals>
<plurals name="notes_synced">
<item quantity="one">%d Notiz synchronisiert</item>
<item quantity="other">%d Notizen synchronisiert</item>
</plurals>
</resources>

View File

@@ -1,94 +1,394 @@
<?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<!-- ============================= -->
<!-- APP IDENTITY -->
<!-- ============================= -->
<string name="app_name">Simple Notes</string> <string name="app_name">Simple Notes</string>
<!-- Main Activity --> <!-- ============================= -->
<string name="no_notes_yet">Noch keine Notizen.\nTippe + um eine zu erstellen.</string> <!-- MAIN SCREEN -->
<string name="add_note">Notiz hinzufügen</string> <!-- ============================= -->
<string name="sync">Synchronisieren</string> <string name="main_title">Simple Notes</string>
<string name="settings">Einstellungen</string> <string name="no_notes_yet">No notes yet.\nTap + to create one.</string>
<string name="add_note">Add note</string>
<string name="sync">Sync</string>
<string name="settings">Settings</string>
<string name="action_sync">Sync</string>
<string name="action_settings">Settings</string>
<string name="action_close_selection">Close selection</string>
<string name="action_select_all">Select all</string>
<string name="action_delete_selected">Delete selected</string>
<string name="selection_count">%d selected</string>
<!-- Empty State --> <!-- ============================= -->
<!-- EMPTY STATE -->
<!-- ============================= -->
<string name="empty_state_emoji">📝</string> <string name="empty_state_emoji">📝</string>
<string name="empty_state_title">Noch keine Notizen</string> <string name="empty_state_title">No notes yet</string>
<string name="empty_state_message">Tippe auf um deine erste Notiz zu erstellen</string> <string name="empty_state_message">Tap + to create a new note</string>
<!-- Note Editor --> <!-- ============================= -->
<string name="edit_note">Notiz bearbeiten</string> <!-- FAB MENU -->
<string name="new_note">Neue Notiz</string> <!-- ============================= -->
<string name="title">Titel</string> <string name="fab_new_note">New note</string>
<string name="content">Inhalt</string> <string name="fab_text_note">Text note</string>
<string name="save">Speichern</string> <string name="fab_checklist">Checklist</string>
<string name="delete">Löschen</string> <string name="create_text_note">Note</string>
<string name="back">Zurück</string> <string name="create_checklist">List</string>
<!-- Note List Item (Preview placeholders) --> <!-- ============================= -->
<!-- NOTE CARD -->
<!-- ============================= -->
<string name="note_title_placeholder">Note Title</string> <string name="note_title_placeholder">Note Title</string>
<string name="note_content_placeholder">Note content preview…</string> <string name="note_content_placeholder">Note content preview…</string>
<string name="note_timestamp_placeholder">Vor 2 Std</string> <string name="note_timestamp_placeholder">2 hours ago</string>
<string name="untitled">Ohne Titel</string> <string name="untitled">Untitled</string>
<string name="checklist_progress">%1$d/%2$d done</string>
<string name="empty_checklist">No entries</string>
<!-- Delete Confirmation Dialog --> <!-- ============================= -->
<string name="delete_note_title">Notiz löschen?</string> <!-- SYNC STATUS BANNER -->
<string name="delete_note_message">Diese Aktion kann nicht rückgängig gemacht werden.</string> <!-- ============================= -->
<string name="cancel">Abbrechen</string> <string name="sync_status">Sync Status</string>
<string name="sync_syncing">Syncing…</string>
<string name="sync_completed">Synced</string>
<string name="sync_error">Error</string>
<string name="sync_status_syncing">Syncing…</string>
<string name="sync_status_completed">Sync completed</string>
<string name="sync_status_error">Sync failed</string>
<string name="sync_already_running">Sync already in progress</string>
<!-- Settings --> <!-- ============================= -->
<string name="server_settings">Server-Einstellungen</string> <!-- DELETE DIALOGS -->
<!-- ============================= -->
<string name="delete_note_title">Delete note?</string>
<string name="delete_notes_title">Delete %d notes?</string>
<string name="delete_note_message">How do you want to delete this note?</string>
<string name="delete_notes_message">How do you want to delete these %d notes?</string>
<string name="delete_everywhere">Delete everywhere (also server)</string>
<string name="delete_local_only">Delete local only</string>
<string name="delete">Delete</string>
<string name="cancel">Cancel</string>
<string name="ok">OK</string>
<!-- Legacy delete dialogs -->
<string name="legacy_delete_dialog_title">Delete note</string>
<string name="legacy_delete_dialog_message">\"%s\" will be deleted locally.\n\nAlso delete from server?</string>
<string name="legacy_delete_from_server">Delete from server</string>
<string name="legacy_delete_with_server">\"%s\" will be deleted locally and from server</string>
<string name="legacy_delete_local_only">\"%s\" deleted locally (server remains)</string>
<!-- ============================= -->
<!-- SNACKBAR MESSAGES -->
<!-- ============================= -->
<string name="snackbar_undo">UNDO</string>
<string name="snackbar_note_deleted_local">\"%s\" deleted locally</string>
<string name="snackbar_note_deleted_server">\"%s\" will be deleted from server</string>
<string name="snackbar_notes_deleted_local">%d note(s) deleted locally</string>
<string name="snackbar_notes_deleted_server">%d note(s) will be deleted from server</string>
<string name="snackbar_deleted_from_server">Deleted from server</string>
<string name="snackbar_server_delete_failed">Server deletion failed</string>
<string name="snackbar_server_error">Server error: %s</string>
<string name="snackbar_already_synced">Already synced</string>
<string name="snackbar_server_unreachable">Server not reachable</string>
<string name="snackbar_synced_count">✅ Synced: %d notes</string>
<!-- ============================= -->
<!-- URL VALIDATION ERRORS -->
<!-- ============================= -->
<string name="error_http_local_only">HTTP is only allowed for local servers (e.g. 192.168.x.x, 10.x.x.x, nas.local). For public servers, please use HTTPS.</string>
<string name="error_invalid_protocol">Invalid protocol: %s. Please use HTTP or HTTPS.</string>
<string name="error_invalid_url">Invalid URL: %s</string>
<string name="error_server_not_configured">WebDAV server not fully configured</string>
<string name="error_sardine_client_failed">Sardine client could not be created</string>
<string name="error_server_url_not_configured">Server URL not configured</string>
<!-- ============================= -->
<!-- NOTE EDITOR -->
<!-- ============================= -->
<string name="new_note">New Note</string>
<string name="edit_note">Edit Note</string>
<string name="new_checklist">New List</string>
<string name="edit_checklist">Edit List</string>
<string name="title">Title</string>
<string name="content">Content</string>
<string name="back">Back</string>
<string name="save">Save</string>
<string name="add_item">Add item</string>
<string name="item_placeholder">New item…</string>
<string name="reorder_item">Reorder item</string>
<string name="drag_to_reorder">Drag to reorder</string>
<string name="delete_item">Delete item</string>
<string name="note_is_empty">Note is empty</string>
<string name="note_saved">Note saved</string>
<string name="note_deleted">Note deleted</string>
<!-- ============================= -->
<!-- SETTINGS - MAIN -->
<!-- ============================= -->
<string name="settings_title">Settings</string>
<string name="settings_language">Language</string>
<string name="settings_language_subtitle">%s</string>
<string name="settings_server">Server Settings</string>
<string name="settings_server_status_reachable">✅ Reachable</string>
<string name="settings_server_status_unreachable">❌ Not reachable</string>
<string name="settings_server_status_checking">🔍 Checking…</string>
<string name="settings_server_status_not_configured">⚠️ Not configured</string>
<string name="settings_sync">Sync Settings</string>
<string name="settings_sync_auto_on">Auto-Sync: On • %s</string>
<string name="settings_sync_auto_off">Auto-Sync: Off</string>
<string name="settings_interval_15min">15 min</string>
<string name="settings_interval_30min">30 min</string>
<string name="settings_interval_60min">60 min</string>
<string name="settings_markdown">Markdown Desktop Integration</string>
<string name="settings_markdown_auto_on">Auto-Sync: On</string>
<string name="settings_markdown_auto_off">Auto-Sync: Off</string>
<string name="settings_backup">Backup &amp; Restore</string>
<string name="settings_backup_subtitle">Local or server backup</string>
<string name="settings_about">About this App</string>
<string name="settings_debug">Debug &amp; Diagnostics</string>
<string name="settings_debug_logging_on">Logging: On</string>
<string name="settings_debug_logging_off">Logging: Off</string>
<!-- ============================= -->
<!-- SETTINGS - SERVER -->
<!-- ============================= -->
<string name="server_settings">Server Settings</string>
<string name="server_settings_title">Server Settings</string>
<string name="server_connection_type">Connection Type</string>
<string name="server_connection_http">🏠 Internal (HTTP)</string>
<string name="server_connection_https">🌐 External (HTTPS)</string>
<string name="server_connection_http_hint">HTTP only for local networks (e.g. 192.168.x.x, 10.x.x.x)</string>
<string name="server_connection_https_hint">HTTPS for secure connections over the internet</string>
<string name="server_address">Server Address</string>
<string name="server_address_hint">e.g. http://192.168.0.188:8080/notes</string>
<string name="server_url">Server URL</string> <string name="server_url">Server URL</string>
<string name="username">Benutzername</string> <string name="username">Username</string>
<string name="password">Passwort</string> <string name="password">Password</string>
<string name="server_status_label">Server-Status:</string> <string name="server_password_show">Show</string>
<string name="server_status_checking">Prüfe…</string> <string name="server_password_hide">Hide</string>
<string name="test_connection">Verbindung testen</string> <string name="server_status_label">Server Status:</string>
<string name="sync_now">Jetzt synchronisieren</string> <string name="server_status_reachable">✅ Reachable</string>
<string name="server_status_unreachable">❌ Not reachable</string>
<string name="server_status_checking">🔍 Checking…</string>
<string name="server_status_not_configured">⚠️ Not configured</string>
<string name="server_status_unknown">❓ Unknown</string>
<string name="test_connection">Test Connection</string>
<string name="sync_now">Sync now</string>
<!-- Auto-Sync Settings --> <!-- ============================= -->
<string name="sync_settings">Sync-Einstellungen</string> <!-- SETTINGS - SYNC -->
<string name="auto_sync">Auto-Sync aktiviert</string> <!-- ============================= -->
<string name="sync_status">Sync-Status</string> <string name="sync_settings">Sync Settings</string>
<string name="auto_sync_info"> Auto-Sync:\n\n• Prüft alle 30 Min ob Server erreichbar\n• Funktioniert bei jeder WiFi-Verbindung\n• Läuft auch im Hintergrund\n• Minimaler Akkuverbrauch (~0.4%%/Tag)</string> <string name="sync_settings_title">Sync Settings</string>
<string name="auto_sync">Auto-Sync enabled</string>
<string name="sync_auto_sync_info">🔄 Auto-Sync:\n• Checks every 30 min if server is reachable\n• Works on any WiFi connection\n• Runs in background\n• Minimal battery usage (~0.4%/day)</string>
<string name="sync_auto_sync_enabled">Auto-Sync enabled</string>
<string name="sync_interval_section">Sync Interval</string>
<string name="sync_interval_info">Determines how often the app syncs in the background. Shorter intervals mean more up-to-date data, but use slightly more battery.\n\n⏱ Note: When your phone is in standby, Android may delay syncs (up to 60 min) to save battery. This is normal and affects all background apps.</string>
<string name="sync_interval_15min_title">⚡ Every 15 minutes</string>
<string name="sync_interval_15min_subtitle">Fastest sync • ~0.8% battery/day (~23 mAh)</string>
<string name="sync_interval_30min_title">✓ Every 30 minutes (Recommended)</string>
<string name="sync_interval_30min_subtitle">Balanced ratio • ~0.4% battery/day (~12 mAh)</string>
<string name="sync_interval_60min_title">🔋 Every 60 minutes</string>
<string name="sync_interval_60min_subtitle">Maximum battery life • ~0.2% battery/day (~6 mAh est.)</string>
<!-- Legacy -->
<string name="auto_sync_info"> Auto-Sync:\n\n• Checks every 30 min if server is reachable\n• Works on any WiFi connection\n• Runs in background\n• Minimal battery usage (~0.4%%/day)</string>
<!-- Backup & Restore --> <!-- ============================= -->
<string name="backup_restore_title">Backup &amp; Wiederherstellung</string> <!-- SETTINGS - MARKDOWN -->
<string name="backup_restore_warning">⚠️ Achtung:\n\nDie Wiederherstellung überschreibt ALLE lokalen Notizen mit den Daten vom Server. Diese Aktion kann nicht rückgängig gemacht werden!</string> <!-- ============================= -->
<string name="restore_from_server">Vom Server wiederherstellen</string> <string name="markdown_settings_title">Markdown Desktop Integration</string>
<string name="restore_confirmation_title">⚠️ Vom Server wiederherstellen?</string> <string name="markdown_dialog_title">Markdown Auto-Sync</string>
<string name="restore_confirmation_message">WARNUNG: Alle lokalen Notizen werden gelöscht und durch die Notizen vom Server ersetzt.\n\nDieser Vorgang kann nicht rückgängig gemacht werden!</string> <string name="markdown_export_complete">✅ Export complete</string>
<string name="restore_button">Wiederherstellen</string> <string name="markdown_export_progress">Exporting %1$d/%2$d notes…</string>
<string name="restore_progress">Stelle Notizen wieder her…</string> <string name="markdown_info">📝 Exports notes additionally as .md files. Mount WebDAV as network drive to edit with VS Code, Typora, or any Markdown editor. JSON sync remains primary format.</string>
<string name="restore_success">✓ %d Notizen wiederhergestellt</string> <string name="markdown_auto_sync_title">Markdown Auto-Sync</string>
<string name="restore_error">Fehler: %s</string> <string name="markdown_auto_sync_subtitle">Automatically syncs notes as .md files (upload + download on each sync)</string>
<string name="markdown_manual_sync_info">Manual sync exports all notes as .md files and imports .md files from the server. Useful for one-time sync.</string>
<string name="markdown_manual_sync_button">📝 Manual Markdown Sync</string>
<!-- Sync Status Banner (v1.3.1) --> <!-- ============================= -->
<string name="sync_status_syncing">Synchronisiere…</string> <!-- SETTINGS - BACKUP -->
<string name="sync_status_completed">Synchronisierung abgeschlossen</string> <!-- ============================= -->
<string name="sync_status_error">Synchronisierung fehlgeschlagen</string> <string name="backup_settings_title">Backup &amp; Restore</string>
<string name="sync_already_running">Synchronisierung läuft bereits</string> <string name="backup_restore_title">Backup &amp; Restore</string>
<string name="backup_auto_info">📦 A safety backup is automatically created before each restore.</string>
<string name="backup_local_section">Local Backup</string>
<string name="backup_create">💾 Create Backup</string>
<string name="backup_restore_file">📂 Restore from File</string>
<string name="backup_server_section">Server Backup</string>
<string name="backup_restore_server">☁️ Restore from Server</string>
<string name="backup_restore_dialog_title">⚠️ Restore Backup?</string>
<string name="backup_restore_source">Source: %s</string>
<string name="backup_restore_source_file">Local File</string>
<string name="backup_restore_source_server">WebDAV Server</string>
<string name="backup_restore_mode_label">Restore Mode:</string>
<string name="backup_mode_merge_title">⚪ Merge (Default)</string>
<string name="backup_mode_merge_subtitle">Add new, keep existing</string>
<string name="backup_mode_merge_full">⚪ Merge (Default)\n → Add new, keep existing</string>
<string name="backup_mode_replace_title">⚪ Replace</string>
<string name="backup_mode_replace_subtitle">Delete all &amp; import backup</string>
<string name="backup_mode_replace_full">⚪ Replace\n → Delete all &amp; import backup</string>
<string name="backup_mode_overwrite_title">⚪ Overwrite duplicates</string>
<string name="backup_mode_overwrite_subtitle">Backup wins on conflicts</string>
<string name="backup_mode_overwrite_full">⚪ Overwrite duplicates\n → Backup wins on conflicts</string>
<string name="backup_restore_info"> A safety backup will be automatically created before restoring.</string>
<string name="backup_restore_button">Restore</string>
<!-- Legacy -->
<string name="backup_restore_warning">⚠️ Warning:\n\nRestoring will overwrite ALL local notes with data from the server. This action cannot be undone!</string>
<string name="restore_from_server">Restore from Server</string>
<string name="restore_confirmation_title">⚠️ Restore from Server?</string>
<string name="restore_confirmation_message">WARNING: All local notes will be deleted and replaced with notes from the server.\n\nThis action cannot be undone!</string>
<string name="restore_button">Restore</string>
<string name="restore_progress">Restoring notes…</string>
<string name="restore_success">✓ %d notes restored</string>
<string name="restore_error">Error: %s</string>
<!-- Debug/Logging Section (v1.3.2) --> <!-- ============================= -->
<string name="file_logging_privacy_notice"> Datenschutz: Logs werden nur lokal auf deinem Gerät gespeichert und niemals an externe Server gesendet. Die Logs enthalten Sync-Aktivitäten zur Fehlerdiagnose. Du kannst sie jederzeit löschen oder exportieren.</string> <!-- SETTINGS - DEBUG -->
<!-- ============================= -->
<string name="debug_settings_title">Debug &amp; Diagnostics</string>
<string name="debug_file_logging_title">File Logging</string>
<string name="debug_file_logging_subtitle">Save sync logs to file</string>
<string name="debug_privacy_info">🔒 Privacy: Logs are only stored locally on your device and are never sent to external servers. Logs contain sync activities for troubleshooting. You can delete or export them at any time.</string>
<string name="debug_log_actions_section">Log Actions</string>
<string name="debug_export_logs">📤 Export &amp; Share Logs</string>
<string name="debug_logs_subject">SimpleNotes Sync Logs</string>
<string name="debug_logs_share_via">Share logs via…</string>
<string name="debug_delete_logs">🗑️ Delete Logs</string>
<string name="debug_delete_logs_title">Delete logs?</string>
<string name="debug_delete_logs_message">All saved sync logs will be permanently deleted.</string>
<!-- Legacy -->
<string name="file_logging_privacy_notice"> Privacy: Logs are only stored locally on your device and are never sent to external servers. Logs contain sync activities for troubleshooting. You can delete or export them at any time.</string>
<!-- ========================== --> <!-- ============================= -->
<!-- CHECKLIST FEATURE (v1.4.0) --> <!-- SETTINGS - LANGUAGE -->
<!-- ========================== --> <!-- ============================= -->
<string name="language_settings_title">Language</string>
<string name="language_system_default">System Default</string>
<string name="language_english">English</string>
<string name="language_german">Deutsch</string>
<string name="language_info"> Choose your preferred language. The app will restart to apply the change.</string>
<string name="language_changed_restart">Language changed. Restarting…</string>
<!-- FAB Menu --> <!-- ============================= -->
<string name="create_text_note">Notiz</string> <!-- SETTINGS - ABOUT -->
<string name="create_checklist">Liste</string> <!-- ============================= -->
<string name="about_settings_title">About this App</string>
<string name="about_app_name">Simple Notes Sync</string>
<string name="about_version">Version %1$s (%2$d)</string>
<string name="about_links_section">Links</string>
<string name="about_github_title">GitHub Repository</string>
<string name="about_github_subtitle">Source code, issues &amp; documentation</string>
<string name="about_developer_title">Developer</string>
<string name="about_developer_subtitle">GitHub Profile: @inventory69</string>
<string name="about_license_title">License</string>
<string name="about_license_subtitle">MIT License - Open Source</string>
<string name="about_privacy_title">🔒 Privacy</string>
<string name="about_privacy_text">This app collects no data. All notes are stored only locally on your device and on your own WebDAV server. No telemetry, no ads.</string>
<!-- Editor --> <!-- ============================= -->
<string name="new_checklist">Neue Liste</string> <!-- TOAST MESSAGES -->
<string name="edit_checklist">Liste bearbeiten</string> <!-- ============================= -->
<string name="add_item">Element hinzufügen</string> <string name="toast_connection_success">✅ Connection successful!</string>
<string name="item_placeholder">Neues Element…</string> <string name="toast_connection_failed">❌ %s</string>
<string name="reorder_item">Element verschieben</string> <string name="toast_error">❌ Error: %s</string>
<string name="drag_to_reorder">Ziehen zum Sortieren</string> <string name="toast_syncing">🔄 Syncing…</string>
<string name="delete_item">Element löschen</string> <string name="toast_already_synced">✅ Already synced</string>
<string name="note_is_empty">Notiz ist leer</string> <string name="toast_sync_success">✅ %d notes synced</string>
<string name="note_saved">Notiz gespeichert</string> <string name="toast_sync_failed">❌ %s</string>
<string name="note_deleted">Notiz gelöscht</string> <string name="toast_auto_sync_enabled">✅ Auto-Sync enabled</string>
<string name="toast_auto_sync_disabled">Auto-Sync disabled</string>
<string name="toast_sync_interval">⏱️ Sync interval: %s</string>
<string name="toast_sync_interval_15min">15 minutes</string>
<string name="toast_sync_interval_30min">30 minutes</string>
<string name="toast_sync_interval_60min">60 minutes</string>
<string name="toast_configure_server_first">⚠️ Please configure WebDAV server first</string>
<string name="toast_markdown_exported">✅ %d notes exported to Markdown</string>
<string name="toast_markdown_enabled">📝 Markdown Auto-Sync enabled</string>
<string name="toast_markdown_disabled">📝 Markdown Auto-Sync disabled</string>
<string name="toast_markdown_syncing">📝 Markdown sync running…</string>
<string name="toast_markdown_result">✅ Export: %1$d • Import: %2$d</string>
<string name="toast_export_failed">❌ Export failed: %s</string>
<string name="toast_backup_success">✅ %s</string>
<string name="toast_backup_failed">❌ Backup failed: %s</string>
<string name="toast_restore_success">✅ %d notes restored</string>
<string name="toast_restore_failed">❌ Restore failed: %s</string>
<string name="toast_notifications_enabled">Notifications enabled</string>
<string name="toast_notifications_disabled">Notifications disabled. You can enable them in settings.</string>
<string name="toast_battery_optimization">Please disable battery optimization manually</string>
<string name="toast_logs_deleted">🗑️ Logs deleted</string>
<string name="toast_no_logs_to_delete">📭 No logs to delete</string>
<string name="toast_logs_delete_error">❌ Error deleting: %s</string>
<string name="toast_link_error">❌ Error opening link</string>
<string name="toast_file_logging_enabled">📝 File logging enabled</string>
<string name="toast_file_logging_disabled">📝 File logging disabled</string>
<string name="toast_sync_interval_changed">⏱️ Sync interval changed to %s</string>
<string name="version_not_available">Version not available</string>
<string name="status_checking">🔍 Checking…</string>
<string name="battery_optimization_dialog_message">Please select \'Not optimized\' for Simple Notes.</string>
<string name="battery_optimization_dialog_title">Background Synchronization</string>
<string name="battery_optimization_dialog_full_message">For the app to sync in the background, battery optimization must be disabled.\n\nPlease select \'Not optimized\' for Simple Notes.</string>
<string name="battery_optimization_open_settings">Open Settings</string>
<string name="battery_optimization_later">Later</string>
<string name="content_description_back">Back</string>
<string name="error_invalid_backup_file">Invalid backup file</string>
<string name="error_backup_version_unsupported">Backup version not supported (v%1$d requires v%2$d+)</string>
<string name="error_backup_empty">Backup contains no notes</string>
<string name="error_backup_invalid_notes">Backup contains %d invalid notes</string>
<string name="error_backup_corrupt">Backup file corrupt or invalid: %s</string>
<string name="error_restore_failed">Restore failed: %s</string>
<string name="restore_merge_result">%1$d new notes imported, %2$d skipped</string>
<string name="restore_overwrite_result">%1$d new, %2$d overwritten</string>
<string name="restore_replace_result">All notes replaced: %d imported</string>
<!-- List Preview --> <!-- ============================= -->
<string name="checklist_progress">%1$d/%2$d erledigt</string> <!-- RELATIVE TIME -->
<string name="empty_checklist">Keine Einträge</string> <!-- ============================= -->
<string name="time_just_now">Just now</string>
<string name="time_minutes_ago">%d min ago</string>
<string name="time_hours_ago">%d hours ago</string>
<string name="time_days_ago">%d days ago</string>
<!-- ============================= -->
<!-- NOTIFICATIONS -->
<!-- ============================= -->
<string name="notification_channel_name">Notes Synchronization</string>
<string name="notification_channel_desc">Notifications about sync status</string>
<string name="notification_sync_success_title">Sync successful</string>
<string name="notification_sync_success_message">%d note(s) synchronized</string>
<string name="notification_sync_failed_title">Sync failed</string>
<string name="notification_sync_progress_title">Syncing…</string>
<string name="notification_sync_progress_message">Notes are being synchronized</string>
<string name="notification_sync_conflict_title">Sync conflict detected</string>
<string name="notification_sync_conflict_message">%d note(s) have conflicts</string>
<string name="notification_sync_warning_title">⚠️ Sync Warning</string>
<string name="notification_sync_warning_message">Server unreachable for %dh</string>
<string name="notification_sync_warning_detail">The WebDAV server has been unreachable for %d hours. Please check your network connection or server settings.</string>
<string name="notification_sync_in_progress_title">Synchronization in progress</string>
<string name="notification_sync_in_progress_message">Notes are being synchronized…</string>
<string name="notification_sync_error_title">Sync Error</string>
<!-- ============================= -->
<!-- PLURALS -->
<!-- ============================= -->
<plurals name="notes_count">
<item quantity="one">%d note</item>
<item quantity="other">%d notes</item>
</plurals>
<plurals name="notes_deleted_local">
<item quantity="one">%d note deleted locally</item>
<item quantity="other">%d notes deleted locally</item>
</plurals>
<plurals name="notes_deleted_server">
<item quantity="one">%d note will be deleted from server</item>
<item quantity="other">%d notes will be deleted from server</item>
</plurals>
<plurals name="notes_synced">
<item quantity="one">%d note synced</item>
<item quantity="other">%d notes synced</item>
</plurals>
</resources> </resources>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<locale-config xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Default/Fallback language -->
<locale android:name="en" />
<!-- Supported languages -->
<locale android:name="de" />
</locale-config>