Vault settings export/import, jump chain pro message fix, free vault import gate

- Add optional "Include settings" checkbox (unchecked by default) to both
  Save Vault Locally and Export Vault flows — exports 56 DataStore prefs
  (keyboard, display, QuickBar customization, hardware actions, etc.)
- Import automatically restores settings when present in vault file
- EXPORTABLE_*_KEYS lists in TerminalPrefsKeys define which prefs are backed up
- Fix misleading "Jump Host" pro upgrade message — now says "Jump host chaining"
  so free users understand single jump hosts work, only chaining is pro-gated
- Gate vault import for free users: can only import local vault saves (MODE_LOCAL),
  not pro-exported vaults (MODE_PASSWORD/MODE_QR)
- All strings in EN/ES/SV/FR/DE

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
jima 2026-04-12 09:58:15 +02:00
parent 6c1440e80e
commit ca4359a996
14 changed files with 255 additions and 13 deletions

View file

@ -91,6 +91,34 @@ object TerminalPrefsKeys {
val RECOVERY_USERNAME = stringPreferencesKey("recovery_username")
val RECOVERY_CONNECT_TIME = longPreferencesKey("recovery_connect_time")
val RECOVERY_PROTOCOL = stringPreferencesKey("recovery_protocol")
// Exportable key lists — used by vault save/export/import
val EXPORTABLE_STRING_KEYS = listOf(
FONT_FAMILY, THEME_NAME, KEYBOARD_LANGUAGE, TIME_FORMAT,
NUMBER_ROW_MODE, QUICK_BAR_POSITION, KEY_COLOR_PRESET, KEY_COLOR_CUSTOM,
QB_COLOR_PRESET, QB_COLOR_CUSTOM, KEYBOARD_TYPE,
AQB_POSITION, AQB_COLOR_PRESET, AQB_COLOR_CUSTOM,
CQB_CUSTOM_KEYS, CQB_CUSTOM_APPS, AQB_CUSTOM_KEYS, AQB_CUSTOM_APPS,
APP_LANGUAGE, CURSOR_SPEED, SESSION_NAV_STYLE, SFTP_SORT_ORDER,
ACTION_VOLUME_UP, ACTION_VOLUME_DOWN, ACTION_SHAKE,
ACTION_VOLUME_UP_CUSTOM, ACTION_VOLUME_DOWN_CUSTOM, ACTION_SHAKE_CUSTOM,
ACTION_VOLUME_UP_DOUBLE, ACTION_VOLUME_DOWN_DOUBLE,
ACTION_VOLUME_UP_DOUBLE_CUSTOM, ACTION_VOLUME_DOWN_DOUBLE_CUSTOM
)
val EXPORTABLE_BOOLEAN_KEYS = listOf(
HAPTIC_FEEDBACK, KEEP_SCREEN_ON, QUICK_BAR_VISIBLE, BIOMETRIC_LOCK,
AUTO_RECONNECT, NOTIFY_ON_DISCONNECT, WIFI_LOCK_DEFAULT,
KEYBOARD_SAME_SIZE_BOTH, SHOW_PAGE_INDICATORS, SHOW_KEY_HINTS,
PROTECT_SCREEN_FULL_APP, PROTECT_SCREEN_VAULT, PROTECT_SCREEN_TERMINAL,
CURSOR_BLINK, SHOW_SESSION_TAB_BAR
)
val EXPORTABLE_INT_KEYS = listOf(
SCROLLBACK_LINES, QUICK_BAR_SIZE, AQB_SIZE,
KEY_REPEAT_DELAY, LONG_PRESS_DELAY, ACTION_DOUBLE_PRESS_DELAY
)
val EXPORTABLE_FLOAT_KEYS = listOf(
FONT_SIZE_SP, KEYBOARD_HEIGHT_PERCENT, KEYBOARD_HEIGHT_LANDSCAPE
)
}
class TerminalPreferences(private val dataStore: DataStore<Preferences>, private val isTablet: Boolean = false) {
@ -634,4 +662,40 @@ class TerminalPreferences(private val dataStore: DataStore<Preferences>, private
protocol = prefs[TerminalPrefsKeys.RECOVERY_PROTOCOL] ?: "ssh"
)
}
// ========================================================================
// Vault settings export / import
// ========================================================================
/** Read all exportable preferences as a single snapshot. */
suspend fun exportableSnapshot(): Preferences = dataStore.data.first()
/** Apply imported settings, overwriting current values in a single transaction. */
suspend fun applyImportedSettings(settings: SettingsData) {
dataStore.edit { prefs ->
for ((name, value) in settings.stringValues) {
val key = TerminalPrefsKeys.EXPORTABLE_STRING_KEYS.firstOrNull { it.name == name } ?: continue
prefs[key] = value
}
for ((name, value) in settings.booleanValues) {
val key = TerminalPrefsKeys.EXPORTABLE_BOOLEAN_KEYS.firstOrNull { it.name == name } ?: continue
prefs[key] = value
}
for ((name, value) in settings.intValues) {
val key = TerminalPrefsKeys.EXPORTABLE_INT_KEYS.firstOrNull { it.name == name } ?: continue
prefs[key] = value
}
for ((name, value) in settings.floatValues) {
val key = TerminalPrefsKeys.EXPORTABLE_FLOAT_KEYS.firstOrNull { it.name == name } ?: continue
prefs[key] = value
}
}
}
}
data class SettingsData(
val stringValues: Map<String, String>,
val booleanValues: Map<String, Boolean>,
val intValues: Map<String, Int>,
val floatValues: Map<String, Float>
)

View file

@ -37,7 +37,6 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
@ -66,6 +65,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import com.roundingmobile.sshworkbench.R
import com.roundingmobile.sshworkbench.ui.theme.AppColors
import com.roundingmobile.sshworkbench.ui.theme.AppSwitch
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.fragment.app.FragmentActivity
@ -463,7 +463,7 @@ fun EditConnectionScreen(
overflow = TextOverflow.Ellipsis
)
}
Switch(
AppSwitch(
checked = viewModel.agentForwarding,
onCheckedChange = {
if (proFeatures != null && !proFeatures.agentForwarding) {
@ -577,7 +577,7 @@ fun EditConnectionScreen(
if (wouldCycle) return@DropdownMenuItem
if (hasOwnJump && proFeatures != null && proFeatures.maxJumpHops() <= 1) {
jumpDropdownExpanded = false
proFeatures.showUpgradePrompt(jumpContext, jumpContext.getString(R.string.jump_host))
proFeatures.showUpgradePrompt(jumpContext, jumpContext.getString(R.string.jump_host_chain))
} else {
viewModel.jumpHostId = conn.id
jumpDropdownExpanded = false
@ -642,7 +642,7 @@ fun EditConnectionScreen(
overflow = TextOverflow.Ellipsis
)
}
Switch(
AppSwitch(
checked = viewModel.wifiLock,
onCheckedChange = { viewModel.wifiLock = it }
)
@ -685,7 +685,7 @@ fun EditConnectionScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Switch(
AppSwitch(
checked = viewModel.autoReconnect,
onCheckedChange = { viewModel.autoReconnect = it }
)

View file

@ -86,6 +86,7 @@ fun VaultExportScreen(
val selectedKeyIds by viewModel.selectedKeyIds.collectAsStateWithLifecycle()
val selectedSnippetIds by viewModel.selectedSnippetIds.collectAsStateWithLifecycle()
val includeCredentials by viewModel.includeCredentials.collectAsStateWithLifecycle()
val includeSettings by viewModel.includeSettings.collectAsStateWithLifecycle()
val exportMode by viewModel.exportMode.collectAsStateWithLifecycle()
val password by viewModel.password.collectAsStateWithLifecycle()
val confirmPassword by viewModel.confirmPassword.collectAsStateWithLifecycle()
@ -203,6 +204,14 @@ fun VaultExportScreen(
}
}
// Settings
Spacer(Modifier.height(8.dp))
CheckboxRow(
label = stringResource(R.string.vault_include_settings),
checked = includeSettings,
onToggle = { viewModel.toggleSettings() }
)
HorizontalDivider(Modifier.padding(vertical = 12.dp))
// --- Mode selection ---
@ -319,7 +328,7 @@ fun VaultExportScreen(
Spacer(Modifier.height(16.dp))
// --- Export button ---
val hasItems = selectedHostIds.isNotEmpty() || selectedKeyIds.isNotEmpty() || selectedSnippetIds.isNotEmpty()
val hasItems = selectedHostIds.isNotEmpty() || selectedKeyIds.isNotEmpty() || selectedSnippetIds.isNotEmpty() || includeSettings
val canExport = when (exportMode) {
ExportMode.PASSWORD -> viewModel.passwordsMatch() && viewModel.passwordStrong()
ExportMode.QR -> qrKeyBase64 != null && !qrGenerating && qrSavedOrShared

View file

@ -241,12 +241,13 @@ fun VaultImportSheet(
}
ImportState.SUCCESS -> {
val parts = importSummary?.split("|") ?: listOf("0", "0")
val parts = importSummary?.split("|") ?: listOf("0", "0", "false")
val imported = parts.getOrElse(0) { "0" }.toIntOrNull() ?: 0
val skippedCount = parts.getOrElse(1) { "0" }.toIntOrNull() ?: 0
val settingsRestored = parts.getOrElse(2) { "false" } == "true"
Box(
Modifier.fillMaxWidth().height(120.dp),
Modifier.fillMaxWidth().height(if (settingsRestored) 150.dp else 120.dp),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
@ -259,6 +260,14 @@ fun VaultImportSheet(
stringResource(R.string.vault_imported_summary, imported, skippedCount),
style = MaterialTheme.typography.bodyMedium
)
if (settingsRestored) {
Spacer(Modifier.height(2.dp))
Text(
stringResource(R.string.vault_settings_restored),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
Spacer(Modifier.height(16.dp))
Button(onClick = {
viewModel.resetState()

View file

@ -1,5 +1,6 @@
package com.roundingmobile.sshworkbench.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@ -36,6 +37,7 @@ fun VaultLocalSaveScreen(
val confirmPassword by viewModel.confirmPassword.collectAsStateWithLifecycle()
val errorMessage by viewModel.errorMessage.collectAsStateWithLifecycle()
val savedPath by viewModel.savedPath.collectAsStateWithLifecycle()
val includeSettings by viewModel.includeSettings.collectAsStateWithLifecycle()
DisposableEffect(Unit) {
onDispose { viewModel.reset() }
@ -136,6 +138,26 @@ fun VaultLocalSaveScreen(
Spacer(modifier = Modifier.height(8.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { viewModel.toggleSettings() }
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = includeSettings,
onCheckedChange = { viewModel.toggleSettings() }
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(R.string.vault_include_settings),
style = MaterialTheme.typography.bodyMedium
)
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { viewModel.save(context) },
enabled = viewModel.passwordStrong() && viewModel.passwordsMatch() && confirmPassword.isNotEmpty(),

View file

@ -1,5 +1,8 @@
package com.roundingmobile.sshworkbench.vault
import androidx.datastore.preferences.core.Preferences
import com.roundingmobile.sshworkbench.data.SettingsData
import com.roundingmobile.sshworkbench.data.TerminalPrefsKeys
import com.roundingmobile.sshworkbench.data.local.PortForward
import com.roundingmobile.sshworkbench.data.local.SavedConnection
import com.roundingmobile.sshworkbench.data.local.Snippet
@ -36,7 +39,8 @@ object VaultExportSerializer {
val credentials: List<CredentialEntry>,
val keys: List<KeyEntry>,
val snippets: List<Snippet>,
val portForwards: List<PortForward>
val portForwards: List<PortForward>,
val settings: SettingsData? = null
)
data class CredentialEntry(
@ -60,6 +64,7 @@ object VaultExportSerializer {
put("keys", serializeKeys(data.keys))
put("snippets", serializeSnippets(data.snippets))
put("port_forwards", serializePortForwards(data.portForwards))
if (data.settings != null) put("settings", serializeSettings(data.settings))
}
return json.toString().toByteArray(Charsets.UTF_8)
}
@ -77,7 +82,8 @@ object VaultExportSerializer {
credentials = deserializeCredentials(json.getJSONArray("credentials")),
keys = deserializeKeys(json.getJSONArray("keys")),
snippets = deserializeSnippets(json.getJSONArray("snippets")),
portForwards = deserializePortForwards(json.optJSONArray("port_forwards") ?: JSONArray())
portForwards = deserializePortForwards(json.optJSONArray("port_forwards") ?: JSONArray()),
settings = json.optJSONObject("settings")?.let { deserializeSettings(it) }
)
}
@ -334,4 +340,70 @@ object VaultExportSerializer {
}
return list
}
// ========================================================================
// Settings serialization
// ========================================================================
/** Build SettingsData from a DataStore preferences snapshot. */
fun snapshotSettings(prefs: Preferences): SettingsData {
val strings = mutableMapOf<String, String>()
for (key in TerminalPrefsKeys.EXPORTABLE_STRING_KEYS) {
prefs[key]?.let { strings[key.name] = it }
}
val booleans = mutableMapOf<String, Boolean>()
for (key in TerminalPrefsKeys.EXPORTABLE_BOOLEAN_KEYS) {
prefs[key]?.let { booleans[key.name] = it }
}
val ints = mutableMapOf<String, Int>()
for (key in TerminalPrefsKeys.EXPORTABLE_INT_KEYS) {
prefs[key]?.let { ints[key.name] = it }
}
val floats = mutableMapOf<String, Float>()
for (key in TerminalPrefsKeys.EXPORTABLE_FLOAT_KEYS) {
prefs[key]?.let { floats[key.name] = it }
}
return SettingsData(strings, booleans, ints, floats)
}
private fun serializeSettings(settings: SettingsData): JSONObject {
val obj = JSONObject()
val strObj = JSONObject()
for ((k, v) in settings.stringValues) strObj.put(k, v)
obj.put("string", strObj)
val boolObj = JSONObject()
for ((k, v) in settings.booleanValues) boolObj.put(k, v)
obj.put("boolean", boolObj)
val intObj = JSONObject()
for ((k, v) in settings.intValues) intObj.put(k, v)
obj.put("int", intObj)
val floatObj = JSONObject()
for ((k, v) in settings.floatValues) floatObj.put(k, v.toDouble())
obj.put("float", floatObj)
return obj
}
private fun deserializeSettings(obj: JSONObject): SettingsData {
val strings = mutableMapOf<String, String>()
obj.optJSONObject("string")?.let { s ->
for (key in s.keys()) strings[key] = s.getString(key)
}
val booleans = mutableMapOf<String, Boolean>()
obj.optJSONObject("boolean")?.let { b ->
for (key in b.keys()) booleans[key] = b.getBoolean(key)
}
val ints = mutableMapOf<String, Int>()
obj.optJSONObject("int")?.let { i ->
for (key in i.keys()) ints[key] = i.getInt(key)
}
val floats = mutableMapOf<String, Float>()
obj.optJSONObject("float")?.let { f ->
for (key in f.keys()) floats[key] = f.getDouble(key).toFloat()
}
return SettingsData(strings, booleans, ints, floats)
}
}

View file

@ -6,6 +6,7 @@ import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.roundingmobile.sshworkbench.data.CredentialStore
import com.roundingmobile.sshworkbench.data.TerminalPreferences
import com.roundingmobile.sshworkbench.data.local.PortForwardDao
import com.roundingmobile.sshworkbench.data.local.SavedConnection
import com.roundingmobile.sshworkbench.data.local.SavedConnectionDao
@ -35,6 +36,7 @@ class VaultExportViewModel @Inject constructor(
private val snippetDao: SnippetDao,
private val portForwardDao: PortForwardDao,
private val credentialStore: CredentialStore,
private val terminalPreferences: TerminalPreferences,
private val strings: com.roundingmobile.sshworkbench.di.StringResolver
) : ViewModel() {
@ -61,6 +63,9 @@ class VaultExportViewModel @Inject constructor(
private val _includeCredentials = MutableStateFlow(true)
val includeCredentials: StateFlow<Boolean> = _includeCredentials
private val _includeSettings = MutableStateFlow(false)
val includeSettings: StateFlow<Boolean> = _includeSettings
// Export settings
private val _exportMode = MutableStateFlow(ExportMode.PASSWORD)
val exportMode: StateFlow<ExportMode> = _exportMode
@ -132,6 +137,10 @@ class VaultExportViewModel @Inject constructor(
_includeCredentials.value = !_includeCredentials.value
}
fun toggleSettings() {
_includeSettings.value = !_includeSettings.value
}
fun selectAllHosts() {
_selectedHostIds.value = _hosts.value.map { it.id }.toSet()
}
@ -225,13 +234,20 @@ class VaultExportViewModel @Inject constructor(
val selectedHostIdSet = _selectedHostIds.value
val portForwards = allPortForwards.filter { it.connectionId in selectedHostIdSet }
// Settings snapshot (optional)
val settings = if (_includeSettings.value) {
val prefs = terminalPreferences.exportableSnapshot()
VaultExportSerializer.snapshotSettings(prefs)
} else null
// Serialize to JSON
val vaultData = VaultExportSerializer.VaultData(
hosts = selectedHosts,
credentials = credentials,
keys = keyEntries,
snippets = selectedSnippets,
portForwards = portForwards
portForwards = portForwards,
settings = settings
)
val jsonBytes = VaultExportSerializer.serialize(vaultData)

View file

@ -10,10 +10,12 @@ import com.google.zxing.MultiFormatReader
import com.google.zxing.RGBLuminanceSource
import com.google.zxing.common.HybridBinarizer
import com.roundingmobile.sshworkbench.data.CredentialStore
import com.roundingmobile.sshworkbench.data.TerminalPreferences
import com.roundingmobile.sshworkbench.data.local.PortForwardDao
import com.roundingmobile.sshworkbench.data.local.SavedConnectionDao
import com.roundingmobile.sshworkbench.data.local.SnippetDao
import com.roundingmobile.sshworkbench.data.local.SshKeyDao
import com.roundingmobile.sshworkbench.pro.ProFeatures
import com.roundingmobile.sshworkbench.R
import com.roundingmobile.vaultcrypto.VaultCrypto
import dagger.hilt.android.lifecycle.HiltViewModel
@ -37,6 +39,8 @@ class VaultImportViewModel @Inject constructor(
private val snippetDao: SnippetDao,
private val portForwardDao: PortForwardDao,
private val credentialStore: CredentialStore,
private val proFeatures: ProFeatures,
private val terminalPreferences: TerminalPreferences,
private val strings: com.roundingmobile.sshworkbench.di.StringResolver
) : ViewModel() {
@ -85,6 +89,13 @@ class VaultImportViewModel @Inject constructor(
parsedSwb = swb
_isLocalVault.value = swb.mode == VaultExportSerializer.MODE_LOCAL
// Free users can only import local vault saves (MODE_LOCAL)
if (!proFeatures.isPro && swb.mode != VaultExportSerializer.MODE_LOCAL) {
_errorMessage.value = strings.getString(R.string.vault_import_pro_only)
_importState.value = ImportState.ERROR
return@launch
}
when (swb.mode) {
VaultExportSerializer.MODE_PASSWORD,
VaultExportSerializer.MODE_LOCAL -> {
@ -376,7 +387,13 @@ class VaultImportViewModel @Inject constructor(
imported++
}
_importSummary.value = "$imported|$skipped"
// Import settings (if present in vault)
val settingsApplied = data.settings != null
if (data.settings != null) {
terminalPreferences.applyImportedSettings(data.settings)
}
_importSummary.value = "$imported|$skipped|$settingsApplied"
_importState.value = ImportState.SUCCESS
} catch (e: Exception) {
_errorMessage.value = e.message ?: strings.getString(R.string.vault_import_failed)

View file

@ -5,6 +5,7 @@ import android.os.Environment
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.roundingmobile.sshworkbench.data.CredentialStore
import com.roundingmobile.sshworkbench.data.TerminalPreferences
import com.roundingmobile.sshworkbench.data.local.PortForwardDao
import com.roundingmobile.sshworkbench.data.local.SavedConnectionDao
import com.roundingmobile.sshworkbench.data.local.SnippetDao
@ -33,6 +34,7 @@ class VaultLocalSaveViewModel @Inject constructor(
private val snippetDao: SnippetDao,
private val portForwardDao: PortForwardDao,
private val credentialStore: CredentialStore,
private val terminalPreferences: TerminalPreferences,
private val strings: com.roundingmobile.sshworkbench.di.StringResolver
) : ViewModel() {
@ -51,6 +53,11 @@ class VaultLocalSaveViewModel @Inject constructor(
private val _savedPath = MutableStateFlow<String?>(null)
val savedPath: StateFlow<String?> = _savedPath
private val _includeSettings = MutableStateFlow(false)
val includeSettings: StateFlow<Boolean> = _includeSettings
fun toggleSettings() { _includeSettings.value = !_includeSettings.value }
fun setPassword(pwd: String) {
_password.value = pwd
_errorMessage.value = null
@ -100,7 +107,12 @@ class VaultLocalSaveViewModel @Inject constructor(
}
val portForwards = portForwardDao.getAllOnce()
val vaultData = VaultExportSerializer.VaultData(hosts, credentials, keyEntries, snippets, portForwards)
val settings = if (_includeSettings.value) {
val prefs = terminalPreferences.exportableSnapshot()
VaultExportSerializer.snapshotSettings(prefs)
} else null
val vaultData = VaultExportSerializer.VaultData(hosts, credentials, keyEntries, snippets, portForwards, settings)
// Serialize with device fingerprint
val deviceFp = DeviceFingerprint.collect(context)
@ -144,5 +156,6 @@ class VaultLocalSaveViewModel @Inject constructor(
_confirmPassword.value = ""
_errorMessage.value = null
_savedPath.value = null
_includeSettings.value = false
}
}

View file

@ -42,7 +42,10 @@
<string name="vault_local_passwords_dont_match">Passwörter stimmen nicht überein</string>
<string name="vault_local_saved">Vault erfolgreich gespeichert</string>
<string name="vault_local_wrong_device">Dieser Vault wurde auf einem anderen Gerät erstellt und kann hier nicht importiert werden</string>
<string name="vault_import_pro_only">Dieser Vault wurde mit Vault exportieren erstellt, wofür ein Pro-Abonnement zum Importieren erforderlich ist. Gratisnutzer können nur lokale Vault-Sicherungen von diesem Gerät importieren.</string>
<string name="vault_saving">Speichern…</string>
<string name="vault_include_settings">Einstellungen einschließen (Tastatur, Anzeige, QuickBar-Anpassung)</string>
<string name="vault_settings_restored">Einstellungen wiederhergestellt</string>
<string name="vault_try_again">Erneut versuchen</string>
<string name="new_connection">Neue Verbindung</string>
<string name="state_connected">Verbunden</string>
@ -83,6 +86,7 @@
<string name="select_ssh_key">SSH-Schlüssel auswählen</string>
<string name="no_keys_available">Keine Schlüssel verfügbar</string>
<string name="jump_host">Sprunghost</string>
<string name="jump_host_chain">Sprunghost-Verkettung</string>
<string name="no_jump_host">Keiner (Direktverbindung)</string>
<string name="via_jump_chain">über Sprungkette</string>
<string name="cycle_detected">Zyklus erkannt — dies würde eine Endlosschleife erzeugen</string>

View file

@ -42,7 +42,10 @@
<string name="vault_local_passwords_dont_match">Las contraseñas no coinciden</string>
<string name="vault_local_saved">Vault guardado correctamente</string>
<string name="vault_local_wrong_device">Este vault fue creado en otro dispositivo y no se puede importar aquí</string>
<string name="vault_import_pro_only">Este vault fue creado con Exportar Vault, que requiere una suscripción Pro para importar. Los usuarios gratuitos solo pueden importar copias locales de este dispositivo.</string>
<string name="vault_saving">Guardando…</string>
<string name="vault_include_settings">Incluir ajustes (teclado, pantalla, personalización de QuickBar)</string>
<string name="vault_settings_restored">Ajustes restaurados</string>
<string name="vault_try_again">Intentar de nuevo</string>
<string name="new_connection">Nueva conexión</string>
<string name="state_connected">Conectado</string>
@ -83,6 +86,7 @@
<string name="select_ssh_key">Seleccionar clave SSH</string>
<string name="no_keys_available">No hay claves disponibles</string>
<string name="jump_host">Host de salto</string>
<string name="jump_host_chain">Encadenar hosts de salto</string>
<string name="no_jump_host">Ninguno (conexión directa)</string>
<string name="via_jump_chain">a través de cadena de salto</string>
<string name="cycle_detected">Ciclo detectado — esto crearía un bucle infinito</string>

View file

@ -42,7 +42,10 @@
<string name="vault_local_passwords_dont_match">Les mots de passe ne correspondent pas</string>
<string name="vault_local_saved">Vault enregistré avec succès</string>
<string name="vault_local_wrong_device">Ce vault a été créé sur un autre appareil et ne peut pas être importé ici</string>
<string name="vault_import_pro_only">Ce vault a été créé avec Exporter Vault, qui nécessite un abonnement Pro pour l\'importer. Les utilisateurs gratuits ne peuvent importer que les sauvegardes locales de cet appareil.</string>
<string name="vault_saving">Enregistrement…</string>
<string name="vault_include_settings">Inclure les paramètres (clavier, affichage, personnalisation de la QuickBar)</string>
<string name="vault_settings_restored">Paramètres restaurés</string>
<string name="vault_try_again">Réessayer</string>
<string name="new_connection">Nouvelle connexion</string>
<string name="state_connected">Connecté</string>
@ -83,6 +86,7 @@
<string name="select_ssh_key">Sélectionner une clé SSH</string>
<string name="no_keys_available">Aucune clé disponible</string>
<string name="jump_host">Hôte de rebond</string>
<string name="jump_host_chain">Chaîne d\'hôtes de rebond</string>
<string name="no_jump_host">Aucun (connexion directe)</string>
<string name="via_jump_chain">via chaîne de rebond</string>
<string name="cycle_detected">Cycle détecté — cela créerait une boucle infinie</string>

View file

@ -42,7 +42,10 @@
<string name="vault_local_passwords_dont_match">Lösenorden matchar inte</string>
<string name="vault_local_saved">Vault sparad</string>
<string name="vault_local_wrong_device">Denna vault skapades på en annan enhet och kan inte importeras här</string>
<string name="vault_import_pro_only">Denna vault skapades med Exportera Vault, som kräver en Pro-prenumeration för att importera. Gratisanvändare kan bara importera lokala vault-kopior från denna enhet.</string>
<string name="vault_saving">Sparar…</string>
<string name="vault_include_settings">Inkludera inställningar (tangentbord, visning, QuickBar-anpassning)</string>
<string name="vault_settings_restored">Inställningar återställda</string>
<string name="vault_try_again">Försök igen</string>
<string name="new_connection">Ny anslutning</string>
<string name="state_connected">Ansluten</string>
@ -83,6 +86,7 @@
<string name="select_ssh_key">Välj SSH-nyckel</string>
<string name="no_keys_available">Inga nycklar tillgängliga</string>
<string name="jump_host">Hoppvärd</string>
<string name="jump_host_chain">Kedja av hoppvärdar</string>
<string name="no_jump_host">Ingen (direktanslutning)</string>
<string name="via_jump_chain">via hoppkedja</string>
<string name="cycle_detected">Cykel upptäckt — detta skulle skapa en oändlig loop</string>

View file

@ -41,7 +41,10 @@
<string name="vault_local_passwords_dont_match">Passwords do not match</string>
<string name="vault_local_saved">Vault saved successfully</string>
<string name="vault_local_wrong_device">This vault was created on a different device and cannot be imported here</string>
<string name="vault_import_pro_only">This vault was created with Export Vault, which requires a Pro subscription to import. Free users can only import local vault saves from this device.</string>
<string name="vault_saving">Saving…</string>
<string name="vault_include_settings">Include settings (keyboard, display, QuickBar customization)</string>
<string name="vault_settings_restored">Settings restored</string>
<string name="vault_try_again">Try again</string>
<string name="new_connection">New Connection</string>
<string name="state_connected">Connected</string>
@ -82,6 +85,7 @@
<string name="select_ssh_key">Select SSH Key</string>
<string name="no_keys_available">No keys available</string>
<string name="jump_host">Jump Host</string>
<string name="jump_host_chain">Jump host chaining</string>
<string name="no_jump_host">None (direct connection)</string>
<string name="via_jump_chain">via jump host chain</string>
<string name="cycle_detected">Cycle detected — this would create an infinite loop</string>