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:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.SimpleNotes"

View File

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

View File

@@ -227,9 +227,9 @@ class SettingsActivity : AppCompatActivity() {
*/
private fun updateProtocolHint() {
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 {
"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"
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")
} else {
showToast("⏱️ Sync-Intervall gespeichert (Auto-Sync ist deaktiviert)")
@@ -379,7 +379,7 @@ class SettingsActivity : AppCompatActivity() {
textViewAppVersion.text = "Version $versionName ($versionCode)"
} catch (e: Exception) {
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
@@ -475,12 +475,12 @@ class SettingsActivity : AppCompatActivity() {
*/
private fun showClearLogsConfirmation() {
AlertDialog.Builder(this)
.setTitle("Logs löschen?")
.setMessage("Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht.")
.setPositiveButton("Löschen") { _, _ ->
.setTitle(getString(R.string.debug_delete_logs_title))
.setMessage(getString(R.string.debug_delete_logs_message))
.setPositiveButton(getString(R.string.delete)) { _, _ ->
clearLogs()
}
.setNegativeButton("Abbrechen", null)
.setNegativeButton(getString(R.string.cancel), null)
.show()
}
@@ -491,13 +491,13 @@ class SettingsActivity : AppCompatActivity() {
try {
val cleared = Logger.clearLogFile(this)
if (cleared) {
showToast("🗑️ Logs gelöscht")
showToast(getString(R.string.toast_logs_deleted))
} else {
showToast("📭 Keine Logs zum Löschen")
showToast(getString(R.string.toast_no_logs_to_delete))
}
} catch (e: Exception) {
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)
} catch (e: Exception) {
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)
if (fullUrl.isNotEmpty()) {
val (isValid, errorMessage) = UrlValidator.validateHttpUrl(fullUrl)
val (isValid, errorMessage) = UrlValidator.validateHttpUrl(this, fullUrl)
if (!isValid) {
// Only show error in TextField (no Toast)
textInputLayoutServerUrl.isErrorEnabled = true
@@ -552,7 +552,7 @@ class SettingsActivity : AppCompatActivity() {
// 🔥 v1.1.2: Validate before testing
if (fullUrl.isNotEmpty()) {
val (isValid, errorMessage) = UrlValidator.validateHttpUrl(fullUrl)
val (isValid, errorMessage) = UrlValidator.validateHttpUrl(this, fullUrl)
if (!isValid) {
// Only show error in TextField (no Toast)
textInputLayoutServerUrl.isErrorEnabled = true
@@ -646,7 +646,7 @@ class SettingsActivity : AppCompatActivity() {
return
}
textViewServerStatus.text = "🔍 Prüfe..."
textViewServerStatus.text = getString(R.string.status_checking)
textViewServerStatus.setTextColor(getColor(android.R.color.darker_gray))
lifecycleScope.launch {
@@ -803,12 +803,12 @@ class SettingsActivity : AppCompatActivity() {
.setMessage(
"Damit die App im Hintergrund synchronisieren kann, " +
"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()
}
.setNegativeButton("Später") { dialog, _ ->
.setNegativeButton(getString(R.string.battery_optimization_later)) { dialog, _ ->
dialog.dismiss()
}
.setCancelable(false)
@@ -915,20 +915,20 @@ class SettingsActivity : AppCompatActivity() {
// Radio Buttons erstellen
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()
isChecked = true
setPadding(10, 10, 10, 10)
}
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()
setPadding(10, 10, 10, 10)
}
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()
setPadding(10, 10, 10, 10)
}
@@ -978,7 +978,7 @@ class SettingsActivity : AppCompatActivity() {
RestoreSource.WEBDAV_SERVER -> performRestoreFromServer(selectedMode)
}
}
.setNegativeButton("Abbrechen", null)
.setNegativeButton(getString(R.string.cancel), null)
.show()
}

View File

@@ -5,6 +5,7 @@ import android.net.Uri
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.utils.Logger
@@ -144,7 +145,7 @@ class BackupManager(private val context: Context) {
if (!validationResult.isValid) {
return@withContext RestoreResult(
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)
RestoreResult(
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) {
return ValidationResult(
isValid = false,
errorMessage = "Backup-Version nicht unterstützt " +
"(v${backupData.backupVersion} benötigt v${BACKUP_VERSION}+)"
errorMessage = context.getString(R.string.error_backup_version_unsupported, backupData.backupVersion, BACKUP_VERSION)
)
}
@@ -196,7 +196,7 @@ class BackupManager(private val context: Context) {
if (backupData.notes.isEmpty()) {
return ValidationResult(
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()) {
return ValidationResult(
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) {
ValidationResult(
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,
importedNotes = newNotes.size,
skippedNotes = skippedNotes,
message = "${newNotes.size} neue Notizen importiert, $skippedNotes übersprungen"
message = context.getString(R.string.restore_merge_result, newNotes.size, skippedNotes)
)
}
@@ -262,7 +262,7 @@ class BackupManager(private val context: Context) {
success = true,
importedNotes = backupNotes.size,
skippedNotes = 0,
message = "Alle Notizen ersetzt: ${backupNotes.size} importiert"
message = context.getString(R.string.restore_replace_result, backupNotes.size)
)
}
@@ -287,7 +287,7 @@ class BackupManager(private val context: Context) {
importedNotes = newNotes.size,
skippedNotes = 0,
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.impl.OkHttpSardine
import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.DeletionTracker
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.SyncStatus
@@ -1852,15 +1853,15 @@ class WebDavSyncService(private val context: Context) {
suspend fun manualMarkdownSync(): ManualMarkdownSyncResult = withContext(Dispatchers.IO) {
return@withContext try {
val sardine = getOrCreateSardine()
?: throw SyncException("Sardine client konnte nicht erstellt werden")
?: throw SyncException(context.getString(R.string.error_sardine_client_failed))
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 password = prefs.getString(Constants.KEY_PASSWORD, "") ?: ""
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")

View File

@@ -24,10 +24,12 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.lifecycleScope
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.google.android.material.color.DynamicColors
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.editor.ComposeNoteEditorActivity
import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.models.SyncStatus
@@ -339,10 +341,10 @@ class ComposeMainActivity : ComponentActivity() {
REQUEST_NOTIFICATION_PERMISSION -> {
if (grantResults.isNotEmpty() &&
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 {
Toast.makeText(this,
"Benachrichtigungen deaktiviert. Du kannst sie in den Einstellungen aktivieren.",
getString(R.string.toast_notifications_disabled),
Toast.LENGTH_SHORT
).show()
}
@@ -363,21 +365,21 @@ private fun DeleteConfirmationDialog(
) {
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Notiz löschen") },
title = { Text(stringResource(R.string.legacy_delete_dialog_title)) },
text = {
Text("\"$noteTitle\" wird lokal gelöscht.\n\nAuch vom Server löschen?")
Text(stringResource(R.string.legacy_delete_dialog_message, noteTitle))
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text("Abbrechen")
Text(stringResource(R.string.cancel))
}
},
confirmButton = {
TextButton(onClick = onDeleteLocal) {
Text("Nur lokal")
Text(stringResource(R.string.delete_local_only))
}
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.sync.SyncStateManager
import dev.dettmer.simplenotes.ui.main.components.DeleteConfirmationDialog
@@ -231,7 +233,7 @@ private fun MainTopBar(
TopAppBar(
title = {
Text(
text = "Simple Notes",
text = stringResource(R.string.main_title),
style = MaterialTheme.typography.titleLarge
)
},
@@ -242,13 +244,13 @@ private fun MainTopBar(
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = "Synchronisieren"
contentDescription = stringResource(R.string.action_sync)
)
}
IconButton(onClick = onSettingsClick) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Einstellungen"
contentDescription = stringResource(R.string.action_settings)
)
}
},
@@ -276,13 +278,13 @@ private fun SelectionTopBar(
IconButton(onClick = onCloseSelection) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = "Auswahl beenden"
contentDescription = stringResource(R.string.action_close_selection)
)
}
},
title = {
Text(
text = "$selectedCount ausgewählt",
text = stringResource(R.string.selection_count, selectedCount),
style = MaterialTheme.typography.titleLarge
)
},
@@ -292,7 +294,7 @@ private fun SelectionTopBar(
IconButton(onClick = onSelectAll) {
Icon(
imageVector = Icons.Default.SelectAll,
contentDescription = "Alle auswählen"
contentDescription = stringResource(R.string.action_select_all)
)
}
}
@@ -303,7 +305,7 @@ private fun SelectionTopBar(
) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Ausgewählte löschen",
contentDescription = stringResource(R.string.action_delete_selected),
tint = if (selectedCount > 0) {
MaterialTheme.colorScheme.error
} else {

View File

@@ -5,6 +5,7 @@ import android.content.Context
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.storage.NotesStorage
import dev.dettmer.simplenotes.sync.SyncStateManager
import dev.dettmer.simplenotes.sync.WebDavSyncService
@@ -236,15 +237,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Show snackbar with undo
val count = selectedNotes.size
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 {
"$count Notiz${if (count > 1) "en" else ""} lokal gelöscht"
getString(R.string.snackbar_notes_deleted_local, count)
}
viewModelScope.launch {
_showSnackbar.emit(SnackbarData(
message = message,
actionLabel = "RÜCKGÄNGIG",
actionLabel = getString(R.string.snackbar_undo),
onAction = {
undoDeleteMultiple(selectedNotes)
}
@@ -336,15 +337,15 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// Show snackbar with undo
val message = if (deleteFromServer) {
"\"${note.title}\" wird vom Server gelöscht"
getString(R.string.snackbar_note_deleted_server, note.title)
} else {
"\"${note.title}\" lokal gelöscht"
getString(R.string.snackbar_note_deleted_local, note.title)
}
viewModelScope.launch {
_showSnackbar.emit(SnackbarData(
message = message,
actionLabel = "RÜCKGÄNGIG",
actionLabel = getString(R.string.snackbar_undo),
onAction = {
undoDelete(note)
}
@@ -390,12 +391,12 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
if (success) {
_showToast.emit("Vom Server gelöscht")
_showToast.emit(getString(R.string.snackbar_deleted_from_server))
} else {
_showToast.emit("Server-Löschung fehlgeschlagen")
_showToast.emit(getString(R.string.snackbar_server_delete_failed))
}
} catch (e: Exception) {
_showToast.emit("Server-Fehler: ${e.message}")
_showToast.emit(getString(R.string.snackbar_server_error, e.message ?: ""))
} finally {
// Remove from pending deletions
_pendingDeletions.value = _pendingDeletions.value - noteId
@@ -446,7 +447,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (!isReachable) {
Logger.d(TAG, "⏭️ $source Sync: Server not reachable")
SyncStateManager.markError("Server nicht erreichbar")
SyncStateManager.markError(getString(R.string.snackbar_server_unreachable))
return@launch
}
@@ -456,7 +457,7 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
}
if (result.isSuccess) {
SyncStateManager.markCompleted("${result.syncedCount} Notizen")
SyncStateManager.markCompleted(getString(R.string.toast_sync_success, result.syncedCount))
loadNotes()
} else {
SyncStateManager.markError(result.errorMessage)
@@ -524,8 +525,8 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
if (result.isSuccess && result.syncedCount > 0) {
Logger.d(TAG, "✅ Auto-sync successful ($source): ${result.syncedCount} notes")
SyncStateManager.markCompleted("${result.syncedCount} Notizen")
_showToast.emit("✅ Gesynct: ${result.syncedCount} Notizen")
SyncStateManager.markCompleted(getString(R.string.toast_sync_success, result.syncedCount))
_showToast.emit(getString(R.string.snackbar_synced_count, result.syncedCount))
loadNotes()
} else if (result.isSuccess) {
Logger.d(TAG, " Auto-sync ($source): No changes")
@@ -559,6 +560,11 @@ class MainViewModel(application: Application) : AndroidViewModel(application) {
// 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 {
val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null)
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.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
/**
* Delete confirmation dialog with server/local options
@@ -28,15 +30,15 @@ fun DeleteConfirmationDialog(
onDeleteEverywhere: () -> Unit
) {
val title = if (noteCount == 1) {
"Notiz löschen?"
stringResource(R.string.delete_note_title)
} else {
"$noteCount Notizen löschen?"
stringResource(R.string.delete_notes_title, noteCount)
}
val message = if (noteCount == 1) {
"Wie möchtest du diese Notiz löschen?"
stringResource(R.string.delete_note_message)
} else {
"Wie möchtest du diese $noteCount Notizen löschen?"
stringResource(R.string.delete_notes_message, noteCount)
}
AlertDialog(
@@ -66,7 +68,7 @@ fun DeleteConfirmationDialog(
contentColor = MaterialTheme.colorScheme.error
)
) {
Text("Überall löschen (auch Server)")
Text(stringResource(R.string.delete_everywhere))
}
// Delete local only
@@ -74,7 +76,7 @@ fun DeleteConfirmationDialog(
onClick = onDeleteLocal,
modifier = Modifier.fillMaxWidth()
) {
Text("Nur lokal löschen")
Text(stringResource(R.string.delete_local_only))
}
Spacer(modifier = Modifier.height(8.dp))
@@ -84,7 +86,7 @@ fun DeleteConfirmationDialog(
onClick = onDismiss,
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.dettmer.simplenotes.R
/**
* Empty state card shown when no notes exist
@@ -52,7 +54,7 @@ fun EmptyState(
// Title
Text(
text = "Noch keine Notizen",
text = stringResource(R.string.empty_state_title),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
@@ -62,7 +64,7 @@ fun EmptyState(
// Message
Text(
text = "Tippe + um eine neue Notiz zu erstellen",
text = stringResource(R.string.empty_state_message),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center

View File

@@ -39,8 +39,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.Note
import dev.dettmer.simplenotes.models.NoteType
import dev.dettmer.simplenotes.models.SyncStatus
@@ -66,6 +70,7 @@ fun NoteCard(
onClick: () -> Unit,
onLongClick: () -> Unit
) {
val context = LocalContext.current
val borderColor = if (isSelected) {
MaterialTheme.colorScheme.primary
} else {
@@ -137,7 +142,7 @@ fun NoteCard(
// Title
Text(
text = note.title.ifEmpty { "Ohne Titel" },
text = note.title.ifEmpty { stringResource(R.string.untitled) },
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 2,
@@ -154,7 +159,7 @@ fun NoteCard(
NoteType.TEXT -> note.content.take(100)
NoteType.CHECKLIST -> {
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,
@@ -171,7 +176,7 @@ fun NoteCard(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = note.updatedAt.toReadableTime(),
text = note.updatedAt.toReadableTime(context),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.outline,
modifier = Modifier.weight(1f)
@@ -231,7 +236,7 @@ fun NoteCard(
if (isSelected) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Ausgewählt",
contentDescription = stringResource(R.string.selection_count, 1),
tint = MaterialTheme.colorScheme.onPrimary,
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.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.models.NoteType
/**
@@ -42,7 +44,7 @@ fun NoteTypeFAB(
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Neue Notiz"
contentDescription = stringResource(R.string.fab_new_note)
)
// Dropdown inside FAB - renders as popup overlay
@@ -51,7 +53,7 @@ fun NoteTypeFAB(
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = { Text("Text-Notiz") },
text = { Text(stringResource(R.string.fab_text_note)) },
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Description,
@@ -65,7 +67,7 @@ fun NoteTypeFAB(
}
)
DropdownMenuItem(
text = { Text("Checkliste") },
text = { Text(stringResource(R.string.fab_checklist)) },
leadingIcon = {
Icon(
imageVector = Icons.AutoMirrored.Outlined.List,

View File

@@ -16,7 +16,9 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.sync.SyncStateManager
/**
@@ -60,10 +62,10 @@ fun SyncStatusBanner(
Text(
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.COMPLETED -> message ?: "Synchronisiert"
SyncStateManager.SyncState.ERROR -> message ?: "Fehler"
SyncStateManager.SyncState.COMPLETED -> message ?: stringResource(R.string.sync_status_completed)
SyncStateManager.SyncState.ERROR -> message ?: stringResource(R.string.sync_status_error)
SyncStateManager.SyncState.IDLE -> ""
},
style = MaterialTheme.typography.bodyMedium,

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ import android.net.Uri
import android.util.Log
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.backup.BackupManager
import dev.dettmer.simplenotes.backup.RestoreMode
import dev.dettmer.simplenotes.sync.WebDavSyncService
@@ -184,10 +185,10 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
} else {
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) {
_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 {
_isSyncing.value = true
try {
emitToast("🔄 Synchronisiere...")
emitToast(getString(R.string.toast_syncing))
val syncService = WebDavSyncService(getApplication())
if (!syncService.hasUnsyncedChanges()) {
emitToast("✅ Bereits synchronisiert")
emitToast(getString(R.string.toast_already_synced))
return@launch
}
val result = syncService.syncNotes()
if (result.isSuccess) {
emitToast("${result.syncedCount} Notizen synchronisiert")
emitToast(getString(R.string.toast_sync_success, result.syncedCount))
} else {
emitToast("${result.errorMessage}")
emitToast(getString(R.string.toast_sync_failed, result.errorMessage ?: ""))
}
} catch (e: Exception) {
emitToast("❌ Fehler: ${e.message}")
emitToast(getString(R.string.toast_error, e.message ?: ""))
} finally {
_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
_events.emit(SettingsEvent.RequestBatteryOptimization)
_events.emit(SettingsEvent.RestartNetworkMonitor)
emitToast("✅ Auto-Sync aktiviert")
emitToast(getString(R.string.toast_auto_sync_enabled))
} else {
_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()
viewModelScope.launch {
val text = when (minutes) {
15L -> "15 Minuten"
60L -> "60 Minuten"
else -> "30 Minuten"
15L -> getString(R.string.toast_sync_interval_15min)
60L -> getString(R.string.toast_sync_interval_60min)
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, "") ?: ""
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
return@launch
}
@@ -329,7 +330,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
.apply()
_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
kotlinx.coroutines.delay(500)
@@ -342,12 +343,12 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
.putBoolean(Constants.KEY_MARKDOWN_EXPORT, true)
.putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, true)
.apply()
emitToast("📝 Markdown Auto-Sync aktiviert")
emitToast(getString(R.string.toast_markdown_enabled))
}
} catch (e: Exception) {
_markdownExportProgress.value = null
emitToast("❌ Export fehlgeschlagen: ${e.message}")
emitToast(getString(R.string.toast_export_failed, e.message ?: ""))
// Don't enable on error
}
}
@@ -359,7 +360,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
.putBoolean(Constants.KEY_MARKDOWN_AUTO_IMPORT, false)
.apply()
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() {
viewModelScope.launch {
try {
emitToast("📝 Markdown-Sync läuft...")
emitToast(getString(R.string.toast_markdown_syncing))
val syncService = WebDavSyncService(getApplication())
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) {
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
try {
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) {
emitToast("❌ Backup fehlgeschlagen: ${e.message}")
emitToast(getString(R.string.toast_backup_failed, e.message ?: ""))
} finally {
_isBackupInProgress.value = false
}
@@ -400,9 +401,9 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
_isBackupInProgress.value = true
try {
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) {
emitToast("❌ Wiederherstellung fehlgeschlagen: ${e.message}")
emitToast(getString(R.string.toast_restore_failed, e.message ?: ""))
} finally {
_isBackupInProgress.value = false
}
@@ -413,14 +414,14 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
viewModelScope.launch {
_isBackupInProgress.value = true
try {
emitToast("📥 Lade vom Server...")
emitToast(getString(R.string.restore_progress))
val syncService = WebDavSyncService(getApplication())
val result = withContext(Dispatchers.IO) {
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) {
emitToast("❌ Fehler: ${e.message}")
emitToast(getString(R.string.toast_error, e.message ?: ""))
} finally {
_isBackupInProgress.value = false
}
@@ -436,7 +437,7 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
prefs.edit().putBoolean(Constants.KEY_FILE_LOGGING_ENABLED, enabled).apply()
Logger.setFileLoggingEnabled(enabled)
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 {
try {
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) {
emitToast("❌ Fehler: ${e.message}")
emitToast(getString(R.string.toast_error, e.message ?: ""))
}
}
}
@@ -457,6 +458,11 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
// 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) {
_showToast.emit(message)
}

View File

@@ -13,6 +13,8 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import dev.dettmer.simplenotes.R
/**
* Reusable Scaffold with back-navigation TopAppBar
@@ -40,7 +42,7 @@ fun SettingsScaffold(
IconButton(onClick = onBack) {
Icon(
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.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
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.SettingsScaffold
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"
SettingsScaffold(
title = "Über diese App",
title = stringResource(R.string.about_settings_title),
onBack = onBack
) { paddingValues ->
Column(
@@ -110,7 +112,7 @@ fun AboutScreen(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Simple Notes Sync",
text = stringResource(R.string.about_app_name),
style = MaterialTheme.typography.headlineSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
@@ -118,7 +120,7 @@ fun AboutScreen(
Spacer(modifier = Modifier.height(4.dp))
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,
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.8f)
)
@@ -127,13 +129,13 @@ fun AboutScreen(
Spacer(modifier = Modifier.height(24.dp))
SettingsSectionHeader(text = "Links")
SettingsSectionHeader(text = stringResource(R.string.about_links_section))
// GitHub Repository
AboutLinkItem(
icon = Icons.Default.Code,
title = "GitHub Repository",
subtitle = "Quellcode, Issues & Dokumentation",
title = stringResource(R.string.about_github_title),
subtitle = stringResource(R.string.about_github_subtitle),
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubRepoUrl))
context.startActivity(intent)
@@ -143,8 +145,8 @@ fun AboutScreen(
// Developer
AboutLinkItem(
icon = Icons.Default.Person,
title = "Entwickler",
subtitle = "GitHub Profil: @inventory69",
title = stringResource(R.string.about_developer_title),
subtitle = stringResource(R.string.about_developer_subtitle),
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(githubProfileUrl))
context.startActivity(intent)
@@ -154,8 +156,8 @@ fun AboutScreen(
// License
AboutLinkItem(
icon = Icons.Default.Policy,
title = "Lizenz",
subtitle = "MIT License - Open Source",
title = stringResource(R.string.about_license_title),
subtitle = stringResource(R.string.about_license_subtitle),
onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(licenseUrl))
context.startActivity(intent)
@@ -177,14 +179,12 @@ fun AboutScreen(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "🔒 Datenschutz",
text = stringResource(R.string.about_privacy_title),
style = MaterialTheme.typography.titleSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
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.",
text = stringResource(R.string.about_privacy_text),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)

View File

@@ -21,7 +21,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.backup.RestoreMode
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.RadioOption
@@ -71,7 +73,7 @@ fun BackupSettingsScreen(
}
SettingsScaffold(
title = "Backup & Wiederherstellung",
title = stringResource(R.string.backup_settings_title),
onBack = onBack
) { paddingValues ->
Column(
@@ -84,19 +86,18 @@ fun BackupSettingsScreen(
// Info Card
SettingsInfoCard(
text = "📦 Bei jeder Wiederherstellung wird automatisch ein " +
"Sicherheits-Backup erstellt."
text = stringResource(R.string.backup_auto_info)
)
Spacer(modifier = Modifier.height(16.dp))
// Local Backup Section
SettingsSectionHeader(text = "Lokales Backup")
SettingsSectionHeader(text = stringResource(R.string.backup_local_section))
Spacer(modifier = Modifier.height(8.dp))
SettingsButton(
text = "💾 Backup erstellen",
text = stringResource(R.string.backup_create),
onClick = {
val timestamp = SimpleDateFormat("yyyy-MM-dd_HHmmss", Locale.US)
.format(Date())
@@ -110,7 +111,7 @@ fun BackupSettingsScreen(
Spacer(modifier = Modifier.height(8.dp))
SettingsOutlinedButton(
text = "📂 Aus Datei wiederherstellen",
text = stringResource(R.string.backup_restore_file),
onClick = {
restoreFileLauncher.launch(arrayOf("application/json"))
},
@@ -121,12 +122,12 @@ fun BackupSettingsScreen(
SettingsDivider()
// Server Backup Section
SettingsSectionHeader(text = "Server-Backup")
SettingsSectionHeader(text = stringResource(R.string.backup_server_section))
Spacer(modifier = Modifier.height(8.dp))
SettingsOutlinedButton(
text = "☁️ Vom Server wiederherstellen",
text = stringResource(R.string.backup_restore_server),
onClick = {
restoreSource = RestoreSource.Server
showRestoreDialog = true
@@ -186,42 +187,42 @@ private fun RestoreModeDialog(
onDismiss: () -> Unit
) {
val sourceText = when (source) {
RestoreSource.LocalFile -> "Lokale Datei"
RestoreSource.Server -> "WebDAV Server"
RestoreSource.LocalFile -> stringResource(R.string.backup_restore_source_file)
RestoreSource.Server -> stringResource(R.string.backup_restore_source_server)
}
val modeOptions = listOf(
RadioOption(
value = RestoreMode.MERGE,
title = "⚪ Zusammenführen (Standard)",
subtitle = "Neue hinzufügen, Bestehende behalten"
title = stringResource(R.string.backup_mode_merge_title),
subtitle = stringResource(R.string.backup_mode_merge_subtitle)
),
RadioOption(
value = RestoreMode.REPLACE,
title = "⚪ Ersetzen",
subtitle = "Alle löschen & Backup importieren"
title = stringResource(R.string.backup_mode_replace_title),
subtitle = stringResource(R.string.backup_mode_replace_subtitle)
),
RadioOption(
value = RestoreMode.OVERWRITE_DUPLICATES,
title = "⚪ Duplikate überschreiben",
subtitle = "Backup gewinnt bei Konflikten"
title = stringResource(R.string.backup_mode_overwrite_title),
subtitle = stringResource(R.string.backup_mode_overwrite_subtitle)
)
)
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("⚠️ Backup wiederherstellen?") },
title = { Text(stringResource(R.string.backup_restore_dialog_title)) },
text = {
Column {
Text(
text = "Quelle: $sourceText",
text = stringResource(R.string.backup_restore_source, sourceText),
style = MaterialTheme.typography.bodyMedium
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Wiederherstellungs-Modus:",
text = stringResource(R.string.backup_restore_mode_label),
style = MaterialTheme.typography.labelLarge
)
@@ -236,7 +237,7 @@ private fun RestoreModeDialog(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = " Ein Sicherheits-Backup wird vor dem Wiederherstellen automatisch erstellt.",
text = stringResource(R.string.backup_restore_info),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
@@ -244,12 +245,12 @@ private fun RestoreModeDialog(
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text("Wiederherstellen")
Text(stringResource(R.string.backup_restore_button))
}
},
dismissButton = {
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.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.content.FileProvider
import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
import dev.dettmer.simplenotes.ui.settings.components.SettingsDangerButton
@@ -48,7 +50,7 @@ fun DebugSettingsScreen(
var showClearLogsDialog by remember { mutableStateOf(false) }
SettingsScaffold(
title = "Debug & Diagnose",
title = stringResource(R.string.debug_settings_title),
onBack = onBack
) { paddingValues ->
Column(
@@ -61,8 +63,8 @@ fun DebugSettingsScreen(
// File Logging Toggle
SettingsSwitch(
title = "Datei-Logging",
subtitle = "Sync-Logs in Datei speichern",
title = stringResource(R.string.debug_file_logging_title),
subtitle = stringResource(R.string.debug_file_logging_subtitle),
checked = fileLoggingEnabled,
onCheckedChange = { viewModel.setFileLogging(it) },
icon = Icons.AutoMirrored.Filled.Notes
@@ -70,21 +72,18 @@ fun DebugSettingsScreen(
// Privacy Info
SettingsInfoCard(
text = "🔒 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."
text = stringResource(R.string.debug_privacy_info)
)
SettingsDivider()
SettingsSectionHeader(text = "Log-Aktionen")
SettingsSectionHeader(text = stringResource(R.string.debug_log_actions_section))
Spacer(modifier = Modifier.height(8.dp))
// Export Logs Button
SettingsButton(
text = "📤 Logs exportieren & teilen",
text = stringResource(R.string.debug_export_logs),
onClick = {
val logFile = viewModel.getLogFile()
if (logFile != null && logFile.exists() && logFile.length() > 0L) {
@@ -97,11 +96,11 @@ fun DebugSettingsScreen(
val shareIntent = Intent(Intent.ACTION_SEND).apply {
type = "text/plain"
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)
}
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)
@@ -111,7 +110,7 @@ fun DebugSettingsScreen(
// Clear Logs Button
SettingsDangerButton(
text = "🗑️ Logs löschen",
text = stringResource(R.string.debug_delete_logs),
onClick = { showClearLogsDialog = true },
modifier = Modifier.padding(horizontal = 16.dp)
)
@@ -124,9 +123,9 @@ fun DebugSettingsScreen(
if (showClearLogsDialog) {
AlertDialog(
onDismissRequest = { showClearLogsDialog = false },
title = { Text("Logs löschen?") },
title = { Text(stringResource(R.string.debug_delete_logs_title)) },
text = {
Text("Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht.")
Text(stringResource(R.string.debug_delete_logs_message))
},
confirmButton = {
TextButton(
@@ -135,12 +134,12 @@ fun DebugSettingsScreen(
viewModel.clearLogs()
}
) {
Text("Löschen")
Text(stringResource(R.string.delete))
}
},
dismissButton = {
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.SettingsButton
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
@@ -44,7 +46,7 @@ fun MarkdownSettingsScreen(
exportProgress?.let { progress ->
AlertDialog(
onDismissRequest = { /* Not dismissable */ },
title = { Text("Markdown Auto-Sync") },
title = { Text(stringResource(R.string.markdown_dialog_title)) },
text = {
Column(
modifier = Modifier.fillMaxWidth(),
@@ -53,9 +55,9 @@ fun MarkdownSettingsScreen(
) {
Text(
text = if (progress.isComplete) {
"✅ Export abgeschlossen"
stringResource(R.string.markdown_export_complete)
} else {
"Exportiere ${progress.current}/${progress.total} Notizen..."
stringResource(R.string.markdown_export_progress, progress.current, progress.total)
},
style = MaterialTheme.typography.bodyMedium
)
@@ -75,7 +77,7 @@ fun MarkdownSettingsScreen(
}
SettingsScaffold(
title = "Markdown Desktop-Integration",
title = stringResource(R.string.markdown_settings_title),
onBack = onBack
) { paddingValues ->
Column(
@@ -88,17 +90,15 @@ fun MarkdownSettingsScreen(
// Info Card
SettingsInfoCard(
text = "📝 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."
text = stringResource(R.string.markdown_info)
)
Spacer(modifier = Modifier.height(8.dp))
// Markdown Auto-Sync Toggle
SettingsSwitch(
title = "Markdown Auto-Sync",
subtitle = "Synchronisiert Notizen automatisch als .md-Dateien (Upload + Download bei jedem Sync)",
title = stringResource(R.string.markdown_auto_sync_title),
subtitle = stringResource(R.string.markdown_auto_sync_subtitle),
checked = markdownAutoSync,
onCheckedChange = { viewModel.setMarkdownAutoSync(it) },
icon = Icons.Default.Description
@@ -109,14 +109,13 @@ fun MarkdownSettingsScreen(
SettingsDivider()
SettingsInfoCard(
text = "Manueller Sync exportiert alle Notizen als .md-Dateien und " +
"importiert .md-Dateien vom Server. Nützlich für einmalige Synchronisation."
text = stringResource(R.string.markdown_manual_sync_info)
)
Spacer(modifier = Modifier.height(8.dp))
SettingsButton(
text = "📝 Manueller Markdown-Sync",
text = stringResource(R.string.markdown_manual_sync_button),
onClick = { viewModel.performManualMarkdownSync() },
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.graphics.Color
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.VisualTransformation
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.SettingsScaffold
@@ -71,7 +73,7 @@ fun ServerSettingsScreen(
}
SettingsScaffold(
title = "Server-Einstellungen",
title = stringResource(R.string.server_settings_title),
onBack = onBack
) { paddingValues ->
Column(
@@ -83,7 +85,7 @@ fun ServerSettingsScreen(
) {
// Verbindungstyp
Text(
text = "Verbindungstyp",
text = stringResource(R.string.server_connection_type),
style = MaterialTheme.typography.labelLarge,
modifier = Modifier.padding(bottom = 8.dp)
)
@@ -95,22 +97,22 @@ fun ServerSettingsScreen(
FilterChip(
selected = !isHttps,
onClick = { viewModel.updateProtocol(false) },
label = { Text("🏠 Intern (HTTP)") },
label = { Text(stringResource(R.string.server_connection_http)) },
modifier = Modifier.weight(1f)
)
FilterChip(
selected = isHttps,
onClick = { viewModel.updateProtocol(true) },
label = { Text("🌐 Extern (HTTPS)") },
label = { Text(stringResource(R.string.server_connection_https)) },
modifier = Modifier.weight(1f)
)
}
Text(
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 {
"HTTPS für sichere Verbindungen über das Internet"
stringResource(R.string.server_connection_https_hint)
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -121,8 +123,8 @@ fun ServerSettingsScreen(
OutlinedTextField(
value = serverUrl,
onValueChange = { viewModel.updateServerUrl(it) },
label = { Text("Server-Adresse") },
supportingText = { Text("z.B. http://192.168.0.188:8080/notes") },
label = { Text(stringResource(R.string.server_address)) },
supportingText = { Text(stringResource(R.string.server_address_hint)) },
leadingIcon = { Icon(Icons.Default.Language, null) },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
@@ -135,7 +137,7 @@ fun ServerSettingsScreen(
OutlinedTextField(
value = username,
onValueChange = { viewModel.updateUsername(it) },
label = { Text("Benutzername") },
label = { Text(stringResource(R.string.username)) },
leadingIcon = { Icon(Icons.Default.Person, null) },
modifier = Modifier.fillMaxWidth(),
singleLine = true
@@ -147,7 +149,7 @@ fun ServerSettingsScreen(
OutlinedTextField(
value = password,
onValueChange = { viewModel.updatePassword(it) },
label = { Text("Passwort") },
label = { Text(stringResource(R.string.password)) },
leadingIcon = { Icon(Icons.Default.Lock, null) },
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
@@ -157,7 +159,7 @@ fun ServerSettingsScreen(
} else {
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,
verticalAlignment = Alignment.CenterVertically
) {
Text("Server-Status:", style = MaterialTheme.typography.labelLarge)
Text(stringResource(R.string.server_status_label), style = MaterialTheme.typography.labelLarge)
Text(
text = when (serverStatus) {
is SettingsViewModel.ServerStatus.Reachable -> "✅ Erreichbar"
is SettingsViewModel.ServerStatus.Unreachable -> "❌ Nicht erreichbar"
is SettingsViewModel.ServerStatus.Checking -> "🔍 Prüfe..."
is SettingsViewModel.ServerStatus.NotConfigured -> "⚠️ Nicht konfiguriert"
else -> "❓ Unbekannt"
is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.server_status_reachable)
is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.server_status_unreachable)
is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.server_status_checking)
is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.server_status_not_configured)
else -> stringResource(R.string.server_status_unknown)
},
color = when (serverStatus) {
is SettingsViewModel.ServerStatus.Reachable -> Color(0xFF4CAF50)
@@ -217,7 +219,7 @@ fun ServerSettingsScreen(
onClick = { viewModel.testConnection() },
modifier = Modifier.weight(1f)
) {
Text("Verbindung testen")
Text(stringResource(R.string.test_connection))
}
Button(
@@ -233,7 +235,7 @@ fun ServerSettingsScreen(
)
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
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
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.Description
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Sync
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -17,8 +19,10 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.BuildConfig
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsRoute
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.SettingsCard
@@ -46,8 +50,18 @@ fun SettingsMainScreen(
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(
title = "Einstellungen",
title = stringResource(R.string.settings_title),
onBack = onBack
) { paddingValues ->
LazyColumn(
@@ -56,6 +70,16 @@ fun SettingsMainScreen(
.padding(paddingValues),
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
item {
// v1.5.0 Fix: Nur Prefix-URLs gelten als "nicht konfiguriert"
@@ -65,13 +89,13 @@ fun SettingsMainScreen(
SettingsCard(
icon = Icons.Default.Cloud,
title = "Server-Einstellungen",
title = stringResource(R.string.settings_server),
subtitle = if (isConfigured) serverUrl else null,
statusText = when (serverStatus) {
is SettingsViewModel.ServerStatus.Reachable -> "✅ Erreichbar"
is SettingsViewModel.ServerStatus.Unreachable -> "❌ Nicht erreichbar"
is SettingsViewModel.ServerStatus.Checking -> "🔍 Prüfe..."
is SettingsViewModel.ServerStatus.NotConfigured -> "⚠️ Nicht konfiguriert"
is SettingsViewModel.ServerStatus.Reachable -> stringResource(R.string.settings_server_status_reachable)
is SettingsViewModel.ServerStatus.Unreachable -> stringResource(R.string.settings_server_status_unreachable)
is SettingsViewModel.ServerStatus.Checking -> stringResource(R.string.settings_server_status_checking)
is SettingsViewModel.ServerStatus.NotConfigured -> stringResource(R.string.settings_server_status_not_configured)
else -> null
},
statusColor = when (serverStatus) {
@@ -87,14 +111,14 @@ fun SettingsMainScreen(
// Sync-Einstellungen
item {
val intervalText = when (syncInterval) {
15L -> "15 Min"
60L -> "60 Min"
else -> "30 Min"
15L -> stringResource(R.string.settings_interval_15min)
60L -> stringResource(R.string.settings_interval_60min)
else -> stringResource(R.string.settings_interval_30min)
}
SettingsCard(
icon = Icons.Default.Sync,
title = "Sync-Einstellungen",
subtitle = if (autoSyncEnabled) "Auto-Sync: An • $intervalText" else "Auto-Sync: Aus",
title = stringResource(R.string.settings_sync),
subtitle = if (autoSyncEnabled) stringResource(R.string.settings_sync_auto_on, intervalText) else stringResource(R.string.settings_sync_auto_off),
onClick = { onNavigate(SettingsRoute.Sync) }
)
}
@@ -103,8 +127,8 @@ fun SettingsMainScreen(
item {
SettingsCard(
icon = Icons.Default.Description,
title = "Markdown Desktop-Integration",
subtitle = if (markdownAutoSync) "Auto-Sync: An" else "Auto-Sync: Aus",
title = stringResource(R.string.settings_markdown),
subtitle = if (markdownAutoSync) stringResource(R.string.settings_markdown_auto_on) else stringResource(R.string.settings_markdown_auto_off),
onClick = { onNavigate(SettingsRoute.Markdown) }
)
}
@@ -113,8 +137,8 @@ fun SettingsMainScreen(
item {
SettingsCard(
icon = Icons.Default.Backup,
title = "Backup & Wiederherstellung",
subtitle = "Lokales oder Server-Backup",
title = stringResource(R.string.settings_backup),
subtitle = stringResource(R.string.settings_backup_subtitle),
onClick = { onNavigate(SettingsRoute.Backup) }
)
}
@@ -123,8 +147,8 @@ fun SettingsMainScreen(
item {
SettingsCard(
icon = Icons.Default.Info,
title = "Über diese App",
subtitle = "Version ${BuildConfig.VERSION_NAME}",
title = stringResource(R.string.settings_about),
subtitle = stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
onClick = { onNavigate(SettingsRoute.About) }
)
}
@@ -133,8 +157,8 @@ fun SettingsMainScreen(
item {
SettingsCard(
icon = Icons.Default.BugReport,
title = "Debug & Diagnose",
subtitle = if (fileLoggingEnabled) "Logging: An" else "Logging: Aus",
title = stringResource(R.string.settings_debug),
subtitle = if (fileLoggingEnabled) stringResource(R.string.settings_debug_logging_on) else stringResource(R.string.settings_debug_logging_off),
onClick = { onNavigate(SettingsRoute.Debug) }
)
}

View File

@@ -13,7 +13,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.dettmer.simplenotes.R
import dev.dettmer.simplenotes.ui.settings.SettingsViewModel
import dev.dettmer.simplenotes.ui.settings.components.RadioOption
import dev.dettmer.simplenotes.ui.settings.components.SettingsDivider
@@ -36,7 +38,7 @@ fun SyncSettingsScreen(
val syncInterval by viewModel.syncInterval.collectAsState()
SettingsScaffold(
title = "Sync-Einstellungen",
title = stringResource(R.string.sync_settings_title),
onBack = onBack
) { paddingValues ->
Column(
@@ -49,18 +51,14 @@ fun SyncSettingsScreen(
// Auto-Sync Info
SettingsInfoCard(
text = "🔄 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)"
text = stringResource(R.string.sync_auto_sync_info)
)
Spacer(modifier = Modifier.height(8.dp))
// Auto-Sync Toggle
SettingsSwitch(
title = "Auto-Sync aktiviert",
title = stringResource(R.string.sync_auto_sync_enabled),
checked = autoSyncEnabled,
onCheckedChange = { viewModel.setAutoSync(it) },
icon = Icons.Default.Sync
@@ -69,14 +67,10 @@ fun SyncSettingsScreen(
SettingsDivider()
// Sync Interval Section
SettingsSectionHeader(text = "Sync-Intervall")
SettingsSectionHeader(text = stringResource(R.string.sync_interval_section))
SettingsInfoCard(
text = "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."
text = stringResource(R.string.sync_interval_info)
)
Spacer(modifier = Modifier.height(8.dp))
@@ -85,18 +79,18 @@ fun SyncSettingsScreen(
val intervalOptions = listOf(
RadioOption(
value = 15L,
title = "⚡ Alle 15 Minuten",
subtitle = "Schnellste Synchronisation • ~0.8% Akku/Tag (~23 mAh)"
title = stringResource(R.string.sync_interval_15min_title),
subtitle = stringResource(R.string.sync_interval_15min_subtitle)
),
RadioOption(
value = 30L,
title = "✓ Alle 30 Minuten (Empfohlen)",
subtitle = "Ausgewogenes Verhältnis • ~0.4% Akku/Tag (~12 mAh)"
title = stringResource(R.string.sync_interval_30min_title),
subtitle = stringResource(R.string.sync_interval_30min_subtitle)
),
RadioOption(
value = 60L,
title = "🔋 Alle 60 Minuten",
subtitle = "Maximale Akkulaufzeit • ~0.2% Akku/Tag (~6 mAh geschätzt)"
title = stringResource(R.string.sync_interval_60min_title),
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.widget.Toast
import dev.dettmer.simplenotes.R
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -15,7 +16,7 @@ fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}
// Timestamp to readable format
// Timestamp to readable format (legacy - without context, uses German)
fun Long.toReadableTime(): String {
val now = System.currentTimeMillis()
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
fun String.truncate(maxLength: Int): String {
return if (length > maxLength) {

View File

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

View File

@@ -1,5 +1,7 @@
package dev.dettmer.simplenotes.utils
import android.content.Context
import dev.dettmer.simplenotes.R
import java.net.URL
/**
@@ -91,7 +93,7 @@ object UrlValidator {
* Validiert ob HTTP URL erlaubt ist
* @return Pair<Boolean, String?> - (isValid, errorMessage)
*/
fun validateHttpUrl(url: String): Pair<Boolean, String?> {
fun validateHttpUrl(context: Context, url: String): Pair<Boolean, String?> {
return try {
val parsedUrl = URL(url)
@@ -107,16 +109,15 @@ object UrlValidator {
} else {
return Pair(
false,
"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."
context.getString(R.string.error_http_local_only)
)
}
}
// 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) {
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>
<!-- ============================= -->
<!-- APP IDENTITY -->
<!-- ============================= -->
<string name="app_name">Simple Notes</string>
<!-- Main Activity -->
<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>
<!-- ============================= -->
<!-- MAIN SCREEN -->
<!-- ============================= -->
<string name="main_title">Simple Notes</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_title">Noch keine Notizen</string>
<string name="empty_state_message">Tippe auf um deine erste Notiz zu erstellen</string>
<string name="empty_state_title">No notes yet</string>
<string name="empty_state_message">Tap + to create a new note</string>
<!-- Note Editor -->
<string name="edit_note">Notiz bearbeiten</string>
<string name="new_note">Neue Notiz</string>
<string name="title">Titel</string>
<string name="content">Inhalt</string>
<string name="save">Speichern</string>
<string name="delete">Löschen</string>
<string name="back">Zurück</string>
<!-- ============================= -->
<!-- FAB MENU -->
<!-- ============================= -->
<string name="fab_new_note">New note</string>
<string name="fab_text_note">Text note</string>
<string name="fab_checklist">Checklist</string>
<string name="create_text_note">Note</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_content_placeholder">Note content preview…</string>
<string name="note_timestamp_placeholder">Vor 2 Std</string>
<string name="untitled">Ohne Titel</string>
<string name="note_timestamp_placeholder">2 hours ago</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>
<string name="delete_note_message">Diese Aktion kann nicht rückgängig gemacht werden.</string>
<string name="cancel">Abbrechen</string>
<!-- ============================= -->
<!-- SYNC STATUS BANNER -->
<!-- ============================= -->
<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="username">Benutzername</string>
<string name="password">Passwort</string>
<string name="server_status_label">Server-Status:</string>
<string name="server_status_checking">Prüfe…</string>
<string name="test_connection">Verbindung testen</string>
<string name="sync_now">Jetzt synchronisieren</string>
<string name="username">Username</string>
<string name="password">Password</string>
<string name="server_password_show">Show</string>
<string name="server_password_hide">Hide</string>
<string name="server_status_label">Server Status:</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>
<string name="auto_sync">Auto-Sync aktiviert</string>
<string name="sync_status">Sync-Status</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>
<!-- ============================= -->
<!-- SETTINGS - SYNC -->
<!-- ============================= -->
<string name="sync_settings">Sync Settings</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>
<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 - 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 complete</string>
<string name="markdown_export_progress">Exporting %1$d/%2$d notes…</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="markdown_auto_sync_title">Markdown Auto-Sync</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>
<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>
<!-- ============================= -->
<!-- SETTINGS - BACKUP -->
<!-- ============================= -->
<string name="backup_settings_title">Backup &amp; Restore</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>
<string name="create_checklist">Liste</string>
<!-- ============================= -->
<!-- SETTINGS - ABOUT -->
<!-- ============================= -->
<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>
<string name="edit_checklist">Liste bearbeiten</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>
<!-- ============================= -->
<!-- TOAST MESSAGES -->
<!-- ============================= -->
<string name="toast_connection_success">✅ Connection successful!</string>
<string name="toast_connection_failed">❌ %s</string>
<string name="toast_error">❌ Error: %s</string>
<string name="toast_syncing">🔄 Syncing…</string>
<string name="toast_already_synced">✅ Already synced</string>
<string name="toast_sync_success">✅ %d notes synced</string>
<string name="toast_sync_failed">❌ %s</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>
<string name="empty_checklist">Keine Einträge</string>
<!-- ============================= -->
<!-- RELATIVE TIME -->
<!-- ============================= -->
<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>

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>