diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ea90fd7..bc7094a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -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" diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt index 928f39a..46139a0 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/MainActivity.kt @@ -467,10 +467,10 @@ class MainActivity : AppCompatActivity() { val checkboxAlways = dialogView.findViewById(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)) } } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt index a4c926f..8306374 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/SettingsActivity.kt @@ -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() } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/backup/BackupManager.kt b/android/app/src/main/java/dev/dettmer/simplenotes/backup/BackupManager.kt index e741261..e6db9d5 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/backup/BackupManager.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/backup/BackupManager.kt @@ -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,10 +262,10 @@ 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) ) } - + /** * Restore-Modus: OVERWRITE_DUPLICATES * Backup überschreibt bei ID-Konflikten @@ -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) ) } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt index 0c3caa7..580a573 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/sync/WebDavSyncService.kt @@ -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") diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt index 21dcb2c..5af51b1 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/ComposeMainActivity.kt @@ -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)) } } ) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt index ed35821..44fc3da 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainScreen.kt @@ -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 { diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt index 0921d92..4b47c66 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/MainViewModel.kt @@ -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().getString(resId) + + private fun getString(resId: Int, vararg formatArgs: Any): String = + getApplication().getString(resId, *formatArgs) + fun isServerConfigured(): Boolean { val serverUrl = prefs.getString(Constants.KEY_SERVER_URL, null) return !serverUrl.isNullOrEmpty() && serverUrl != "http://" && serverUrl != "https://" diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/DeleteConfirmationDialog.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/DeleteConfirmationDialog.kt index 5ea14dc..443f037 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/DeleteConfirmationDialog.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/DeleteConfirmationDialog.kt @@ -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)) } } }, diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/EmptyState.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/EmptyState.kt index d959c0e..929d412 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/EmptyState.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/EmptyState.kt @@ -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 diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt index e48cf77..9a9ff8b 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteCard.kt @@ -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) ) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteTypeFAB.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteTypeFAB.kt index 23b65d4..f49c57c 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteTypeFAB.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/NoteTypeFAB.kt @@ -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, diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt index 2436b27..ac9ba26 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/main/components/SyncStatusBanner.kt @@ -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, diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/ComposeSettingsActivity.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/ComposeSettingsActivity.kt index 1569fac..ec9ddbd 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/ComposeSettingsActivity.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/ComposeSettingsActivity.kt @@ -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) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt index 2b8f5d9..18c1c0e 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsNavigation.kt @@ -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( diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt index 3ce1c25..ffd3a23 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsRoute.kt @@ -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") diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt index 533240c..2a55f6f 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/SettingsViewModel.kt @@ -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().getString(resId) + + private fun getString(resId: Int, vararg formatArgs: Any): String = + getApplication().getString(resId, *formatArgs) + private suspend fun emitToast(message: String) { _showToast.emit(message) } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt index a08b383..457b423 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/components/SettingsScaffold.kt @@ -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) ) } }, diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt index ad742a2..1d3c98b 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/AboutScreen.kt @@ -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 ) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt index e3ca5c2..a93bc19 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/BackupSettingsScreen.kt @@ -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)) } } ) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt index c68593c..f3bd74b 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/DebugSettingsScreen.kt @@ -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)) } } ) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/LanguageSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/LanguageSettingsScreen.kt new file mode 100644 index 0000000..f47d9f2 --- /dev/null +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/LanguageSettingsScreen.kt @@ -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() +} diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt index 0b245af..d62f1b7 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/MarkdownSettingsScreen.kt @@ -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) ) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt index 6927805..b7ae5c6 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/ServerSettingsScreen.kt @@ -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)) } } } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt index 25c256f..dd696be 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SettingsMainScreen.kt @@ -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) } ) } diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt index 7452bae..b2196a8 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/ui/settings/screens/SyncSettingsScreen.kt @@ -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) ) ) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Extensions.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Extensions.kt index 84139b9..4bb7212 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/Extensions.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/Extensions.kt @@ -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) { diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt index 37eee56..b3ab6e5 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/NotificationHelper.kt @@ -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) diff --git a/android/app/src/main/java/dev/dettmer/simplenotes/utils/UrlValidator.kt b/android/app/src/main/java/dev/dettmer/simplenotes/utils/UrlValidator.kt index 4c7410b..57fa06a 100644 --- a/android/app/src/main/java/dev/dettmer/simplenotes/utils/UrlValidator.kt +++ b/android/app/src/main/java/dev/dettmer/simplenotes/utils/UrlValidator.kt @@ -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 - (isValid, errorMessage) */ - fun validateHttpUrl(url: String): Pair { + fun validateHttpUrl(context: Context, url: String): Pair { 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 ?: "")) } } } diff --git a/android/app/src/main/res/values-de/strings.xml b/android/app/src/main/res/values-de/strings.xml new file mode 100644 index 0000000..f5e5e8d --- /dev/null +++ b/android/app/src/main/res/values-de/strings.xml @@ -0,0 +1,393 @@ + + + + + + Simple Notes + + + + + Simple Notes + Noch keine Notizen.\nTippe + um eine zu erstellen. + Notiz hinzufügen + Synchronisieren + Einstellungen + Synchronisieren + Einstellungen + Auswahl beenden + Alle auswählen + Ausgewählte löschen + %d ausgewählt + + + + + Noch keine Notizen + Tippe + um eine neue Notiz zu erstellen + + + + + Neue Notiz + Text-Notiz + Checkliste + Notiz + Liste + + + + + Notiz-Titel + Notiz-Vorschau… + Vor 2 Std + Ohne Titel + %1$d/%2$d erledigt + Keine Einträge + + + + + Sync-Status + Synchronisiere… + Synchronisiert + Fehler + Synchronisiere… + Synchronisierung abgeschlossen + Synchronisierung fehlgeschlagen + Synchronisierung läuft bereits + + + + + Notiz löschen? + %d Notizen löschen? + Wie möchtest du diese Notiz löschen? + Wie möchtest du diese %d Notizen löschen? + Überall löschen (auch Server) + Nur lokal löschen + Löschen + Abbrechen + OK + + Notiz löschen + \"%s\" wird lokal gelöscht.\n\nAuch vom Server löschen? + Vom Server löschen + \"%s\" wird lokal und vom Server gelöscht + \"%s\" lokal gelöscht (Server bleibt) + + + + + RÜCKGÄNGIG + \"%s\" lokal gelöscht + \"%s\" wird vom Server gelöscht + %d Notiz(en) lokal gelöscht + %d Notiz(en) werden vom Server gelöscht + Vom Server gelöscht + Server-Löschung fehlgeschlagen + Server-Fehler: %s + Bereits synchronisiert + Server nicht erreichbar + ✅ Gesynct: %d Notizen + + + + + 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. + Ungültiges Protokoll: %s. Bitte verwende HTTP oder HTTPS. + Ungültige URL: %s + WebDAV-Server nicht vollständig konfiguriert + Sardine Client konnte nicht erstellt werden + Server-URL nicht konfiguriert + + + + + Neue Notiz + Notiz bearbeiten + Neue Liste + Liste bearbeiten + Titel + Inhalt + Zurück + Speichern + Element hinzufügen + Neues Element… + Element verschieben + Ziehen zum Sortieren + Element löschen + Notiz ist leer + Notiz gespeichert + Notiz gelöscht + + + + + Einstellungen + Sprache + %s + Server-Einstellungen + ✅ Erreichbar + ❌ Nicht erreichbar + 🔍 Prüfe… + ⚠️ Nicht konfiguriert + Sync-Einstellungen + Auto-Sync: An • %s + Auto-Sync: Aus + 15 Min + 30 Min + 60 Min + Markdown Desktop-Integration + Auto-Sync: An + Auto-Sync: Aus + Backup & Wiederherstellung + Lokales oder Server-Backup + Über diese App + Debug & Diagnose + Logging: An + Logging: Aus + + + + + Server-Einstellungen + Server-Einstellungen + Verbindungstyp + 🏠 Intern (HTTP) + 🌐 Extern (HTTPS) + HTTP nur für lokale Netzwerke (z.B. 192.168.x.x, 10.x.x.x) + HTTPS für sichere Verbindungen über das Internet + Server-Adresse + z.B. http://192.168.0.188:8080/notes + Server URL + Benutzername + Passwort + Anzeigen + Verstecken + Server-Status: + ✅ Erreichbar + ❌ Nicht erreichbar + 🔍 Prüfe… + ⚠️ Nicht konfiguriert + ❓ Unbekannt + Verbindung testen + Jetzt synchronisieren + + + + + Sync-Einstellungen + Sync-Einstellungen + Auto-Sync aktiviert + 🔄 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) + Auto-Sync aktiviert + Sync-Intervall + 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. + ⚡ Alle 15 Minuten + Schnellste Synchronisation • ~0.8% Akku/Tag (~23 mAh) + ✓ Alle 30 Minuten (Empfohlen) + Ausgewogenes Verhältnis • ~0.4% Akku/Tag (~12 mAh) + 🔋 Alle 60 Minuten + Maximale Akkulaufzeit • ~0.2% Akku/Tag (~6 mAh geschätzt) + + ℹ️ 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) + + + + + Markdown Desktop-Integration + Markdown Auto-Sync + ✅ Export abgeschlossen + Exportiere %1$d/%2$d Notizen… + 📝 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. + Markdown Auto-Sync + Synchronisiert Notizen automatisch als .md-Dateien (Upload + Download bei jedem Sync) + Manueller Sync exportiert alle Notizen als .md-Dateien und importiert .md-Dateien vom Server. Nützlich für einmalige Synchronisation. + 📝 Manueller Markdown-Sync + + + + + Backup & Wiederherstellung + Backup & Wiederherstellung + 📦 Bei jeder Wiederherstellung wird automatisch ein Sicherheits-Backup erstellt. + Lokales Backup + 💾 Backup erstellen + 📂 Aus Datei wiederherstellen + Server-Backup + ☁️ Vom Server wiederherstellen + ⚠️ Backup wiederherstellen? + Quelle: %s + Lokale Datei + WebDAV Server + Wiederherstellungs-Modus: + ⚪ Zusammenführen (Standard) + Neue hinzufügen, Bestehende behalten + ⚪ Zusammenführen (Standard)\n → Neue hinzufügen, Bestehende behalten + ⚪ Ersetzen + Alle löschen & Backup importieren + ⚪ Ersetzen\n → Alle löschen & Backup importieren + ⚪ Duplikate überschreiben + Backup gewinnt bei Konflikten + ⚪ Duplikate überschreiben\n → Backup gewinnt bei Konflikten + ℹ️ Ein Sicherheits-Backup wird vor dem Wiederherstellen automatisch erstellt. + Wiederherstellen + + ⚠️ Achtung:\n\nDie Wiederherstellung überschreibt ALLE lokalen Notizen mit den Daten vom Server. Diese Aktion kann nicht rückgängig gemacht werden! + Vom Server wiederherstellen + ⚠️ Vom Server wiederherstellen? + WARNUNG: Alle lokalen Notizen werden gelöscht und durch die Notizen vom Server ersetzt.\n\nDieser Vorgang kann nicht rückgängig gemacht werden! + Wiederherstellen + Stelle Notizen wieder her… + ✓ %d Notizen wiederhergestellt + Fehler: %s + + + + + Debug & Diagnose + Datei-Logging + Sync-Logs in Datei speichern + 🔒 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. + Log-Aktionen + 📤 Logs exportieren & teilen + SimpleNotes Sync Logs + Logs teilen via… + 🗑️ Logs löschen + Logs löschen? + Alle gespeicherten Sync-Logs werden unwiderruflich gelöscht. + + ℹ️ 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. + + + + + Sprache + Systemstandard + English + Deutsch + ℹ️ Wähle deine bevorzugte Sprache. Die App wird neu gestartet, um die Änderung anzuwenden. + Sprache geändert. Neustart… + + + + + Über diese App + Simple Notes Sync + Version %1$s (%2$d) + Links + GitHub Repository + Quellcode, Issues & Dokumentation + Entwickler + GitHub Profil: @inventory69 + Lizenz + MIT License - Open Source + 🔒 Datenschutz + 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. + + + + + ✅ Verbindung erfolgreich! + ❌ %s + ❌ Fehler: %s + 🔄 Synchronisiere… + ✅ Bereits synchronisiert + ✅ %d Notizen synchronisiert + ❌ %s + ✅ Auto-Sync aktiviert + Auto-Sync deaktiviert + ⏱️ Sync-Intervall: %s + 15 Minuten + 30 Minuten + 60 Minuten + ⚠️ Bitte zuerst WebDAV-Server konfigurieren + ✅ %d Notizen nach Markdown exportiert + 📝 Markdown Auto-Sync aktiviert + 📝 Markdown Auto-Sync deaktiviert + 📝 Markdown-Sync läuft… + ✅ Export: %1$d • Import: %2$d + ❌ Export fehlgeschlagen: %s + ✅ %s + ❌ Backup fehlgeschlagen: %s + ✅ %d Notizen wiederhergestellt + ❌ Wiederherstellung fehlgeschlagen: %s + Benachrichtigungen aktiviert + Benachrichtigungen deaktiviert. Du kannst sie in den Einstellungen aktivieren. + Bitte Akku-Optimierung manuell deaktivieren + 🗑️ Logs gelöscht + 📭 Keine Logs zum Löschen + ❌ Fehler beim Löschen: %s + ❌ Fehler beim Öffnen des Links + 📝 Datei-Logging aktiviert + 📝 Datei-Logging deaktiviert + ⏱️ Sync-Intervall auf %s geändert + Version nicht verfügbar + 🔍 Prüfe… + Bitte wähle \'Nicht optimieren\' für Simple Notes. + Hintergrund-Synchronisation + Damit die App im Hintergrund synchronisieren kann, muss die Akku-Optimierung deaktiviert werden.\n\nBitte wähle \'Nicht optimieren\' für Simple Notes. + Einstellungen öffnen + Später + Zurück + Ungültige Backup-Datei + Backup-Version nicht unterstützt (v%1$d benötigt v%2$d+) + Backup enthält keine Notizen + Backup enthält %d ungültige Notizen + Backup-Datei beschädigt oder ungültig: %s + Wiederherstellung fehlgeschlagen: %s + %1$d neue Notizen importiert, %2$d übersprungen + %1$d neu, %2$d überschrieben + Alle Notizen ersetzt: %d importiert + + + + + Gerade eben + Vor %d Min + Vor %d Std + Vor %d Tagen + + + + + Notizen Synchronisierung + Benachrichtigungen über Sync-Status + Sync erfolgreich + %d Notiz(en) synchronisiert + Sync fehlgeschlagen + Synchronisiere… + Notizen werden synchronisiert + Sync-Konflikt erkannt + %d Notiz(en) haben Konflikte + ⚠️ Sync-Warnung + Server seit %dh nicht erreichbar + Der WebDAV-Server ist seit %d Stunden nicht erreichbar. Bitte prüfe deine Netzwerkverbindung oder Server-Einstellungen. + Synchronisierung läuft + Notizen werden synchronisiert… + Sync Fehler + + + + + + %d Notiz + %d Notizen + + + + %d Notiz lokal gelöscht + %d Notizen lokal gelöscht + + + + %d Notiz wird vom Server gelöscht + %d Notizen werden vom Server gelöscht + + + + %d Notiz synchronisiert + %d Notizen synchronisiert + + diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index aaebe35..744688d 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -1,94 +1,394 @@ + + + + Simple Notes - - Noch keine Notizen.\nTippe + um eine zu erstellen. - Notiz hinzufügen - Synchronisieren - Einstellungen + + + + Simple Notes + No notes yet.\nTap + to create one. + Add note + Sync + Settings + Sync + Settings + Close selection + Select all + Delete selected + %d selected - + + + 📝 - Noch keine Notizen - Tippe auf ➕ um deine erste Notiz zu erstellen + No notes yet + Tap + to create a new note - - Notiz bearbeiten - Neue Notiz - Titel - Inhalt - Speichern - Löschen - Zurück + + + + New note + Text note + Checklist + Note + List - + + + Note Title Note content preview… - Vor 2 Std - Ohne Titel + 2 hours ago + Untitled + %1$d/%2$d done + No entries - - Notiz löschen? - Diese Aktion kann nicht rückgängig gemacht werden. - Abbrechen + + + + Sync Status + Syncing… + Synced + Error + Syncing… + Sync completed + Sync failed + Sync already in progress - - Server-Einstellungen + + + + Delete note? + Delete %d notes? + How do you want to delete this note? + How do you want to delete these %d notes? + Delete everywhere (also server) + Delete local only + Delete + Cancel + OK + + Delete note + \"%s\" will be deleted locally.\n\nAlso delete from server? + Delete from server + \"%s\" will be deleted locally and from server + \"%s\" deleted locally (server remains) + + + + + UNDO + \"%s\" deleted locally + \"%s\" will be deleted from server + %d note(s) deleted locally + %d note(s) will be deleted from server + Deleted from server + Server deletion failed + Server error: %s + Already synced + Server not reachable + ✅ Synced: %d notes + + + + + 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. + Invalid protocol: %s. Please use HTTP or HTTPS. + Invalid URL: %s + WebDAV server not fully configured + Sardine client could not be created + Server URL not configured + + + + + New Note + Edit Note + New List + Edit List + Title + Content + Back + Save + Add item + New item… + Reorder item + Drag to reorder + Delete item + Note is empty + Note saved + Note deleted + + + + + Settings + Language + %s + Server Settings + ✅ Reachable + ❌ Not reachable + 🔍 Checking… + ⚠️ Not configured + Sync Settings + Auto-Sync: On • %s + Auto-Sync: Off + 15 min + 30 min + 60 min + Markdown Desktop Integration + Auto-Sync: On + Auto-Sync: Off + Backup & Restore + Local or server backup + About this App + Debug & Diagnostics + Logging: On + Logging: Off + + + + + Server Settings + Server Settings + Connection Type + 🏠 Internal (HTTP) + 🌐 External (HTTPS) + HTTP only for local networks (e.g. 192.168.x.x, 10.x.x.x) + HTTPS for secure connections over the internet + Server Address + e.g. http://192.168.0.188:8080/notes Server URL - Benutzername - Passwort - Server-Status: - Prüfe… - Verbindung testen - Jetzt synchronisieren + Username + Password + Show + Hide + Server Status: + ✅ Reachable + ❌ Not reachable + 🔍 Checking… + ⚠️ Not configured + ❓ Unknown + Test Connection + Sync now - - Sync-Einstellungen - Auto-Sync aktiviert - Sync-Status - ℹ️ 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) + + + + Sync Settings + Sync Settings + Auto-Sync enabled + 🔄 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) + Auto-Sync enabled + Sync Interval + 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. + ⚡ Every 15 minutes + Fastest sync • ~0.8% battery/day (~23 mAh) + ✓ Every 30 minutes (Recommended) + Balanced ratio • ~0.4% battery/day (~12 mAh) + 🔋 Every 60 minutes + Maximum battery life • ~0.2% battery/day (~6 mAh est.) + + ℹ️ 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) - - Backup & Wiederherstellung - ⚠️ Achtung:\n\nDie Wiederherstellung überschreibt ALLE lokalen Notizen mit den Daten vom Server. Diese Aktion kann nicht rückgängig gemacht werden! - Vom Server wiederherstellen - ⚠️ Vom Server wiederherstellen? - WARNUNG: Alle lokalen Notizen werden gelöscht und durch die Notizen vom Server ersetzt.\n\nDieser Vorgang kann nicht rückgängig gemacht werden! - Wiederherstellen - Stelle Notizen wieder her… - ✓ %d Notizen wiederhergestellt - Fehler: %s + + + + Markdown Desktop Integration + Markdown Auto-Sync + ✅ Export complete + Exporting %1$d/%2$d notes… + 📝 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. + Markdown Auto-Sync + Automatically syncs notes as .md files (upload + download on each sync) + Manual sync exports all notes as .md files and imports .md files from the server. Useful for one-time sync. + 📝 Manual Markdown Sync - - Synchronisiere… - Synchronisierung abgeschlossen - Synchronisierung fehlgeschlagen - Synchronisierung läuft bereits + + + + Backup & Restore + Backup & Restore + 📦 A safety backup is automatically created before each restore. + Local Backup + 💾 Create Backup + 📂 Restore from File + Server Backup + ☁️ Restore from Server + ⚠️ Restore Backup? + Source: %s + Local File + WebDAV Server + Restore Mode: + ⚪ Merge (Default) + Add new, keep existing + ⚪ Merge (Default)\n → Add new, keep existing + ⚪ Replace + Delete all & import backup + ⚪ Replace\n → Delete all & import backup + ⚪ Overwrite duplicates + Backup wins on conflicts + ⚪ Overwrite duplicates\n → Backup wins on conflicts + ℹ️ A safety backup will be automatically created before restoring. + Restore + + ⚠️ Warning:\n\nRestoring will overwrite ALL local notes with data from the server. This action cannot be undone! + Restore from Server + ⚠️ Restore from Server? + WARNING: All local notes will be deleted and replaced with notes from the server.\n\nThis action cannot be undone! + Restore + Restoring notes… + ✓ %d notes restored + Error: %s - - ℹ️ 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. + + + + Debug & Diagnostics + File Logging + Save sync logs to file + 🔒 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. + Log Actions + 📤 Export & Share Logs + SimpleNotes Sync Logs + Share logs via… + 🗑️ Delete Logs + Delete logs? + All saved sync logs will be permanently deleted. + + ℹ️ 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. - - - + + + + Language + System Default + English + Deutsch + ℹ️ Choose your preferred language. The app will restart to apply the change. + Language changed. Restarting… - - Notiz - Liste + + + + About this App + Simple Notes Sync + Version %1$s (%2$d) + Links + GitHub Repository + Source code, issues & documentation + Developer + GitHub Profile: @inventory69 + License + MIT License - Open Source + 🔒 Privacy + This app collects no data. All notes are stored only locally on your device and on your own WebDAV server. No telemetry, no ads. - - Neue Liste - Liste bearbeiten - Element hinzufügen - Neues Element… - Element verschieben - Ziehen zum Sortieren - Element löschen - Notiz ist leer - Notiz gespeichert - Notiz gelöscht + + + + ✅ Connection successful! + ❌ %s + ❌ Error: %s + 🔄 Syncing… + ✅ Already synced + ✅ %d notes synced + ❌ %s + ✅ Auto-Sync enabled + Auto-Sync disabled + ⏱️ Sync interval: %s + 15 minutes + 30 minutes + 60 minutes + ⚠️ Please configure WebDAV server first + ✅ %d notes exported to Markdown + 📝 Markdown Auto-Sync enabled + 📝 Markdown Auto-Sync disabled + 📝 Markdown sync running… + ✅ Export: %1$d • Import: %2$d + ❌ Export failed: %s + ✅ %s + ❌ Backup failed: %s + ✅ %d notes restored + ❌ Restore failed: %s + Notifications enabled + Notifications disabled. You can enable them in settings. + Please disable battery optimization manually + 🗑️ Logs deleted + 📭 No logs to delete + ❌ Error deleting: %s + ❌ Error opening link + 📝 File logging enabled + 📝 File logging disabled + ⏱️ Sync interval changed to %s + Version not available + 🔍 Checking… + Please select \'Not optimized\' for Simple Notes. + Background Synchronization + For the app to sync in the background, battery optimization must be disabled.\n\nPlease select \'Not optimized\' for Simple Notes. + Open Settings + Later + Back + Invalid backup file + Backup version not supported (v%1$d requires v%2$d+) + Backup contains no notes + Backup contains %d invalid notes + Backup file corrupt or invalid: %s + Restore failed: %s + %1$d new notes imported, %2$d skipped + %1$d new, %2$d overwritten + All notes replaced: %d imported - - %1$d/%2$d erledigt - Keine Einträge + + + + Just now + %d min ago + %d hours ago + %d days ago + + + + + Notes Synchronization + Notifications about sync status + Sync successful + %d note(s) synchronized + Sync failed + Syncing… + Notes are being synchronized + Sync conflict detected + %d note(s) have conflicts + ⚠️ Sync Warning + Server unreachable for %dh + The WebDAV server has been unreachable for %d hours. Please check your network connection or server settings. + Synchronization in progress + Notes are being synchronized… + Sync Error + + + + + + %d note + %d notes + + + + %d note deleted locally + %d notes deleted locally + + + + %d note will be deleted from server + %d notes will be deleted from server + + + + %d note synced + %d notes synced + diff --git a/android/app/src/main/res/xml/locales_config.xml b/android/app/src/main/res/xml/locales_config.xml new file mode 100644 index 0000000..9eb057d --- /dev/null +++ b/android/app/src/main/res/xml/locales_config.xml @@ -0,0 +1,7 @@ + + + + + + +