feat: import servers from ~/.ssh/config (#873)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-31 19:33:29 +08:00
committed by GitHub
parent a97b3cf43e
commit 12a243d139
42 changed files with 2850 additions and 334 deletions

View File

@@ -78,7 +78,7 @@ Future<SSHClient> genClient(
Loggers.app.warning('genClient', e); Loggers.app.warning('genClient', e);
if (spi.alterUrl == null) rethrow; if (spi.alterUrl == null) rethrow;
try { try {
final res = spi.fromStringUrl(); final res = spi.parseAlterUrl();
alterUser = res.$2; alterUser = res.$2;
return await SSHSocket.connect(res.$1, res.$3, timeout: timeout); return await SSHSocket.connect(res.$1, res.$3, timeout: timeout);
} catch (e) { } catch (e) {

View File

@@ -0,0 +1,84 @@
import 'package:server_box/data/model/server/server_private_info.dart';
import 'package:server_box/data/store/server.dart';
class ServerDeduplication {
/// Remove duplicate servers from the import list based on existing servers
/// Returns the deduplicated list
static List<Spi> deduplicateServers(List<Spi> importedServers) {
final existingServers = ServerStore.instance.fetch();
final deduplicated = <Spi>[];
for (final imported in importedServers) {
if (!_isDuplicate(imported, existingServers)) {
deduplicated.add(imported);
}
}
return deduplicated;
}
/// Check if an imported server is a duplicate of an existing server
static bool _isDuplicate(Spi imported, List<Spi> existing) {
for (final existingSpi in existing) {
if (imported.isSameAs(existingSpi)) {
return true;
}
}
return false;
}
/// Resolve name conflicts by appending suffixes
static List<Spi> resolveNameConflicts(List<Spi> importedServers) {
final existingServers = ServerStore.instance.fetch();
final existingNames = existingServers.map((s) => s.name).toSet();
final processedNames = <String>{};
final result = <Spi>[];
for (final server in importedServers) {
String newName = server.name;
int suffix = 1;
// Check against both existing servers and already processed servers
while (existingNames.contains(newName) || processedNames.contains(newName)) {
newName = '${server.name} ($suffix)';
suffix++;
}
processedNames.add(newName);
if (newName != server.name) {
result.add(server.copyWith(name: newName));
} else {
result.add(server);
}
}
return result;
}
/// Get summary of import operation
static ImportSummary getImportSummary(List<Spi> originalList, List<Spi> deduplicatedList) {
final duplicateCount = originalList.length - deduplicatedList.length;
return ImportSummary(
total: originalList.length,
duplicates: duplicateCount,
toImport: deduplicatedList.length,
);
}
}
class ImportSummary {
final int total;
final int duplicates;
final int toImport;
const ImportSummary({
required this.total,
required this.duplicates,
required this.toImport,
});
bool get hasDuplicates => duplicates > 0;
bool get hasItemsToImport => toImport > 0;
}

View File

@@ -0,0 +1,187 @@
import 'dart:io';
import 'package:fl_lib/fl_lib.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
/// Utility class to parse SSH config files under `~/.ssh/config`
abstract final class SSHConfig {
static const String _defaultPath = '~/.ssh/config';
static String? get _homePath {
final homePath = isWindows ? Platform.environment['USERPROFILE'] : Platform.environment['HOME'];
if (homePath == null || homePath.isEmpty) {
return null;
}
return homePath;
}
/// Get possible SSH config file paths, with macOS-specific handling
static List<String> get _possibleConfigPaths {
final paths = <String>[];
final homePath = _homePath;
if (homePath != null) {
// Standard path
paths.add('$homePath/.ssh/config');
// On macOS, also try the actual user home directory
if (isMacOS) {
// Try to get the real user home directory
final username = Platform.environment['USER'];
if (username != null) {
paths.add('/Users/$username/.ssh/config');
}
}
}
return paths;
}
/// Parse SSH config file and return a list of Spi objects
static Future<List<Spi>> parseConfig([String? configPath]) async {
final (file, exists) = configExists(configPath);
if (!exists || file == null) {
Loggers.app.info('SSH config file does not exist at path: ${configPath ?? _defaultPath}');
return [];
}
final content = await file.readAsString();
return _parseSSHConfig(content);
}
/// Parse SSH config content
static List<Spi> _parseSSHConfig(String content) {
final servers = <Spi>[];
final lines = content.split('\n');
String? currentHost;
String? hostname;
String? user;
int port = 22;
String? identityFile;
String? jumpHost;
void addServer() {
if (currentHost != null && currentHost != '*' && hostname != null) {
final spi = Spi(
name: currentHost,
ip: hostname,
port: port,
user: user ?? 'root', // Default user is 'root'
keyId: identityFile,
jumpId: jumpHost,
);
servers.add(spi);
}
}
for (final line in lines) {
final trimmed = line.trim();
if (trimmed.isEmpty || trimmed.startsWith('#')) continue;
// Handle inline comments
final commentIndex = trimmed.indexOf('#');
final cleanLine = commentIndex != -1 ? trimmed.substring(0, commentIndex).trim() : trimmed;
if (cleanLine.isEmpty) continue;
final parts = cleanLine.split(RegExp(r'\s+'));
if (parts.length < 2) continue;
final key = parts[0].toLowerCase();
var value = parts.sublist(1).join(' ');
// Remove quotes from values
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
value = value.substring(1, value.length - 1);
}
switch (key) {
case 'host':
// Save previous host config
addServer();
// Reset for new host
final originalValue = parts.sublist(1).join(' ');
final isQuoted =
(originalValue.startsWith('"') && originalValue.endsWith('"')) ||
(originalValue.startsWith("'") && originalValue.endsWith("'"));
currentHost = value;
// Skip hosts with multiple patterns (contains spaces but not quoted)
if (currentHost.contains(' ') && !isQuoted) {
currentHost = null; // Mark as invalid to skip
}
hostname = null;
user = null;
port = 22;
identityFile = null;
jumpHost = null;
break;
case 'hostname':
hostname = value;
break;
case 'user':
user = value;
break;
case 'port':
port = int.tryParse(value) ?? 22;
break;
case 'identityfile':
identityFile = value; // Store the path directly
break;
case 'proxyjump':
case 'proxycommand':
jumpHost = _extractJumpHost(value);
break;
}
}
// Add the last server
addServer();
return servers;
}
/// Extract jump host from ProxyJump or ProxyCommand
static String? _extractJumpHost(String value) {
// For ProxyJump, the format is usually: user@host:port
// For ProxyCommand, it's more complex and might need custom parsing
if (value.contains('@')) {
return value.split(' ').first;
}
return null;
}
/// Check if SSH config file exists, trying multiple possible paths
static (File?, bool) configExists([String? configPath]) {
if (configPath != null) {
// If specific path is provided, use it directly
final homePath = _homePath;
if (homePath == null) {
Loggers.app.warning('Cannot determine home directory for SSH config parsing.');
return (null, false);
}
final expandedPath = configPath.replaceFirst('~', homePath);
dprint('Checking SSH config at path: $expandedPath');
final file = File(expandedPath);
return (file, file.existsSync());
}
// Try multiple possible paths
for (final path in _possibleConfigPaths) {
dprint('Checking SSH config at path: $path');
final file = File(path);
if (file.existsSync()) {
dprint('Found SSH config at: $path');
return (file, true);
}
}
dprint('SSH config file not found in any of the expected locations');
return (null, false);
}
}

View File

@@ -56,6 +56,7 @@ abstract class Spi with _$Spi {
@override @override
String toString() => 'Spi<$oldId>'; String toString() => 'Spi<$oldId>';
/// Parse the [id], if it's null or empty, generate a new one.
static String parseId(Object? id) { static String parseId(Object? id) {
if (id == null || id is! String || id.isEmpty) return ShortId.generate(); if (id == null || id is! String || id.isEmpty) return ShortId.generate();
return id; return id;
@@ -83,20 +84,26 @@ extension Spix on Spi {
return newSpi.id; return newSpi.id;
} }
/// Json encode to string.
String toJsonString() => json.encode(toJson()); String toJsonString() => json.encode(toJson());
bool shouldReconnect(Spi old) { /// Returns true if the connection info is the same as [other].
return user != old.user || bool isSameAs(Spi other) {
ip != old.ip || return user == other.user &&
port != old.port || ip == other.ip &&
pwd != old.pwd || port == other.port &&
keyId != old.keyId || pwd == other.pwd &&
alterUrl != old.alterUrl || keyId == other.keyId &&
jumpId != old.jumpId || jumpId == other.jumpId;
custom?.cmds != old.custom?.cmds;
} }
(String ip, String usr, int port) fromStringUrl() { /// Returns true if the connection should be re-established.
bool shouldReconnect(Spi old) {
return !isSameAs(old) || alterUrl != old.alterUrl || custom?.cmds != old.custom?.cmds;
}
/// Parse the [alterUrl] to (ip, user, port).
(String ip, String usr, int port) parseAlterUrl() {
if (alterUrl == null) { if (alterUrl == null) {
throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null'); throw SSHErr(type: SSHErrType.connect, message: 'alterUrl is null');
} }
@@ -141,5 +148,6 @@ extension Spix on Spi {
id: 'id', id: 'id',
); );
/// Returns true if the user is 'root'.
bool get isRoot => user == 'root'; bool get isRoot => user == 'root';
} }

View File

@@ -125,6 +125,7 @@ abstract final class GithubIds {
'zxf945', 'zxf945',
'cnen2018', 'cnen2018',
'xiaomeng9597', 'xiaomeng9597',
'mingzhao2019',
}; };
} }

View File

@@ -277,4 +277,7 @@ class SettingStore extends HiveStore {
/// The backup password /// The backup password
late final backupasswd = SecureProp('bakPasswd'); late final backupasswd = SecureProp('bakPasswd');
/// Whether to read SSH config from ~/.ssh/config on first time
late final firstTimeReadSSHCfg = propertyDefault('firstTimeReadSSHCfg', true);
} }

View File

@@ -185,17 +185,17 @@ abstract class AppLocalizations {
/// **'Automatic home widget update'** /// **'Automatic home widget update'**
String get autoUpdateHomeWidget; String get autoUpdateHomeWidget;
/// No description provided for @backupTip. /// No description provided for @backupEncrypted.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'The exported data can be encrypted with password. \nPlease keep it safe.'** /// **'Backup is encrypted'**
String get backupTip; String get backupEncrypted;
/// No description provided for @backupVersionNotMatch. /// No description provided for @backupNotEncrypted.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Backup version is not match.'** /// **'Backup is not encrypted'**
String get backupVersionNotMatch; String get backupNotEncrypted;
/// No description provided for @backupPassword. /// No description provided for @backupPassword.
/// ///
@@ -203,6 +203,18 @@ abstract class AppLocalizations {
/// **'Backup password'** /// **'Backup password'**
String get backupPassword; String get backupPassword;
/// No description provided for @backupPasswordRemoved.
///
/// In en, this message translates to:
/// **'Backup password removed'**
String get backupPasswordRemoved;
/// No description provided for @backupPasswordSet.
///
/// In en, this message translates to:
/// **'Backup password set'**
String get backupPasswordSet;
/// No description provided for @backupPasswordTip. /// No description provided for @backupPasswordTip.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -215,29 +227,17 @@ abstract class AppLocalizations {
/// **'Incorrect backup password'** /// **'Incorrect backup password'**
String get backupPasswordWrong; String get backupPasswordWrong;
/// No description provided for @backupEncrypted. /// No description provided for @backupTip.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Backup is encrypted'** /// **'The exported data can be encrypted with password. \nPlease keep it safe.'**
String get backupEncrypted; String get backupTip;
/// No description provided for @backupNotEncrypted. /// No description provided for @backupVersionNotMatch.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
/// **'Backup is not encrypted'** /// **'Backup version is not match.'**
String get backupNotEncrypted; String get backupVersionNotMatch;
/// No description provided for @backupPasswordSet.
///
/// In en, this message translates to:
/// **'Backup password set'**
String get backupPasswordSet;
/// No description provided for @backupPasswordRemoved.
///
/// In en, this message translates to:
/// **'Backup password removed'**
String get backupPasswordRemoved;
/// No description provided for @battery. /// No description provided for @battery.
/// ///
@@ -1202,6 +1202,84 @@ abstract class AppLocalizations {
/// **'Spent time: {time}'** /// **'Spent time: {time}'**
String spentTime(Object time); String spentTime(Object time);
/// No description provided for @sshConfigAllExist.
///
/// In en, this message translates to:
/// **'All servers already exist ({duplicateCount} duplicates found)'**
String sshConfigAllExist(Object duplicateCount);
/// No description provided for @sshConfigDuplicatesSkipped.
///
/// In en, this message translates to:
/// **'{duplicateCount} duplicates will be skipped'**
String sshConfigDuplicatesSkipped(Object duplicateCount);
/// No description provided for @sshConfigFound.
///
/// In en, this message translates to:
/// **'We found SSH configuration on your system.'**
String get sshConfigFound;
/// No description provided for @sshConfigFoundServers.
///
/// In en, this message translates to:
/// **'Found {totalCount} servers'**
String sshConfigFoundServers(Object totalCount);
/// No description provided for @sshConfigImport.
///
/// In en, this message translates to:
/// **'SSH Config Import'**
String get sshConfigImport;
/// No description provided for @sshConfigImportHelp.
///
/// In en, this message translates to:
/// **'Only basic information can be imported, for example: IP/Port.'**
String get sshConfigImportHelp;
/// No description provided for @sshConfigImportPermission.
///
/// In en, this message translates to:
/// **'Would you like to give permission to read ~/.ssh/config and automatically import server settings?'**
String get sshConfigImportPermission;
/// No description provided for @sshConfigImportTip.
///
/// In en, this message translates to:
/// **'Prompt to read ~/.ssh/config on first server creation'**
String get sshConfigImportTip;
/// No description provided for @sshConfigImported.
///
/// In en, this message translates to:
/// **'Imported {count} servers from SSH config'**
String sshConfigImported(Object count);
/// No description provided for @sshConfigManualSelect.
///
/// In en, this message translates to:
/// **'Would you like to select the SSH config file manually?'**
String get sshConfigManualSelect;
/// No description provided for @sshConfigNoServers.
///
/// In en, this message translates to:
/// **'No servers found in SSH config'**
String get sshConfigNoServers;
/// No description provided for @sshConfigPermissionDenied.
///
/// In en, this message translates to:
/// **'Cannot access SSH config file due to macOS permissions.'**
String get sshConfigPermissionDenied;
/// No description provided for @sshConfigServersToImport.
///
/// In en, this message translates to:
/// **'{importCount} servers will be imported'**
String sshConfigServersToImport(Object importCount);
/// No description provided for @sshTermHelp. /// No description provided for @sshTermHelp.
/// ///
/// In en, this message translates to: /// In en, this message translates to:

View File

@@ -46,16 +46,20 @@ class AppLocalizationsDe extends AppLocalizations {
String get autoUpdateHomeWidget => 'Home-Widget automatisch aktualisieren'; String get autoUpdateHomeWidget => 'Home-Widget automatisch aktualisieren';
@override @override
String get backupTip => String get backupEncrypted => 'Backup ist verschlüsselt';
'Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.';
@override @override
String get backupVersionNotMatch => String get backupNotEncrypted => 'Backup ist nicht verschlüsselt';
'Die Backup-Version stimmt nicht überein.';
@override @override
String get backupPassword => 'Backup-Passwort'; String get backupPassword => 'Backup-Passwort';
@override
String get backupPasswordRemoved => 'Backup-Passwort entfernt';
@override
String get backupPasswordSet => 'Backup-Passwort gesetzt';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.'; 'Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.';
@@ -64,16 +68,12 @@ class AppLocalizationsDe extends AppLocalizations {
String get backupPasswordWrong => 'Falsches Backup-Passwort'; String get backupPasswordWrong => 'Falsches Backup-Passwort';
@override @override
String get backupEncrypted => 'Backup ist verschlüsselt'; String get backupTip =>
'Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.';
@override @override
String get backupNotEncrypted => 'Backup ist nicht verschlüsselt'; String get backupVersionNotMatch =>
'Die Backup-Version stimmt nicht überein.';
@override
String get backupPasswordSet => 'Backup-Passwort gesetzt';
@override
String get backupPasswordRemoved => 'Backup-Passwort entfernt';
@override @override
String get battery => 'Batterie'; String get battery => 'Batterie';
@@ -606,6 +606,62 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Benötigte Zeit: $time'; return 'Benötigte Zeit: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Alle Server existieren bereits ($duplicateCount Duplikate gefunden)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount Duplikate werden übersprungen';
}
@override
String get sshConfigFound =>
'Wir haben SSH-Konfiguration auf Ihrem System gefunden.';
@override
String sshConfigFoundServers(Object totalCount) {
return '$totalCount Server gefunden';
}
@override
String get sshConfigImport => 'SSH-Konfiguration importieren';
@override
String get sshConfigImportHelp =>
'Es können nur Basisinformationen importiert werden, zum Beispiel: IP/Port.';
@override
String get sshConfigImportPermission =>
'Möchten Sie die Berechtigung erteilen, ~/.ssh/config zu lesen und Server-Einstellungen automatisch zu importieren?';
@override
String get sshConfigImportTip =>
'Bei der ersten Server-Erstellung zum Lesen von ~/.ssh/config auffordern';
@override
String sshConfigImported(Object count) {
return '$count Server aus SSH-Konfiguration importiert';
}
@override
String get sshConfigManualSelect =>
'Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?';
@override
String get sshConfigNoServers =>
'Keine Server in der SSH-Konfiguration gefunden';
@override
String get sshConfigPermissionDenied =>
'Aufgrund der macOS-Berechtigungen kann nicht auf die SSH-Konfigurationsdatei zugegriffen werden.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount Server werden importiert';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.'; 'Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.';

View File

@@ -46,15 +46,20 @@ class AppLocalizationsEn extends AppLocalizations {
String get autoUpdateHomeWidget => 'Automatic home widget update'; String get autoUpdateHomeWidget => 'Automatic home widget update';
@override @override
String get backupTip => String get backupEncrypted => 'Backup is encrypted';
'The exported data can be encrypted with password. \nPlease keep it safe.';
@override @override
String get backupVersionNotMatch => 'Backup version is not match.'; String get backupNotEncrypted => 'Backup is not encrypted';
@override @override
String get backupPassword => 'Backup password'; String get backupPassword => 'Backup password';
@override
String get backupPasswordRemoved => 'Backup password removed';
@override
String get backupPasswordSet => 'Backup password set';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Set a password to encrypt backup files. Leave empty to disable encryption.'; 'Set a password to encrypt backup files. Leave empty to disable encryption.';
@@ -63,16 +68,11 @@ class AppLocalizationsEn extends AppLocalizations {
String get backupPasswordWrong => 'Incorrect backup password'; String get backupPasswordWrong => 'Incorrect backup password';
@override @override
String get backupEncrypted => 'Backup is encrypted'; String get backupTip =>
'The exported data can be encrypted with password. \nPlease keep it safe.';
@override @override
String get backupNotEncrypted => 'Backup is not encrypted'; String get backupVersionNotMatch => 'Backup version is not match.';
@override
String get backupPasswordSet => 'Backup password set';
@override
String get backupPasswordRemoved => 'Backup password removed';
@override @override
String get battery => 'Battery'; String get battery => 'Battery';
@@ -602,6 +602,60 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Spent time: $time'; return 'Spent time: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'All servers already exist ($duplicateCount duplicates found)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount duplicates will be skipped';
}
@override
String get sshConfigFound => 'We found SSH configuration on your system.';
@override
String sshConfigFoundServers(Object totalCount) {
return 'Found $totalCount servers';
}
@override
String get sshConfigImport => 'SSH Config Import';
@override
String get sshConfigImportHelp =>
'Only basic information can be imported, for example: IP/Port.';
@override
String get sshConfigImportPermission =>
'Would you like to give permission to read ~/.ssh/config and automatically import server settings?';
@override
String get sshConfigImportTip =>
'Prompt to read ~/.ssh/config on first server creation';
@override
String sshConfigImported(Object count) {
return 'Imported $count servers from SSH config';
}
@override
String get sshConfigManualSelect =>
'Would you like to select the SSH config file manually?';
@override
String get sshConfigNoServers => 'No servers found in SSH config';
@override
String get sshConfigPermissionDenied =>
'Cannot access SSH config file due to macOS permissions.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount servers will be imported';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.'; 'When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.';

View File

@@ -46,16 +46,20 @@ class AppLocalizationsEs extends AppLocalizations {
'Actualizar automáticamente el widget del escritorio'; 'Actualizar automáticamente el widget del escritorio';
@override @override
String get backupTip => String get backupEncrypted => 'El respaldo está encriptado';
'Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.';
@override @override
String get backupVersionNotMatch => String get backupNotEncrypted => 'El respaldo no está encriptado';
'La versión de la copia de seguridad no coincide, no se puede restaurar';
@override @override
String get backupPassword => 'Contraseña de respaldo'; String get backupPassword => 'Contraseña de respaldo';
@override
String get backupPasswordRemoved => 'Contraseña de respaldo eliminada';
@override
String get backupPasswordSet => 'Contraseña de respaldo establecida';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.'; 'Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.';
@@ -64,16 +68,12 @@ class AppLocalizationsEs extends AppLocalizations {
String get backupPasswordWrong => 'Contraseña de respaldo incorrecta'; String get backupPasswordWrong => 'Contraseña de respaldo incorrecta';
@override @override
String get backupEncrypted => 'El respaldo está encriptado'; String get backupTip =>
'Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.';
@override @override
String get backupNotEncrypted => 'El respaldo no está encriptado'; String get backupVersionNotMatch =>
'La versión de la copia de seguridad no coincide, no se puede restaurar';
@override
String get backupPasswordSet => 'Contraseña de respaldo establecida';
@override
String get backupPasswordRemoved => 'Contraseña de respaldo eliminada';
@override @override
String get battery => 'Batería'; String get battery => 'Batería';
@@ -609,6 +609,61 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Tiempo gastado: $time'; return 'Tiempo gastado: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Todos los servidores ya existen (se encontraron $duplicateCount duplicados)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return 'Se omitirán $duplicateCount duplicados';
}
@override
String get sshConfigFound => 'Encontramos configuración SSH en tu sistema';
@override
String sshConfigFoundServers(Object totalCount) {
return 'Se encontraron $totalCount servidores';
}
@override
String get sshConfigImport => 'Importar Configuración SSH';
@override
String get sshConfigImportHelp =>
'Solo se pueden importar datos básicos, por ejemplo: IP/Puerto.';
@override
String get sshConfigImportPermission =>
'¿Te gustaría dar permiso para leer ~/.ssh/config e importar automáticamente la configuración de servidores?';
@override
String get sshConfigImportTip =>
'Sugerencia para leer ~/.ssh/config al crear el primer servidor';
@override
String sshConfigImported(Object count) {
return 'Se importaron $count servidores desde la configuración SSH';
}
@override
String get sshConfigManualSelect =>
'¿Te gustaría seleccionar manualmente el archivo de configuración SSH?';
@override
String get sshConfigNoServers =>
'No se encontraron servidores en la configuración SSH';
@override
String get sshConfigPermissionDenied =>
'No se puede acceder al archivo de configuración SSH debido a los permisos de macOS.';
@override
String sshConfigServersToImport(Object importCount) {
return 'Se importarán $importCount servidores';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.'; 'Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.';

View File

@@ -46,16 +46,20 @@ class AppLocalizationsFr extends AppLocalizations {
'Mise à jour automatique du widget d\'accueil'; 'Mise à jour automatique du widget d\'accueil';
@override @override
String get backupTip => String get backupEncrypted => 'La sauvegarde est chiffrée';
'Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.';
@override @override
String get backupVersionNotMatch => String get backupNotEncrypted => 'La sauvegarde n\'est pas chiffrée';
'La version de sauvegarde ne correspond pas.';
@override @override
String get backupPassword => 'Mot de passe de sauvegarde'; String get backupPassword => 'Mot de passe de sauvegarde';
@override
String get backupPasswordRemoved => 'Mot de passe de sauvegarde supprimé';
@override
String get backupPasswordSet => 'Mot de passe de sauvegarde défini';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.'; 'Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.';
@@ -64,16 +68,12 @@ class AppLocalizationsFr extends AppLocalizations {
String get backupPasswordWrong => 'Mot de passe de sauvegarde incorrect'; String get backupPasswordWrong => 'Mot de passe de sauvegarde incorrect';
@override @override
String get backupEncrypted => 'La sauvegarde est chiffrée'; String get backupTip =>
'Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.';
@override @override
String get backupNotEncrypted => 'La sauvegarde n\'est pas chiffrée'; String get backupVersionNotMatch =>
'La version de sauvegarde ne correspond pas.';
@override
String get backupPasswordSet => 'Mot de passe de sauvegarde défini';
@override
String get backupPasswordRemoved => 'Mot de passe de sauvegarde supprimé';
@override @override
String get battery => 'Batterie'; String get battery => 'Batterie';
@@ -610,6 +610,62 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Temps écoulé : $time'; return 'Temps écoulé : $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Tous les serveurs existent déjà ($duplicateCount doublons trouvés)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount doublons seront ignorés';
}
@override
String get sshConfigFound =>
'Nous avons trouvé une configuration SSH sur votre système.';
@override
String sshConfigFoundServers(Object totalCount) {
return '$totalCount serveurs trouvés';
}
@override
String get sshConfigImport => 'Importation de configuration SSH';
@override
String get sshConfigImportHelp =>
'Seules les informations de base peuvent être importées, par exemple : IP/Port.';
@override
String get sshConfigImportPermission =>
'Souhaitez-vous donner la permission de lire ~/.ssh/config et d\'importer automatiquement les paramètres du serveur ?';
@override
String get sshConfigImportTip =>
'Proposer de lire ~/.ssh/config lors de la première création de serveur';
@override
String sshConfigImported(Object count) {
return '$count serveurs importés depuis la configuration SSH';
}
@override
String get sshConfigManualSelect =>
'Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?';
@override
String get sshConfigNoServers =>
'Aucun serveur trouvé dans la configuration SSH';
@override
String get sshConfigPermissionDenied =>
'Impossible d\'accéder au fichier de configuration SSH en raison des permissions macOS.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount serveurs seront importés';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L\'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu\'aucun texte n\'est sélectionné et qu\'il y a du contenu dans le presse-papiers. L\'icône de code colle des extraits de code dans le terminal et les exécute.'; 'Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L\'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu\'aucun texte n\'est sélectionné et qu\'il y a du contenu dans le presse-papiers. L\'icône de code colle des extraits de code dans le terminal et les exécute.';
@@ -798,5 +854,5 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get writeScriptTip => String get writeScriptTip =>
'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller létat du système. Vous pouvez examiner le contenu du script.'; 'Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l\'état du système. Vous pouvez examiner le contenu du script.';
} }

View File

@@ -46,15 +46,20 @@ class AppLocalizationsId extends AppLocalizations {
String get autoUpdateHomeWidget => 'Widget Rumah Pembaruan Otomatis'; String get autoUpdateHomeWidget => 'Widget Rumah Pembaruan Otomatis';
@override @override
String get backupTip => String get backupEncrypted => 'Cadangan telah dienkripsi';
'Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.';
@override @override
String get backupVersionNotMatch => 'Versi cadangan tidak cocok.'; String get backupNotEncrypted => 'Cadangan tidak dienkripsi';
@override @override
String get backupPassword => 'Kata sandi cadangan'; String get backupPassword => 'Kata sandi cadangan';
@override
String get backupPasswordRemoved => 'Kata sandi cadangan dihapus';
@override
String get backupPasswordSet => 'Kata sandi cadangan ditetapkan';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.'; 'Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.';
@@ -63,16 +68,11 @@ class AppLocalizationsId extends AppLocalizations {
String get backupPasswordWrong => 'Kata sandi cadangan salah'; String get backupPasswordWrong => 'Kata sandi cadangan salah';
@override @override
String get backupEncrypted => 'Cadangan telah dienkripsi'; String get backupTip =>
'Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.';
@override @override
String get backupNotEncrypted => 'Cadangan tidak dienkripsi'; String get backupVersionNotMatch => 'Versi cadangan tidak cocok.';
@override
String get backupPasswordSet => 'Kata sandi cadangan ditetapkan';
@override
String get backupPasswordRemoved => 'Kata sandi cadangan dihapus';
@override @override
String get battery => 'Baterai'; String get battery => 'Baterai';
@@ -603,6 +603,61 @@ class AppLocalizationsId extends AppLocalizations {
return 'Menghabiskan waktu: $time'; return 'Menghabiskan waktu: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Semua server sudah ada (ditemukan $duplicateCount duplikat)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount duplikat akan dilewati';
}
@override
String get sshConfigFound => 'Kami menemukan konfigurasi SSH di sistem Anda';
@override
String sshConfigFoundServers(Object totalCount) {
return 'Ditemukan $totalCount server';
}
@override
String get sshConfigImport => 'Impor Konfigurasi SSH';
@override
String get sshConfigImportHelp =>
'Hanya informasi dasar yang dapat diimpor, misalnya: IP/Port.';
@override
String get sshConfigImportPermission =>
'Apakah Anda ingin memberikan izin untuk membaca ~/.ssh/config dan secara otomatis mengimpor pengaturan server?';
@override
String get sshConfigImportTip =>
'Prompt untuk membaca ~/.ssh/config saat pembuatan server pertama';
@override
String sshConfigImported(Object count) {
return 'Berhasil mengimpor $count server dari konfigurasi SSH';
}
@override
String get sshConfigManualSelect =>
'Apakah Anda ingin memilih file konfigurasi SSH secara manual?';
@override
String get sshConfigNoServers =>
'Tidak ada server yang ditemukan dalam konfigurasi SSH';
@override
String get sshConfigPermissionDenied =>
'Tidak dapat mengakses file konfigurasi SSH karena izin macOS.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount server akan diimpor';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.'; 'Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.';

View File

@@ -43,14 +43,20 @@ class AppLocalizationsJa extends AppLocalizations {
String get autoUpdateHomeWidget => 'ホームウィジェットを自動更新'; String get autoUpdateHomeWidget => 'ホームウィジェットを自動更新';
@override @override
String get backupTip => 'エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。'; String get backupEncrypted => 'バックアップは暗号化されています';
@override @override
String get backupVersionNotMatch => 'バックアップバージョンが一致しないため、復元できません'; String get backupNotEncrypted => 'バックアップは暗号化されていません';
@override @override
String get backupPassword => 'バックアップパスワード'; String get backupPassword => 'バックアップパスワード';
@override
String get backupPasswordRemoved => 'バックアップパスワードが削除されました';
@override
String get backupPasswordSet => 'バックアップパスワードが設定されました';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。'; 'バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。';
@@ -59,16 +65,10 @@ class AppLocalizationsJa extends AppLocalizations {
String get backupPasswordWrong => 'バックアップパスワードが間違っています'; String get backupPasswordWrong => 'バックアップパスワードが間違っています';
@override @override
String get backupEncrypted => 'バックアップは暗号化されています'; String get backupTip => 'エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。';
@override @override
String get backupNotEncrypted => 'バックアップは暗号化されていません'; String get backupVersionNotMatch => 'バックアップバージョンが一致しないため、復元できません';
@override
String get backupPasswordSet => 'バックアップパスワードが設定されました';
@override
String get backupPasswordRemoved => 'バックアップパスワードが削除されました';
@override @override
String get battery => 'バッテリー'; String get battery => 'バッテリー';
@@ -587,6 +587,56 @@ class AppLocalizationsJa extends AppLocalizations {
return '費した時間: $time'; return '費した時間: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'すべてのサーバーがすでに存在します($duplicateCount個の重複が見つかりました';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount個の重複がスキップされます';
}
@override
String get sshConfigFound => 'システムにSSH設定が見つかりました。';
@override
String sshConfigFoundServers(Object totalCount) {
return '$totalCount個のサーバーが見つかりました';
}
@override
String get sshConfigImport => 'SSH設定のインポート';
@override
String get sshConfigImportHelp => 'インポートできるのは基本情報のみです。例IP/ポート。';
@override
String get sshConfigImportPermission =>
'~/.ssh/configを読み取ってサーバー設定を自動的にインポートする権限を与えますか';
@override
String get sshConfigImportTip => '初回サーバー作成時に~/.ssh/configの読み取りを促す';
@override
String sshConfigImported(Object count) {
return 'SSH設定から$count個のサーバーをインポートしました';
}
@override
String get sshConfigManualSelect => 'SSH設定ファイルを手動で選択しますか';
@override
String get sshConfigNoServers => 'SSH設定でサーバーが見つかりませんでした';
@override
String get sshConfigPermissionDenied => 'macOSの権限により、SSH設定ファイルにアクセスできません。';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount個のサーバーがインポートされます';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。'; 'ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。';

View File

@@ -46,15 +46,20 @@ class AppLocalizationsNl extends AppLocalizations {
String get autoUpdateHomeWidget => 'Automatische update van home-widget'; String get autoUpdateHomeWidget => 'Automatische update van home-widget';
@override @override
String get backupTip => String get backupEncrypted => 'Back-up is versleuteld';
'De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.';
@override @override
String get backupVersionNotMatch => 'Back-upversie komt niet overeen.'; String get backupNotEncrypted => 'Back-up is niet versleuteld';
@override @override
String get backupPassword => 'Back-up wachtwoord'; String get backupPassword => 'Back-up wachtwoord';
@override
String get backupPasswordRemoved => 'Back-up wachtwoord verwijderd';
@override
String get backupPasswordSet => 'Back-up wachtwoord ingesteld';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.'; 'Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.';
@@ -63,16 +68,11 @@ class AppLocalizationsNl extends AppLocalizations {
String get backupPasswordWrong => 'Onjuist back-up wachtwoord'; String get backupPasswordWrong => 'Onjuist back-up wachtwoord';
@override @override
String get backupEncrypted => 'Back-up is versleuteld'; String get backupTip =>
'De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.';
@override @override
String get backupNotEncrypted => 'Back-up is niet versleuteld'; String get backupVersionNotMatch => 'Back-upversie komt niet overeen.';
@override
String get backupPasswordSet => 'Back-up wachtwoord ingesteld';
@override
String get backupPasswordRemoved => 'Back-up wachtwoord verwijderd';
@override @override
String get battery => 'Batterij'; String get battery => 'Batterij';
@@ -605,6 +605,61 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Gebruikte tijd: $time'; return 'Gebruikte tijd: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Alle servers bestaan al ($duplicateCount duplicaten gevonden)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount duplicaten worden overgeslagen';
}
@override
String get sshConfigFound =>
'We hebben SSH-configuratie op uw systeem gevonden';
@override
String sshConfigFoundServers(Object totalCount) {
return '$totalCount servers gevonden';
}
@override
String get sshConfigImport => 'SSH Configuratie Importeren';
@override
String get sshConfigImportHelp =>
'Alleen basisinformatie kan worden geïmporteerd, bijvoorbeeld: IP/Poort.';
@override
String get sshConfigImportPermission =>
'Wilt u toestemming geven om ~/.ssh/config te lezen en automatisch serverinstellingen te importeren?';
@override
String get sshConfigImportTip =>
'Prompt om ~/.ssh/config te lezen bij het aanmaken van de eerste server';
@override
String sshConfigImported(Object count) {
return '$count servers geïmporteerd uit SSH-configuratie';
}
@override
String get sshConfigManualSelect =>
'Wilt u het SSH-configuratiebestand handmatig selecteren?';
@override
String get sshConfigNoServers => 'Geen servers gevonden in SSH-configuratie';
@override
String get sshConfigPermissionDenied =>
'Kan geen toegang krijgen tot SSH-configuratiebestand vanwege macOS-rechten.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount servers worden geïmporteerd';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.'; 'Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.';

View File

@@ -46,16 +46,20 @@ class AppLocalizationsPt extends AppLocalizations {
'Atualização automática do widget da tela inicial'; 'Atualização automática do widget da tela inicial';
@override @override
String get backupTip => String get backupEncrypted => 'Backup está criptografado';
'Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.';
@override @override
String get backupVersionNotMatch => String get backupNotEncrypted => 'Backup não está criptografado';
'Versão de backup não compatível, não é possível restaurar';
@override @override
String get backupPassword => 'Senha de backup'; String get backupPassword => 'Senha de backup';
@override
String get backupPasswordRemoved => 'Senha de backup removida';
@override
String get backupPasswordSet => 'Senha de backup definida';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.'; 'Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.';
@@ -64,16 +68,12 @@ class AppLocalizationsPt extends AppLocalizations {
String get backupPasswordWrong => 'Senha de backup incorreta'; String get backupPasswordWrong => 'Senha de backup incorreta';
@override @override
String get backupEncrypted => 'Backup está criptografado'; String get backupTip =>
'Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.';
@override @override
String get backupNotEncrypted => 'Backup não está criptografado'; String get backupVersionNotMatch =>
'Versão de backup não compatível, não é possível restaurar';
@override
String get backupPasswordSet => 'Senha de backup definida';
@override
String get backupPasswordRemoved => 'Senha de backup removida';
@override @override
String get battery => 'Bateria'; String get battery => 'Bateria';
@@ -604,6 +604,61 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Tempo gasto: $time'; return 'Tempo gasto: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Todos os servidores já existem (encontradas $duplicateCount duplicatas)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount duplicatas serão ignoradas';
}
@override
String get sshConfigFound => 'Encontramos configuração SSH no seu sistema';
@override
String sshConfigFoundServers(Object totalCount) {
return 'Encontrados $totalCount servidores';
}
@override
String get sshConfigImport => 'Importar Configuração SSH';
@override
String get sshConfigImportHelp =>
'Só é possível importar informações básicas, por exemplo: IP/Porta.';
@override
String get sshConfigImportPermission =>
'Gostaria de dar permissão para ler ~/.ssh/config e importar automaticamente as configurações do servidor?';
@override
String get sshConfigImportTip =>
'Sugestão para ler ~/.ssh/config na criação do primeiro servidor';
@override
String sshConfigImported(Object count) {
return 'Importados $count servidores da configuração SSH';
}
@override
String get sshConfigManualSelect =>
'Gostaria de selecionar manualmente o arquivo de configuração SSH?';
@override
String get sshConfigNoServers =>
'Nenhum servidor encontrado na configuração SSH';
@override
String get sshConfigPermissionDenied =>
'Não é possível acessar o arquivo de configuração SSH devido às permissões do macOS.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount servidores serão importados';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.'; 'Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.';

View File

@@ -46,16 +46,20 @@ class AppLocalizationsRu extends AppLocalizations {
'Автоматическое обновление виджета на главном экране'; 'Автоматическое обновление виджета на главном экране';
@override @override
String get backupTip => String get backupEncrypted => 'Резервная копия зашифрована';
'Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.';
@override @override
String get backupVersionNotMatch => String get backupNotEncrypted => 'Резервная копия не зашифрована';
'Версия резервной копии не совпадает, восстановление невозможно';
@override @override
String get backupPassword => 'Пароль резервной копии'; String get backupPassword => 'Пароль резервной копии';
@override
String get backupPasswordRemoved => 'Пароль резервной копии удален';
@override
String get backupPasswordSet => 'Пароль резервной копии установлен';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.'; 'Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.';
@@ -64,16 +68,12 @@ class AppLocalizationsRu extends AppLocalizations {
String get backupPasswordWrong => 'Неверный пароль резервной копии'; String get backupPasswordWrong => 'Неверный пароль резервной копии';
@override @override
String get backupEncrypted => 'Резервная копия зашифрована'; String get backupTip =>
'Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.';
@override @override
String get backupNotEncrypted => 'Резервная копия не зашифрована'; String get backupVersionNotMatch =>
'Версия резервной копии не совпадает, восстановление невозможно';
@override
String get backupPasswordSet => 'Пароль резервной копии установлен';
@override
String get backupPasswordRemoved => 'Пароль резервной копии удален';
@override @override
String get battery => 'Батарея'; String get battery => 'Батарея';
@@ -607,6 +607,60 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Затрачено времени: $time'; return 'Затрачено времени: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Все серверы уже существуют (найдено $duplicateCount дубликатов)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount дубликатов будут пропущены';
}
@override
String get sshConfigFound => 'Мы нашли SSH-конфигурацию в вашей системе';
@override
String sshConfigFoundServers(Object totalCount) {
return 'Найдено $totalCount серверов';
}
@override
String get sshConfigImport => 'Импорт SSH Конфигурации';
@override
String get sshConfigImportHelp =>
'Можно импортировать только базовую информацию, например: IP/порт.';
@override
String get sshConfigImportPermission =>
'Хотите ли вы дать разрешение на чтение ~/.ssh/config и автоматический импорт настроек сервера?';
@override
String get sshConfigImportTip =>
'Предложение прочитать ~/.ssh/config при создании первого сервера';
@override
String sshConfigImported(Object count) {
return 'Импортировано $count серверов из SSH-конфигурации';
}
@override
String get sshConfigManualSelect =>
'Хотели бы вы вручную выбрать файл конфигурации SSH?';
@override
String get sshConfigNoServers => 'Серверы не найдены в SSH-конфигурации';
@override
String get sshConfigPermissionDenied =>
'Невозможно получить доступ к файлу конфигурации SSH из-за разрешений macOS.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount серверов будут импортированы';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.'; 'Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.';

View File

@@ -45,15 +45,20 @@ class AppLocalizationsTr extends AppLocalizations {
String get autoUpdateHomeWidget => 'Ana ekran bileşenini otomatik güncelle'; String get autoUpdateHomeWidget => 'Ana ekran bileşenini otomatik güncelle';
@override @override
String get backupTip => String get backupEncrypted => 'Yedekleme şifrelenmiş';
'Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.';
@override @override
String get backupVersionNotMatch => 'Yedekleme sürümü eşleşmiyor.'; String get backupNotEncrypted => 'Yedekleme şifreli değil';
@override @override
String get backupPassword => 'Yedekleme parolası'; String get backupPassword => 'Yedekleme parolası';
@override
String get backupPasswordRemoved => 'Yedekleme parolası kaldırıldı';
@override
String get backupPasswordSet => 'Yedekleme parolası ayarlandı';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.'; 'Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.';
@@ -62,16 +67,11 @@ class AppLocalizationsTr extends AppLocalizations {
String get backupPasswordWrong => 'Yanlış yedekleme parolası'; String get backupPasswordWrong => 'Yanlış yedekleme parolası';
@override @override
String get backupEncrypted => 'Yedekleme şifrelenmiş'; String get backupTip =>
'Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.';
@override @override
String get backupNotEncrypted => 'Yedekleme şifreli değil'; String get backupVersionNotMatch => 'Yedekleme sürümü eşleşmiyor.';
@override
String get backupPasswordSet => 'Yedekleme parolası ayarlandı';
@override
String get backupPasswordRemoved => 'Yedekleme parolası kaldırıldı';
@override @override
String get battery => 'Pil'; String get battery => 'Pil';
@@ -603,6 +603,60 @@ class AppLocalizationsTr extends AppLocalizations {
return 'Harcanan süre: $time'; return 'Harcanan süre: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Tüm sunucular zaten mevcut ($duplicateCount kopya bulundu)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount kopya atlanacak';
}
@override
String get sshConfigFound => 'Sisteminizde SSH yapılandırması bulduk';
@override
String sshConfigFoundServers(Object totalCount) {
return '$totalCount sunucu bulundu';
}
@override
String get sshConfigImport => 'SSH Yapılandırma İçe Aktarma';
@override
String get sshConfigImportHelp =>
'Yalnızca temel bilgiler içe aktarılabilir, örneğin: IP/Port.';
@override
String get sshConfigImportPermission =>
'~/.ssh/config dosyasını okumak ve sunucu ayarlarını otomatik olarak içe aktarmak için izin vermek ister misiniz?';
@override
String get sshConfigImportTip =>
'İlk sunucu oluşturulurken ~/.ssh/config okuma istemi';
@override
String sshConfigImported(Object count) {
return 'SSH yapılandırmasından $count sunucu içe aktarıldı';
}
@override
String get sshConfigManualSelect =>
'SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?';
@override
String get sshConfigNoServers => 'SSH yapılandırmasında sunucu bulunamadı';
@override
String get sshConfigPermissionDenied =>
'macOS izinleri nedeniyle SSH yapılandırma dosyasına erişilemiyor.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount sunucu içe aktarılacak';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Terminal kaydırılabilir olduğunda, yatay olarak sürüklemek metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapar. Dosya simgesi mevcut yolu SFTP\'de açar. Pano düğmesi, metin seçiliyken içeriği kopyalar ve metin seçili değilken panoda içerik varsa terminale yapıştırır. Kod simgesi, kod parçacıklarını terminale yapıştırır ve yürütür.'; 'Terminal kaydırılabilir olduğunda, yatay olarak sürüklemek metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapar. Dosya simgesi mevcut yolu SFTP\'de açar. Pano düğmesi, metin seçiliyken içeriği kopyalar ve metin seçili değilken panoda içerik varsa terminale yapıştırır. Kod simgesi, kod parçacıklarını terminale yapıştırır ve yürütür.';

View File

@@ -46,16 +46,20 @@ class AppLocalizationsUk extends AppLocalizations {
'Автоматичне оновлення віджетів на головному екрані'; 'Автоматичне оновлення віджетів на головному екрані';
@override @override
String get backupTip => String get backupEncrypted => 'Резервна копія зашифрована';
'Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.';
@override @override
String get backupVersionNotMatch => String get backupNotEncrypted => 'Резервна копія не зашифрована';
'Версія резервного копіювання не збіглася.';
@override @override
String get backupPassword => 'Пароль резервного копіювання'; String get backupPassword => 'Пароль резервного копіювання';
@override
String get backupPasswordRemoved => 'Пароль резервного копіювання видалено';
@override
String get backupPasswordSet => 'Пароль резервного копіювання встановлено';
@override @override
String get backupPasswordTip => String get backupPasswordTip =>
'Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.'; 'Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.';
@@ -64,16 +68,12 @@ class AppLocalizationsUk extends AppLocalizations {
String get backupPasswordWrong => 'Неправильний пароль резервного копіювання'; String get backupPasswordWrong => 'Неправильний пароль резервного копіювання';
@override @override
String get backupEncrypted => 'Резервна копія зашифрована'; String get backupTip =>
'Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.';
@override @override
String get backupNotEncrypted => 'Резервна копія не зашифрована'; String get backupVersionNotMatch =>
'Версія резервного копіювання не збіглася.';
@override
String get backupPasswordSet => 'Пароль резервного копіювання встановлено';
@override
String get backupPasswordRemoved => 'Пароль резервного копіювання видалено';
@override @override
String get battery => 'Акумулятор'; String get battery => 'Акумулятор';
@@ -608,6 +608,60 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Витрачений час: $time'; return 'Витрачений час: $time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return 'Всі сервери вже існують (знайдено $duplicateCount дублікатів)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount дублікатів буде пропущено';
}
@override
String get sshConfigFound => 'Ми знайшли SSH-конфігурацію у вашій системі';
@override
String sshConfigFoundServers(Object totalCount) {
return 'Знайдено $totalCount серверів';
}
@override
String get sshConfigImport => 'Імпорт SSH Конфігурації';
@override
String get sshConfigImportHelp =>
'Можна імпортувати лише базову інформацію, наприклад: IP/порт.';
@override
String get sshConfigImportPermission =>
'Чи хочете ви надати дозвіл на читання ~/.ssh/config та автоматичний імпорт налаштувань сервера?';
@override
String get sshConfigImportTip =>
'Пропозиція прочитати ~/.ssh/config при створенні першого сервера';
@override
String sshConfigImported(Object count) {
return 'Імпортовано $count серверів з SSH-конфігурації';
}
@override
String get sshConfigManualSelect =>
'Чи хочете ви вручну вибрати файл конфігурації SSH?';
@override
String get sshConfigNoServers => 'Сервери не знайдені в SSH-конфігурації';
@override
String get sshConfigPermissionDenied =>
'Неможливо отримати доступ до файлу конфігурації SSH через дозволи macOS.';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount серверів буде імпортовано';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.'; 'Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.';

View File

@@ -42,14 +42,20 @@ class AppLocalizationsZh extends AppLocalizations {
String get autoUpdateHomeWidget => '自动更新桌面小部件'; String get autoUpdateHomeWidget => '自动更新桌面小部件';
@override @override
String get backupTip => '导出数据可通过密码加密,请妥善保管。'; String get backupEncrypted => '备份已加密';
@override @override
String get backupVersionNotMatch => '备份版本不兼容,无法恢复'; String get backupNotEncrypted => '备份未加密';
@override @override
String get backupPassword => '备份密码'; String get backupPassword => '备份密码';
@override
String get backupPasswordRemoved => '备份密码已移除';
@override
String get backupPasswordSet => '备份密码已设置';
@override @override
String get backupPasswordTip => '设置密码以加密备份文件。留空则禁用加密。'; String get backupPasswordTip => '设置密码以加密备份文件。留空则禁用加密。';
@@ -57,16 +63,10 @@ class AppLocalizationsZh extends AppLocalizations {
String get backupPasswordWrong => '备份密码错误'; String get backupPasswordWrong => '备份密码错误';
@override @override
String get backupEncrypted => '备份已加密'; String get backupTip => '导出数据可通过密码加密,请妥善保管。';
@override @override
String get backupNotEncrypted => '备份未加密'; String get backupVersionNotMatch => '备份版本不兼容,无法恢复';
@override
String get backupPasswordSet => '备份密码已设置';
@override
String get backupPasswordRemoved => '备份密码已移除';
@override @override
String get battery => '电池'; String get battery => '电池';
@@ -578,6 +578,55 @@ class AppLocalizationsZh extends AppLocalizations {
return '耗时:$time'; return '耗时:$time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return '所有服务器已存在(发现 $duplicateCount 个重复项)';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '$duplicateCount 个重复项将被跳过';
}
@override
String get sshConfigFound => '我们在您的系统中发现了 SSH 配置。';
@override
String sshConfigFoundServers(Object totalCount) {
return '发现 $totalCount 个服务器';
}
@override
String get sshConfigImport => 'SSH 配置导入';
@override
String get sshConfigImportHelp => '只能导入基础信息例如IP/端口';
@override
String get sshConfigImportPermission => '是否允许读取 ~/.ssh/config 并自动导入服务器设置?';
@override
String get sshConfigImportTip => '首次创建服务器时提示读取 ~/.ssh/config';
@override
String sshConfigImported(Object count) {
return '从 SSH 配置导入了 $count 个服务器';
}
@override
String get sshConfigManualSelect => '是否要手动选择 SSH 配置文件?';
@override
String get sshConfigNoServers => 'SSH 配置中未找到服务器';
@override
String get sshConfigPermissionDenied => '由于 macOS 权限限制,无法访问 SSH 配置文件。';
@override
String sshConfigServersToImport(Object importCount) {
return '$importCount 个服务器将被导入';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。'; '在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。';
@@ -794,14 +843,20 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get autoUpdateHomeWidget => '自動更新桌面小工具'; String get autoUpdateHomeWidget => '自動更新桌面小工具';
@override @override
String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。'; String get backupEncrypted => '備份已加密';
@override @override
String get backupVersionNotMatch => '備份版本不相容,無法還原'; String get backupNotEncrypted => '備份未加密';
@override @override
String get backupPassword => '備份密碼'; String get backupPassword => '備份密碼';
@override
String get backupPasswordRemoved => '備份密碼已移除';
@override
String get backupPasswordSet => '備份密碼已設定';
@override @override
String get backupPasswordTip => '設定密碼來加密備份檔案。留空則停用加密。'; String get backupPasswordTip => '設定密碼來加密備份檔案。留空則停用加密。';
@@ -809,16 +864,10 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
String get backupPasswordWrong => '備份密碼錯誤'; String get backupPasswordWrong => '備份密碼錯誤';
@override @override
String get backupEncrypted => '備份已加密'; String get backupTip => '匯出的資料可透過密碼加密,請妥善保管。';
@override @override
String get backupNotEncrypted => '備份未加密'; String get backupVersionNotMatch => '備份版本不相容,無法還原';
@override
String get backupPasswordSet => '備份密碼已設定';
@override
String get backupPasswordRemoved => '備份密碼已移除';
@override @override
String get battery => '電池'; String get battery => '電池';
@@ -1330,6 +1379,55 @@ class AppLocalizationsZhTw extends AppLocalizationsZh {
return '耗時:$time'; return '耗時:$time';
} }
@override
String sshConfigAllExist(Object duplicateCount) {
return '所有伺服器均已存在(發現$duplicateCount個重複項';
}
@override
String sshConfigDuplicatesSkipped(Object duplicateCount) {
return '將跳過$duplicateCount個重複項';
}
@override
String get sshConfigFound => '我們在您的系統中發現了SSH設定';
@override
String sshConfigFoundServers(Object totalCount) {
return '發現$totalCount個伺服器';
}
@override
String get sshConfigImport => '匯入SSH設定';
@override
String get sshConfigImportHelp => '只能匯入基礎資訊例如IP/端口。';
@override
String get sshConfigImportPermission => '您是否希望允許讀取 ~/.ssh/config 並自動匯入伺服器設定?';
@override
String get sshConfigImportTip => '在建立第一個伺服器時提示讀取 ~/.ssh/config';
@override
String sshConfigImported(Object count) {
return '已從SSH設定匯入$count個伺服器';
}
@override
String get sshConfigManualSelect => '是否要手動選擇 SSH 設定檔案?';
@override
String get sshConfigNoServers => 'SSH設定中未找到伺服器';
@override
String get sshConfigPermissionDenied => '由於 macOS 權限限制,無法存取 SSH 設定檔案。';
@override
String sshConfigServersToImport(Object importCount) {
return '將匯入$importCount個伺服器';
}
@override @override
String get sshTermHelp => String get sshTermHelp =>
'在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。'; '在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。';

View File

@@ -11,15 +11,15 @@
"autoConnect": "Automatisch verbinden", "autoConnect": "Automatisch verbinden",
"autoRun": "Automatischer Start", "autoRun": "Automatischer Start",
"autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren", "autoUpdateHomeWidget": "Home-Widget automatisch aktualisieren",
"backupTip": "Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.",
"backupVersionNotMatch": "Die Backup-Version stimmt nicht überein.",
"backupPassword": "Backup-Passwort",
"backupPasswordTip": "Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.",
"backupPasswordWrong": "Falsches Backup-Passwort",
"backupEncrypted": "Backup ist verschlüsselt", "backupEncrypted": "Backup ist verschlüsselt",
"backupNotEncrypted": "Backup ist nicht verschlüsselt", "backupNotEncrypted": "Backup ist nicht verschlüsselt",
"backupPasswordSet": "Backup-Passwort gesetzt", "backupPassword": "Backup-Passwort",
"backupPasswordRemoved": "Backup-Passwort entfernt", "backupPasswordRemoved": "Backup-Passwort entfernt",
"backupPasswordSet": "Backup-Passwort gesetzt",
"backupPasswordTip": "Setzen Sie ein Passwort, um Backup-Dateien zu verschlüsseln. Leer lassen, um Verschlüsselung zu deaktivieren.",
"backupPasswordWrong": "Falsches Backup-Passwort",
"backupTip": "Die exportierten Daten können mit einem Passwort verschlüsselt werden. \nBitte sicher aufbewahren.",
"backupVersionNotMatch": "Die Backup-Version stimmt nicht überein.",
"battery": "Batterie", "battery": "Batterie",
"bgRun": "Hintergrundaktualisierung", "bgRun": "Hintergrundaktualisierung",
"bgRunTip": "Dieser Schalter bedeutet nur, dass die App versuchen wird, im Hintergrund zu laufen. Ob sie im Hintergrund laufen kann, hängt davon ab, ob die Berechtigungen aktiviert sind oder nicht. Bei nativem Android deaktivieren Sie bitte \"Batterieoptimierung\" in dieser App, und bei miui ändern Sie bitte die Energiesparrichtlinie auf \"Unbegrenzt\".", "bgRunTip": "Dieser Schalter bedeutet nur, dass die App versuchen wird, im Hintergrund zu laufen. Ob sie im Hintergrund laufen kann, hängt davon ab, ob die Berechtigungen aktiviert sind oder nicht. Bei nativem Android deaktivieren Sie bitte \"Batterieoptimierung\" in dieser App, und bei miui ändern Sie bitte die Energiesparrichtlinie auf \"Unbegrenzt\".",
@@ -180,6 +180,19 @@
"specifyDevTip": "Zum Beispiel bezieht sich die Standard-Netzwerkverkehrsstatistik auf alle Geräte. Hier können Sie ein bestimmtes Gerät angeben.", "specifyDevTip": "Zum Beispiel bezieht sich die Standard-Netzwerkverkehrsstatistik auf alle Geräte. Hier können Sie ein bestimmtes Gerät angeben.",
"speed": "Tempo", "speed": "Tempo",
"spentTime": "Benötigte Zeit: {time}", "spentTime": "Benötigte Zeit: {time}",
"sshConfigAllExist": "Alle Server existieren bereits ({duplicateCount} Duplikate gefunden)",
"sshConfigDuplicatesSkipped": "{duplicateCount} Duplikate werden übersprungen",
"sshConfigFound": "Wir haben SSH-Konfiguration auf Ihrem System gefunden.",
"sshConfigFoundServers": "{totalCount} Server gefunden",
"sshConfigImport": "SSH-Konfiguration importieren",
"sshConfigImportHelp": "Es können nur Basisinformationen importiert werden, zum Beispiel: IP/Port.",
"sshConfigImportPermission": "Möchten Sie die Berechtigung erteilen, ~/.ssh/config zu lesen und Server-Einstellungen automatisch zu importieren?",
"sshConfigImportTip": "Bei der ersten Server-Erstellung zum Lesen von ~/.ssh/config auffordern",
"sshConfigImported": "{count} Server aus SSH-Konfiguration importiert",
"sshConfigManualSelect": "Möchten Sie die SSH-Konfigurationsdatei manuell auswählen?",
"sshConfigNoServers": "Keine Server in der SSH-Konfiguration gefunden",
"sshConfigPermissionDenied": "Aufgrund der macOS-Berechtigungen kann nicht auf die SSH-Konfigurationsdatei zugegriffen werden.",
"sshConfigServersToImport": "{importCount} Server werden importiert",
"sshTermHelp": "Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.", "sshTermHelp": "Wenn das Terminal scrollbar ist, kann durch horizontales Ziehen Text ausgewählt werden. Durch Klicken auf die Tastentaste wird die Tastatur ein- oder ausgeschaltet. Das Dateisymbol öffnet den aktuellen Pfad SFTP. Die Zwischenablage-Schaltfläche kopiert den Inhalt, wenn Text ausgewählt ist, und fügt Inhalte aus der Zwischenablage in das Terminal ein, wenn kein Text ausgewählt ist und Inhalte in der Zwischenablage vorhanden sind. Das Codesymbol fügt Code-Schnipsel ins Terminal ein und führt sie aus.",
"sshTip": "Diese Funktion befindet sich jetzt in der Experimentierphase.\n\nBitte melde Bugs auf {url} oder mach mit bei der Entwicklung.", "sshTip": "Diese Funktion befindet sich jetzt in der Experimentierphase.\n\nBitte melde Bugs auf {url} oder mach mit bei der Entwicklung.",
"sshVirtualKeyAutoOff": "Automatische Umschaltung der virtuellen Tasten", "sshVirtualKeyAutoOff": "Automatische Umschaltung der virtuellen Tasten",

View File

@@ -11,15 +11,15 @@
"autoConnect": "Auto connect", "autoConnect": "Auto connect",
"autoRun": "Auto run", "autoRun": "Auto run",
"autoUpdateHomeWidget": "Automatic home widget update", "autoUpdateHomeWidget": "Automatic home widget update",
"backupTip": "The exported data can be encrypted with password. \nPlease keep it safe.",
"backupVersionNotMatch": "Backup version is not match.",
"backupPassword": "Backup password",
"backupPasswordTip": "Set a password to encrypt backup files. Leave empty to disable encryption.",
"backupPasswordWrong": "Incorrect backup password",
"backupEncrypted": "Backup is encrypted", "backupEncrypted": "Backup is encrypted",
"backupNotEncrypted": "Backup is not encrypted", "backupNotEncrypted": "Backup is not encrypted",
"backupPasswordSet": "Backup password set", "backupPassword": "Backup password",
"backupPasswordRemoved": "Backup password removed", "backupPasswordRemoved": "Backup password removed",
"backupPasswordSet": "Backup password set",
"backupPasswordTip": "Set a password to encrypt backup files. Leave empty to disable encryption.",
"backupPasswordWrong": "Incorrect backup password",
"backupTip": "The exported data can be encrypted with password. \nPlease keep it safe.",
"backupVersionNotMatch": "Backup version is not match.",
"battery": "Battery", "battery": "Battery",
"bgRun": "Run in background", "bgRun": "Run in background",
"bgRunTip": "This switch only means the program will try to run in the background. Whether it can run in the background depends on whether the permission is enabled or not. For AOSP-based Android ROMs, please disable \"Battery Optimization\" in this app. For MIUI / HyperOS, please change the power saving policy to \"Unlimited\".", "bgRunTip": "This switch only means the program will try to run in the background. Whether it can run in the background depends on whether the permission is enabled or not. For AOSP-based Android ROMs, please disable \"Battery Optimization\" in this app. For MIUI / HyperOS, please change the power saving policy to \"Unlimited\".",
@@ -180,6 +180,19 @@
"specifyDevTip": "For example, network traffic statistics are by default for all devices. You can specify a particular device here.", "specifyDevTip": "For example, network traffic statistics are by default for all devices. You can specify a particular device here.",
"speed": "Speed", "speed": "Speed",
"spentTime": "Spent time: {time}", "spentTime": "Spent time: {time}",
"sshConfigAllExist": "All servers already exist ({duplicateCount} duplicates found)",
"sshConfigDuplicatesSkipped": "{duplicateCount} duplicates will be skipped",
"sshConfigFound": "We found SSH configuration on your system.",
"sshConfigFoundServers": "Found {totalCount} servers",
"sshConfigImport": "SSH Config Import",
"sshConfigImportHelp": "Only basic information can be imported, for example: IP/Port.",
"sshConfigImportPermission": "Would you like to give permission to read ~/.ssh/config and automatically import server settings?",
"sshConfigImportTip": "Prompt to read ~/.ssh/config on first server creation",
"sshConfigImported": "Imported {count} servers from SSH config",
"sshConfigManualSelect": "Would you like to select the SSH config file manually?",
"sshConfigNoServers": "No servers found in SSH config",
"sshConfigPermissionDenied": "Cannot access SSH config file due to macOS permissions.",
"sshConfigServersToImport": "{importCount} servers will be imported",
"sshTermHelp": "When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.", "sshTermHelp": "When the terminal is scrollable, dragging horizontally can select text. Clicking the keyboard button turns the keyboard on/off. The file icon opens the current path SFTP. The clipboard button copies the content when text is selected, and pastes content from the clipboard into the terminal when no text is selected and there is content on the clipboard. The code icon pastes code snippets into the terminal and executes them.",
"sshTip": "This function is now in the experimental stage.\n\nPlease report bugs on {url} or join our development.", "sshTip": "This function is now in the experimental stage.\n\nPlease report bugs on {url} or join our development.",
"sshVirtualKeyAutoOff": "Auto switching of virtual keys", "sshVirtualKeyAutoOff": "Auto switching of virtual keys",

View File

@@ -11,15 +11,15 @@
"autoConnect": "Conexión automática", "autoConnect": "Conexión automática",
"autoRun": "Ejecución automática", "autoRun": "Ejecución automática",
"autoUpdateHomeWidget": "Actualizar automáticamente el widget del escritorio", "autoUpdateHomeWidget": "Actualizar automáticamente el widget del escritorio",
"backupTip": "Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.",
"backupVersionNotMatch": "La versión de la copia de seguridad no coincide, no se puede restaurar",
"backupPassword": "Contraseña de respaldo",
"backupPasswordTip": "Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.",
"backupPasswordWrong": "Contraseña de respaldo incorrecta",
"backupEncrypted": "El respaldo está encriptado", "backupEncrypted": "El respaldo está encriptado",
"backupNotEncrypted": "El respaldo no está encriptado", "backupNotEncrypted": "El respaldo no está encriptado",
"backupPasswordSet": "Contraseña de respaldo establecida", "backupPassword": "Contraseña de respaldo",
"backupPasswordRemoved": "Contraseña de respaldo eliminada", "backupPasswordRemoved": "Contraseña de respaldo eliminada",
"backupPasswordSet": "Contraseña de respaldo establecida",
"backupPasswordTip": "Establece una contraseña para encriptar archivos de respaldo. Déjalo vacío para desactivar la encriptación.",
"backupPasswordWrong": "Contraseña de respaldo incorrecta",
"backupTip": "Los datos exportados pueden ser encriptados con contraseña. \nPor favor guárdalos en un lugar seguro.",
"backupVersionNotMatch": "La versión de la copia de seguridad no coincide, no se puede restaurar",
"battery": "Batería", "battery": "Batería",
"bgRun": "Ejecución en segundo plano", "bgRun": "Ejecución en segundo plano",
"bgRunTip": "Este interruptor solo indica que la aplicación intentará correr en segundo plano, si puede hacerlo o no depende de si tiene el permiso correspondiente. En Android puro, por favor desactiva la “optimización de batería” para esta app, en MIUI por favor cambia la estrategia de ahorro de energía a “Sin restricciones”.", "bgRunTip": "Este interruptor solo indica que la aplicación intentará correr en segundo plano, si puede hacerlo o no depende de si tiene el permiso correspondiente. En Android puro, por favor desactiva la “optimización de batería” para esta app, en MIUI por favor cambia la estrategia de ahorro de energía a “Sin restricciones”.",
@@ -180,6 +180,19 @@
"specifyDevTip": "Por ejemplo, las estadísticas de tráfico de red son por defecto para todos los dispositivos. Aquí puede especificar un dispositivo en particular.", "specifyDevTip": "Por ejemplo, las estadísticas de tráfico de red son por defecto para todos los dispositivos. Aquí puede especificar un dispositivo en particular.",
"speed": "Velocidad", "speed": "Velocidad",
"spentTime": "Tiempo gastado: {time}", "spentTime": "Tiempo gastado: {time}",
"sshConfigAllExist": "Todos los servidores ya existen (se encontraron {duplicateCount} duplicados)",
"sshConfigDuplicatesSkipped": "Se omitirán {duplicateCount} duplicados",
"sshConfigFound": "Encontramos configuración SSH en tu sistema",
"sshConfigFoundServers": "Se encontraron {totalCount} servidores",
"sshConfigImport": "Importar Configuración SSH",
"sshConfigImportHelp": "Solo se pueden importar datos básicos, por ejemplo: IP/Puerto.",
"sshConfigImportPermission": "¿Te gustaría dar permiso para leer ~/.ssh/config e importar automáticamente la configuración de servidores?",
"sshConfigImportTip": "Sugerencia para leer ~/.ssh/config al crear el primer servidor",
"sshConfigImported": "Se importaron {count} servidores desde la configuración SSH",
"sshConfigManualSelect": "¿Te gustaría seleccionar manualmente el archivo de configuración SSH?",
"sshConfigNoServers": "No se encontraron servidores en la configuración SSH",
"sshConfigPermissionDenied": "No se puede acceder al archivo de configuración SSH debido a los permisos de macOS.",
"sshConfigServersToImport": "Se importarán {importCount} servidores",
"sshTermHelp": "Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.", "sshTermHelp": "Cuando el terminal es desplazable, arrastrar horizontalmente puede seleccionar texto. Hacer clic en el botón del teclado enciende/apaga el teclado. El icono de archivo abre el SFTP de la ruta actual. El botón del portapapeles copia el contenido cuando se selecciona texto y pega el contenido del portapapeles en el terminal cuando no se selecciona texto y hay contenido en el portapapeles. El icono de código pega fragmentos de código en el terminal y los ejecuta.",
"sshTip": "Esta función está en fase de pruebas.\n\nPor favor, informa los problemas en {url}, o únete a nuestro desarrollo.", "sshTip": "Esta función está en fase de pruebas.\n\nPor favor, informa los problemas en {url}, o únete a nuestro desarrollo.",
"sshVirtualKeyAutoOff": "Desactivación automática de teclas virtuales", "sshVirtualKeyAutoOff": "Desactivación automática de teclas virtuales",

View File

@@ -11,15 +11,15 @@
"autoConnect": "Connexion automatique", "autoConnect": "Connexion automatique",
"autoRun": "Exécution automatique", "autoRun": "Exécution automatique",
"autoUpdateHomeWidget": "Mise à jour automatique du widget d'accueil", "autoUpdateHomeWidget": "Mise à jour automatique du widget d'accueil",
"backupTip": "Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.",
"backupVersionNotMatch": "La version de sauvegarde ne correspond pas.",
"backupPassword": "Mot de passe de sauvegarde",
"backupPasswordTip": "Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.",
"backupPasswordWrong": "Mot de passe de sauvegarde incorrect",
"backupEncrypted": "La sauvegarde est chiffrée", "backupEncrypted": "La sauvegarde est chiffrée",
"backupNotEncrypted": "La sauvegarde n'est pas chiffrée", "backupNotEncrypted": "La sauvegarde n'est pas chiffrée",
"backupPasswordSet": "Mot de passe de sauvegarde défini", "backupPassword": "Mot de passe de sauvegarde",
"backupPasswordRemoved": "Mot de passe de sauvegarde supprimé", "backupPasswordRemoved": "Mot de passe de sauvegarde supprimé",
"backupPasswordSet": "Mot de passe de sauvegarde défini",
"backupPasswordTip": "Définissez un mot de passe pour chiffrer les fichiers de sauvegarde. Laissez vide pour désactiver le chiffrement.",
"backupPasswordWrong": "Mot de passe de sauvegarde incorrect",
"backupTip": "Les données exportées peuvent être chiffrées avec un mot de passe. \nVeuillez les garder en sécurité.",
"backupVersionNotMatch": "La version de sauvegarde ne correspond pas.",
"battery": "Batterie", "battery": "Batterie",
"bgRun": "Exécution en arrière-plan", "bgRun": "Exécution en arrière-plan",
"bgRunTip": "Cette option signifie seulement que le programme essaiera de s'exécuter en arrière-plan, que cela soit possible dépend de l'autorisation activée ou non. Pour Android natif, veuillez désactiver l'« Optimisation de la batterie » dans cette application, et pour MIUI, veuillez changer la politique d'économie d'énergie en « Illimité ».", "bgRunTip": "Cette option signifie seulement que le programme essaiera de s'exécuter en arrière-plan, que cela soit possible dépend de l'autorisation activée ou non. Pour Android natif, veuillez désactiver l'« Optimisation de la batterie » dans cette application, et pour MIUI, veuillez changer la politique d'économie d'énergie en « Illimité ».",
@@ -180,6 +180,19 @@
"specifyDevTip": "Par exemple, les statistiques de trafic réseau concernent par défaut tous les appareils. Vous pouvez spécifier ici un appareil particulier.", "specifyDevTip": "Par exemple, les statistiques de trafic réseau concernent par défaut tous les appareils. Vous pouvez spécifier ici un appareil particulier.",
"speed": "Vitesse", "speed": "Vitesse",
"spentTime": "Temps écoulé : {time}", "spentTime": "Temps écoulé : {time}",
"sshConfigAllExist": "Tous les serveurs existent déjà ({duplicateCount} doublons trouvés)",
"sshConfigDuplicatesSkipped": "{duplicateCount} doublons seront ignorés",
"sshConfigFound": "Nous avons trouvé une configuration SSH sur votre système.",
"sshConfigFoundServers": "{totalCount} serveurs trouvés",
"sshConfigImport": "Importation de configuration SSH",
"sshConfigImportHelp": "Seules les informations de base peuvent être importées, par exemple : IP/Port.",
"sshConfigImportPermission": "Souhaitez-vous donner la permission de lire ~/.ssh/config et d'importer automatiquement les paramètres du serveur ?",
"sshConfigImportTip": "Proposer de lire ~/.ssh/config lors de la première création de serveur",
"sshConfigImported": "{count} serveurs importés depuis la configuration SSH",
"sshConfigManualSelect": "Souhaitez-vous sélectionner manuellement le fichier de configuration SSH ?",
"sshConfigNoServers": "Aucun serveur trouvé dans la configuration SSH",
"sshConfigPermissionDenied": "Impossible d'accéder au fichier de configuration SSH en raison des permissions macOS.",
"sshConfigServersToImport": "{importCount} serveurs seront importés",
"sshTermHelp": "Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu'aucun texte n'est sélectionné et qu'il y a du contenu dans le presse-papiers. L'icône de code colle des extraits de code dans le terminal et les exécute.", "sshTermHelp": "Lorsque le terminal est défilable, faire glisser horizontalement permet de sélectionner du texte. En cliquant sur le bouton du clavier, vous activez/désactivez le clavier. L'icône de fichier ouvre le chemin actuel SFTP. Le bouton du presse-papiers copie le contenu lorsque du texte est sélectionné, et colle le contenu du presse-papiers dans le terminal lorsqu'aucun texte n'est sélectionné et qu'il y a du contenu dans le presse-papiers. L'icône de code colle des extraits de code dans le terminal et les exécute.",
"sshTip": "Cette fonctionnalité est actuellement à l'étape expérimentale.\n\nVeuillez signaler les bugs sur {url} ou rejoindre notre développement.", "sshTip": "Cette fonctionnalité est actuellement à l'étape expérimentale.\n\nVeuillez signaler les bugs sur {url} ou rejoindre notre développement.",
"sshVirtualKeyAutoOff": "Activation automatique des touches virtuelles", "sshVirtualKeyAutoOff": "Activation automatique des touches virtuelles",
@@ -236,5 +249,5 @@
"wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.", "wolTip": "Après avoir configuré le WOL (Wake-on-LAN), une requête WOL est envoyée chaque fois que le serveur est connecté.",
"write": "Écrire", "write": "Écrire",
"writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.", "writeScriptFailTip": "Échec de l'écriture dans le script, probablement en raison d'un manque de permissions ou que le répertoire n'existe pas.",
"writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller létat du système. Vous pouvez examiner le contenu du script." "writeScriptTip": "Après la connexion au serveur, un script sera écrit dans `~/.config/server_box` \n | `/tmp/server_box` pour surveiller l'état du système. Vous pouvez examiner le contenu du script."
} }

View File

@@ -11,15 +11,15 @@
"autoConnect": "Hubungkan otomatis", "autoConnect": "Hubungkan otomatis",
"autoRun": "Berjalan Otomatis", "autoRun": "Berjalan Otomatis",
"autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis", "autoUpdateHomeWidget": "Widget Rumah Pembaruan Otomatis",
"backupTip": "Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.",
"backupVersionNotMatch": "Versi cadangan tidak cocok.",
"backupPassword": "Kata sandi cadangan",
"backupPasswordTip": "Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.",
"backupPasswordWrong": "Kata sandi cadangan salah",
"backupEncrypted": "Cadangan telah dienkripsi", "backupEncrypted": "Cadangan telah dienkripsi",
"backupNotEncrypted": "Cadangan tidak dienkripsi", "backupNotEncrypted": "Cadangan tidak dienkripsi",
"backupPasswordSet": "Kata sandi cadangan ditetapkan", "backupPassword": "Kata sandi cadangan",
"backupPasswordRemoved": "Kata sandi cadangan dihapus", "backupPasswordRemoved": "Kata sandi cadangan dihapus",
"backupPasswordSet": "Kata sandi cadangan ditetapkan",
"backupPasswordTip": "Setel kata sandi untuk mengenkripsi file cadangan. Biarkan kosong untuk menonaktifkan enkripsi.",
"backupPasswordWrong": "Kata sandi cadangan salah",
"backupTip": "Data yang diekspor dapat dienkripsi dengan kata sandi. \nHarap jaga keamanannya.",
"backupVersionNotMatch": "Versi cadangan tidak cocok.",
"battery": "Baterai", "battery": "Baterai",
"bgRun": "Jalankan di Backgroud", "bgRun": "Jalankan di Backgroud",
"bgRunTip": "Sakelar ini hanya berarti aplikasi akan mencoba berjalan di latar belakang, apakah aplikasi dapat berjalan di latar belakang tergantung pada apakah izin diaktifkan atau tidak. Untuk Android asli, nonaktifkan \"Pengoptimalan Baterai\" di aplikasi ini, dan untuk miui, ubah kebijakan penghematan daya ke \"Tidak Terbatas\".", "bgRunTip": "Sakelar ini hanya berarti aplikasi akan mencoba berjalan di latar belakang, apakah aplikasi dapat berjalan di latar belakang tergantung pada apakah izin diaktifkan atau tidak. Untuk Android asli, nonaktifkan \"Pengoptimalan Baterai\" di aplikasi ini, dan untuk miui, ubah kebijakan penghematan daya ke \"Tidak Terbatas\".",
@@ -180,6 +180,19 @@
"specifyDevTip": "Misalnya, statistik lalu lintas jaringan secara default adalah untuk semua perangkat. Anda dapat menentukan perangkat tertentu di sini.", "specifyDevTip": "Misalnya, statistik lalu lintas jaringan secara default adalah untuk semua perangkat. Anda dapat menentukan perangkat tertentu di sini.",
"speed": "Kecepatan", "speed": "Kecepatan",
"spentTime": "Menghabiskan waktu: {time}", "spentTime": "Menghabiskan waktu: {time}",
"sshConfigAllExist": "Semua server sudah ada (ditemukan {duplicateCount} duplikat)",
"sshConfigDuplicatesSkipped": "{duplicateCount} duplikat akan dilewati",
"sshConfigFound": "Kami menemukan konfigurasi SSH di sistem Anda",
"sshConfigFoundServers": "Ditemukan {totalCount} server",
"sshConfigImport": "Impor Konfigurasi SSH",
"sshConfigImportHelp": "Hanya informasi dasar yang dapat diimpor, misalnya: IP/Port.",
"sshConfigImportPermission": "Apakah Anda ingin memberikan izin untuk membaca ~/.ssh/config dan secara otomatis mengimpor pengaturan server?",
"sshConfigImportTip": "Prompt untuk membaca ~/.ssh/config saat pembuatan server pertama",
"sshConfigImported": "Berhasil mengimpor {count} server dari konfigurasi SSH",
"sshConfigManualSelect": "Apakah Anda ingin memilih file konfigurasi SSH secara manual?",
"sshConfigNoServers": "Tidak ada server yang ditemukan dalam konfigurasi SSH",
"sshConfigPermissionDenied": "Tidak dapat mengakses file konfigurasi SSH karena izin macOS.",
"sshConfigServersToImport": "{importCount} server akan diimpor",
"sshTermHelp": "Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.", "sshTermHelp": "Ketika terminal dapat digulirkan, menggeser secara horizontal dapat memilih teks. Mengklik tombol keyboard mengaktifkan/menonaktifkan keyboard. Ikon file membuka SFTP jalur saat ini. Tombol papan klip menyalin konten saat teks dipilih, dan menempelkan konten dari papan klip ke terminal saat tidak ada teks yang dipilih dan ada konten di papan klip. Ikon kode menempelkan potongan kode ke terminal dan mengeksekusinya.",
"sshTip": "Fungsi ini sekarang dalam tahap eksperimen.\n\nHarap laporkan bug di {url} atau bergabunglah dengan pengembangan kami.", "sshTip": "Fungsi ini sekarang dalam tahap eksperimen.\n\nHarap laporkan bug di {url} atau bergabunglah dengan pengembangan kami.",
"sshVirtualKeyAutoOff": "Switching Otomatis Kunci Virtual", "sshVirtualKeyAutoOff": "Switching Otomatis Kunci Virtual",

View File

@@ -11,15 +11,15 @@
"autoConnect": "自動接続", "autoConnect": "自動接続",
"autoRun": "自動実行", "autoRun": "自動実行",
"autoUpdateHomeWidget": "ホームウィジェットを自動更新", "autoUpdateHomeWidget": "ホームウィジェットを自動更新",
"backupTip": "エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。",
"backupVersionNotMatch": "バックアップバージョンが一致しないため、復元できません",
"backupPassword": "バックアップパスワード",
"backupPasswordTip": "バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。",
"backupPasswordWrong": "バックアップパスワードが間違っています",
"backupEncrypted": "バックアップは暗号化されています", "backupEncrypted": "バックアップは暗号化されています",
"backupNotEncrypted": "バックアップは暗号化されていません", "backupNotEncrypted": "バックアップは暗号化されていません",
"backupPasswordSet": "バックアップパスワードが設定されました", "backupPassword": "バックアップパスワード",
"backupPasswordRemoved": "バックアップパスワードが削除されました", "backupPasswordRemoved": "バックアップパスワードが削除されました",
"backupPasswordSet": "バックアップパスワードが設定されました",
"backupPasswordTip": "バックアップファイルを暗号化するためのパスワードを設定してください。暗号化を無効にするには空白のままにしてください。",
"backupPasswordWrong": "バックアップパスワードが間違っています",
"backupTip": "エクスポートされたデータはパスワードで暗号化できます。 \n適切に保管してください。",
"backupVersionNotMatch": "バックアップバージョンが一致しないため、復元できません",
"battery": "バッテリー", "battery": "バッテリー",
"bgRun": "バックグラウンド実行", "bgRun": "バックグラウンド実行",
"bgRunTip": "このスイッチはプログラムがバックグラウンドで実行を試みることを意味しますが、実際にバックグラウンドで実行できるかどうかは、権限が有効になっているかに依存します。AOSPベースのAndroid ROMでは、このアプリの「バッテリー最適化」をオフにしてください。MIUIでは、省エネモードを「無制限」に変更してください。", "bgRunTip": "このスイッチはプログラムがバックグラウンドで実行を試みることを意味しますが、実際にバックグラウンドで実行できるかどうかは、権限が有効になっているかに依存します。AOSPベースのAndroid ROMでは、このアプリの「バッテリー最適化」をオフにしてください。MIUIでは、省エネモードを「無制限」に変更してください。",
@@ -180,6 +180,19 @@
"specifyDevTip": "例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。", "specifyDevTip": "例えば、ネットワークトラフィック統計はデフォルトですべてのデバイスに対するものです。ここで特定のデバイスを指定できます。",
"speed": "速度", "speed": "速度",
"spentTime": "費した時間: {time}", "spentTime": "費した時間: {time}",
"sshConfigAllExist": "すべてのサーバーがすでに存在します({duplicateCount}個の重複が見つかりました)",
"sshConfigDuplicatesSkipped": "{duplicateCount}個の重複がスキップされます",
"sshConfigFound": "システムにSSH設定が見つかりました。",
"sshConfigFoundServers": "{totalCount}個のサーバーが見つかりました",
"sshConfigImport": "SSH設定のインポート",
"sshConfigImportHelp": "インポートできるのは基本情報のみです。例IP/ポート。",
"sshConfigImportPermission": "~/.ssh/configを読み取ってサーバー設定を自動的にインポートする権限を与えますか",
"sshConfigImportTip": "初回サーバー作成時に~/.ssh/configの読み取りを促す",
"sshConfigImported": "SSH設定から{count}個のサーバーをインポートしました",
"sshConfigManualSelect": "SSH設定ファイルを手動で選択しますか",
"sshConfigNoServers": "SSH設定でサーバーが見つかりませんでした",
"sshConfigPermissionDenied": "macOSの権限により、SSH設定ファイルにアクセスできません。",
"sshConfigServersToImport": "{importCount}個のサーバーがインポートされます",
"sshTermHelp": "ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。", "sshTermHelp": "ターミナルがスクロール可能な場合、横にドラッグするとテキストを選択できます。キーボードボタンをクリックするとキーボードのオン/オフが切り替わります。ファイルアイコンは現在のパスSFTPを開きます。クリップボードボタンは、テキストが選択されているときに内容をコピーし、テキストが選択されておらずクリップボードに内容がある場合には、その内容をターミナルに貼り付けます。コードアイコンは、コードスニペットをターミナルに貼り付けて実行します。",
"sshTip": "この機能は現在テスト段階にあります。\n\n問題がある場合は、{url}でフィードバックしてください。", "sshTip": "この機能は現在テスト段階にあります。\n\n問題がある場合は、{url}でフィードバックしてください。",
"sshVirtualKeyAutoOff": "仮想キーの自動オフ", "sshVirtualKeyAutoOff": "仮想キーの自動オフ",

View File

@@ -11,15 +11,15 @@
"autoConnect": "Automatisch verbinden", "autoConnect": "Automatisch verbinden",
"autoRun": "Automatisch uitvoeren", "autoRun": "Automatisch uitvoeren",
"autoUpdateHomeWidget": "Automatische update van home-widget", "autoUpdateHomeWidget": "Automatische update van home-widget",
"backupTip": "De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.",
"backupVersionNotMatch": "Back-upversie komt niet overeen.",
"backupPassword": "Back-up wachtwoord",
"backupPasswordTip": "Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.",
"backupPasswordWrong": "Onjuist back-up wachtwoord",
"backupEncrypted": "Back-up is versleuteld", "backupEncrypted": "Back-up is versleuteld",
"backupNotEncrypted": "Back-up is niet versleuteld", "backupNotEncrypted": "Back-up is niet versleuteld",
"backupPasswordSet": "Back-up wachtwoord ingesteld", "backupPassword": "Back-up wachtwoord",
"backupPasswordRemoved": "Back-up wachtwoord verwijderd", "backupPasswordRemoved": "Back-up wachtwoord verwijderd",
"backupPasswordSet": "Back-up wachtwoord ingesteld",
"backupPasswordTip": "Stel een wachtwoord in om back-upbestanden te versleutelen. Laat leeg om versleuteling uit te schakelen.",
"backupPasswordWrong": "Onjuist back-up wachtwoord",
"backupTip": "De geëxporteerde gegevens kunnen worden versleuteld met een wachtwoord. \nBewaar deze aub veilig.",
"backupVersionNotMatch": "Back-upversie komt niet overeen.",
"battery": "Batterij", "battery": "Batterij",
"bgRun": "Uitvoeren op de achtergrond", "bgRun": "Uitvoeren op de achtergrond",
"bgRunTip": "Deze schakelaar betekent alleen dat het programma zal proberen op de achtergrond uit te voeren, of het in de achtergrond kan worden uitgevoerd, hangt af van of de toestemming is ingeschakeld of niet. Voor native Android, schakel \"Batterijoptimalisatie\" uit in deze app, en voor miui, wijzig de energiebesparingsbeleid naar \"Onbeperkt\".", "bgRunTip": "Deze schakelaar betekent alleen dat het programma zal proberen op de achtergrond uit te voeren, of het in de achtergrond kan worden uitgevoerd, hangt af van of de toestemming is ingeschakeld of niet. Voor native Android, schakel \"Batterijoptimalisatie\" uit in deze app, en voor miui, wijzig de energiebesparingsbeleid naar \"Onbeperkt\".",
@@ -180,6 +180,19 @@
"specifyDevTip": "Bijvoorbeeld, netwerkverkeersstatistieken zijn standaard voor alle apparaten. Hier kunt u een specifiek apparaat opgeven.", "specifyDevTip": "Bijvoorbeeld, netwerkverkeersstatistieken zijn standaard voor alle apparaten. Hier kunt u een specifiek apparaat opgeven.",
"speed": "Snelheid", "speed": "Snelheid",
"spentTime": "Gebruikte tijd: {time}", "spentTime": "Gebruikte tijd: {time}",
"sshConfigAllExist": "Alle servers bestaan al ({duplicateCount} duplicaten gevonden)",
"sshConfigDuplicatesSkipped": "{duplicateCount} duplicaten worden overgeslagen",
"sshConfigFound": "We hebben SSH-configuratie op uw systeem gevonden",
"sshConfigFoundServers": "{totalCount} servers gevonden",
"sshConfigImport": "SSH Configuratie Importeren",
"sshConfigImportHelp": "Alleen basisinformatie kan worden geïmporteerd, bijvoorbeeld: IP/Poort.",
"sshConfigImportPermission": "Wilt u toestemming geven om ~/.ssh/config te lezen en automatisch serverinstellingen te importeren?",
"sshConfigImportTip": "Prompt om ~/.ssh/config te lezen bij het aanmaken van de eerste server",
"sshConfigImported": "{count} servers geïmporteerd uit SSH-configuratie",
"sshConfigManualSelect": "Wilt u het SSH-configuratiebestand handmatig selecteren?",
"sshConfigNoServers": "Geen servers gevonden in SSH-configuratie",
"sshConfigPermissionDenied": "Kan geen toegang krijgen tot SSH-configuratiebestand vanwege macOS-rechten.",
"sshConfigServersToImport": "{importCount} servers worden geïmporteerd",
"sshTermHelp": "Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.", "sshTermHelp": "Wanneer het terminal scrollbaar is, kan horizontaal slepen tekst selecteren. Klikken op de toetsenbordknop schakelt het toetsenbord aan/uit. Het bestandsicoon opent de huidige pad SFTP. De klembordknop kopieert de inhoud wanneer tekst is geselecteerd en plakt inhoud van het klembord in de terminal wanneer geen tekst is geselecteerd en er inhoud op het klembord staat. Het code-icoon plakt codefragmenten in de terminal en voert ze uit.",
"sshTip": "Deze functie bevindt zich momenteel in de experimentele fase.\n\nMeld alstublieft bugs op {url} of sluit je aan bij onze ontwikkeling.", "sshTip": "Deze functie bevindt zich momenteel in de experimentele fase.\n\nMeld alstublieft bugs op {url} of sluit je aan bij onze ontwikkeling.",
"sshVirtualKeyAutoOff": "Automatisch schakelen van virtuele toetsen", "sshVirtualKeyAutoOff": "Automatisch schakelen van virtuele toetsen",

View File

@@ -11,15 +11,15 @@
"autoConnect": "Conexão automática", "autoConnect": "Conexão automática",
"autoRun": "Execução automática", "autoRun": "Execução automática",
"autoUpdateHomeWidget": "Atualização automática do widget da tela inicial", "autoUpdateHomeWidget": "Atualização automática do widget da tela inicial",
"backupTip": "Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.",
"backupVersionNotMatch": "Versão de backup não compatível, não é possível restaurar",
"backupPassword": "Senha de backup",
"backupPasswordTip": "Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.",
"backupPasswordWrong": "Senha de backup incorreta",
"backupEncrypted": "Backup está criptografado", "backupEncrypted": "Backup está criptografado",
"backupNotEncrypted": "Backup não está criptografado", "backupNotEncrypted": "Backup não está criptografado",
"backupPasswordSet": "Senha de backup definida", "backupPassword": "Senha de backup",
"backupPasswordRemoved": "Senha de backup removida", "backupPasswordRemoved": "Senha de backup removida",
"backupPasswordSet": "Senha de backup definida",
"backupPasswordTip": "Defina uma senha para criptografar arquivos de backup. Deixe vazio para desabilitar a criptografia.",
"backupPasswordWrong": "Senha de backup incorreta",
"backupTip": "Os dados exportados podem ser criptografados com senha. \nPor favor, guarde-os com segurança.",
"backupVersionNotMatch": "Versão de backup não compatível, não é possível restaurar",
"battery": "Bateria", "battery": "Bateria",
"bgRun": "Execução em segundo plano", "bgRun": "Execução em segundo plano",
"bgRunTip": "Este interruptor indica que o programa tentará rodar em segundo plano, mas a capacidade de fazer isso depende das permissões concedidas. No Android nativo, desative a 'Otimização de bateria' para este app, no MIUI, altere a estratégia de economia de energia para 'Sem restrições'.", "bgRunTip": "Este interruptor indica que o programa tentará rodar em segundo plano, mas a capacidade de fazer isso depende das permissões concedidas. No Android nativo, desative a 'Otimização de bateria' para este app, no MIUI, altere a estratégia de economia de energia para 'Sem restrições'.",
@@ -180,6 +180,19 @@
"specifyDevTip": "Por exemplo, as estatísticas de tráfego de rede são por padrão para todos os dispositivos. Você pode especificar um dispositivo específico aqui.", "specifyDevTip": "Por exemplo, as estatísticas de tráfego de rede são por padrão para todos os dispositivos. Você pode especificar um dispositivo específico aqui.",
"speed": "Velocidade", "speed": "Velocidade",
"spentTime": "Tempo gasto: {time}", "spentTime": "Tempo gasto: {time}",
"sshConfigAllExist": "Todos os servidores já existem (encontradas {duplicateCount} duplicatas)",
"sshConfigDuplicatesSkipped": "{duplicateCount} duplicatas serão ignoradas",
"sshConfigFound": "Encontramos configuração SSH no seu sistema",
"sshConfigFoundServers": "Encontrados {totalCount} servidores",
"sshConfigImport": "Importar Configuração SSH",
"sshConfigImportHelp": "Só é possível importar informações básicas, por exemplo: IP/Porta.",
"sshConfigImportPermission": "Gostaria de dar permissão para ler ~/.ssh/config e importar automaticamente as configurações do servidor?",
"sshConfigImportTip": "Sugestão para ler ~/.ssh/config na criação do primeiro servidor",
"sshConfigImported": "Importados {count} servidores da configuração SSH",
"sshConfigManualSelect": "Gostaria de selecionar manualmente o arquivo de configuração SSH?",
"sshConfigNoServers": "Nenhum servidor encontrado na configuração SSH",
"sshConfigPermissionDenied": "Não é possível acessar o arquivo de configuração SSH devido às permissões do macOS.",
"sshConfigServersToImport": "{importCount} servidores serão importados",
"sshTermHelp": "Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.", "sshTermHelp": "Quando o terminal é rolável, arrastar horizontalmente pode selecionar texto. Clicar no botão do teclado ativa/desativa o teclado. O ícone de arquivo abre o SFTP do caminho atual. O botão da área de transferência copia o conteúdo quando o texto é selecionado e cola o conteúdo da área de transferência no terminal quando nenhum texto é selecionado e há conteúdo na área de transferência. O ícone de código cola trechos de código no terminal e os executa.",
"sshTip": "Esta funcionalidade está em fase de teste.\n\nPor favor, reporte problemas em {url} ou junte-se a nós no desenvolvimento.", "sshTip": "Esta funcionalidade está em fase de teste.\n\nPor favor, reporte problemas em {url} ou junte-se a nós no desenvolvimento.",
"sshVirtualKeyAutoOff": "Desativação automática das teclas virtuais", "sshVirtualKeyAutoOff": "Desativação automática das teclas virtuais",

View File

@@ -11,15 +11,15 @@
"autoConnect": "Автоматическое подключение", "autoConnect": "Автоматическое подключение",
"autoRun": "Автозапуск", "autoRun": "Автозапуск",
"autoUpdateHomeWidget": "Автоматическое обновление виджета на главном экране", "autoUpdateHomeWidget": "Автоматическое обновление виджета на главном экране",
"backupTip": "Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.",
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
"backupPassword": "Пароль резервной копии",
"backupPasswordTip": "Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.",
"backupPasswordWrong": "Неверный пароль резервной копии",
"backupEncrypted": "Резервная копия зашифрована", "backupEncrypted": "Резервная копия зашифрована",
"backupNotEncrypted": "Резервная копия не зашифрована", "backupNotEncrypted": "Резервная копия не зашифрована",
"backupPasswordSet": "Пароль резервной копии установлен", "backupPassword": "Пароль резервной копии",
"backupPasswordRemoved": "Пароль резервной копии удален", "backupPasswordRemoved": "Пароль резервной копии удален",
"backupPasswordSet": "Пароль резервной копии установлен",
"backupPasswordTip": "Установите пароль для шифрования файлов резервных копий. Оставьте пустым, чтобы отключить шифрование.",
"backupPasswordWrong": "Неверный пароль резервной копии",
"backupTip": "Экспортированные данные могут быть зашифрованы паролем. \nПожалуйста, храните их в безопасности.",
"backupVersionNotMatch": "Версия резервной копии не совпадает, восстановление невозможно",
"battery": "Батарея", "battery": "Батарея",
"bgRun": "Работа в фоновом режиме", "bgRun": "Работа в фоновом режиме",
"bgRunTip": "Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».", "bgRunTip": "Этот переключатель означает, что программа будет пытаться работать в фоновом режиме, но фактическое выполнение зависит от того, включено ли разрешение. Для нативного Android отключите «Оптимизацию батареи» для этого приложения, для MIUI измените контроль активности на «Нет ограничений».",
@@ -180,6 +180,19 @@
"specifyDevTip": "Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.", "specifyDevTip": "Например, статистика сетевого трафика по умолчанию относится ко всем устройствам. Здесь вы можете указать конкретное устройство.",
"speed": "Скорость", "speed": "Скорость",
"spentTime": "Затрачено времени: {time}", "spentTime": "Затрачено времени: {time}",
"sshConfigAllExist": "Все серверы уже существуют (найдено {duplicateCount} дубликатов)",
"sshConfigDuplicatesSkipped": "{duplicateCount} дубликатов будут пропущены",
"sshConfigFound": "Мы нашли SSH-конфигурацию в вашей системе",
"sshConfigFoundServers": "Найдено {totalCount} серверов",
"sshConfigImport": "Импорт SSH Конфигурации",
"sshConfigImportHelp": "Можно импортировать только базовую информацию, например: IP/порт.",
"sshConfigImportPermission": "Хотите ли вы дать разрешение на чтение ~/.ssh/config и автоматический импорт настроек сервера?",
"sshConfigImportTip": "Предложение прочитать ~/.ssh/config при создании первого сервера",
"sshConfigImported": "Импортировано {count} серверов из SSH-конфигурации",
"sshConfigManualSelect": "Хотели бы вы вручную выбрать файл конфигурации SSH?",
"sshConfigNoServers": "Серверы не найдены в SSH-конфигурации",
"sshConfigPermissionDenied": "Невозможно получить доступ к файлу конфигурации SSH из-за разрешений macOS.",
"sshConfigServersToImport": "{importCount} серверов будут импортированы",
"sshTermHelp": "Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.", "sshTermHelp": "Когда терминал можно прокручивать, горизонтальное перетаскивание позволяет выделить текст. Нажатие на кнопку клавиатуры включает/выключает клавиатуру. Иконка файла открывает текущий путь SFTP. Кнопка буфера обмена копирует содержимое, когда текст выделен, и вставляет содержимое из буфера обмена в терминал, когда текст не выделен, а в буфере есть содержимое. Иконка кода вставляет фрагменты кода в терминал и выполняет их.",
"sshTip": "Эта функция находится в стадии тестирования.\n\nПожалуйста, отправляйте отчеты о проблемах на {url} или присоединяйтесь к нашей разработке.", "sshTip": "Эта функция находится в стадии тестирования.\n\nПожалуйста, отправляйте отчеты о проблемах на {url} или присоединяйтесь к нашей разработке.",
"sshVirtualKeyAutoOff": "Автоматическое переключение виртуальных клавиш", "sshVirtualKeyAutoOff": "Автоматическое переключение виртуальных клавиш",

View File

@@ -11,15 +11,15 @@
"autoConnect": "Otomatik bağlan", "autoConnect": "Otomatik bağlan",
"autoRun": "Otomatik çalıştır", "autoRun": "Otomatik çalıştır",
"autoUpdateHomeWidget": "Ana ekran bileşenini otomatik güncelle", "autoUpdateHomeWidget": "Ana ekran bileşenini otomatik güncelle",
"backupTip": "Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.",
"backupVersionNotMatch": "Yedekleme sürümü eşleşmiyor.",
"backupPassword": "Yedekleme parolası",
"backupPasswordTip": "Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.",
"backupPasswordWrong": "Yanlış yedekleme parolası",
"backupEncrypted": "Yedekleme şifrelenmiş", "backupEncrypted": "Yedekleme şifrelenmiş",
"backupNotEncrypted": "Yedekleme şifreli değil", "backupNotEncrypted": "Yedekleme şifreli değil",
"backupPasswordSet": "Yedekleme parolası ayarlandı", "backupPassword": "Yedekleme parolası",
"backupPasswordRemoved": "Yedekleme parolası kaldırıldı", "backupPasswordRemoved": "Yedekleme parolası kaldırıldı",
"backupPasswordSet": "Yedekleme parolası ayarlandı",
"backupPasswordTip": "Yedekleme dosyalarını şifrelemek için bir parola belirleyin. Şifrelemeyi devre dışı bırakmak için boş bırakın.",
"backupPasswordWrong": "Yanlış yedekleme parolası",
"backupTip": "Dışa aktarılan veriler parola ile şifrelenebilir. \nLütfen güvenli bir şekilde saklayın.",
"backupVersionNotMatch": "Yedekleme sürümü eşleşmiyor.",
"battery": "Pil", "battery": "Pil",
"bgRun": "Arka planda çalıştır", "bgRun": "Arka planda çalıştır",
"bgRunTip": "Bu anahtar yalnızca programın arka planda çalışmayı deneyeceği anlamına gelir. Arka planda çalışıp çalışamayacağı, iznin etkinleştirilip etkinleştirilmediğine bağlıdır. AOSP tabanlı Android ROM'lar için lütfen bu uygulamada \"Pil Optimizasyonu\"nu devre dışı bırakın. MIUI / HyperOS için lütfen güç tasarrufu politikasını \"Sınırsız\" olarak değiştirin.", "bgRunTip": "Bu anahtar yalnızca programın arka planda çalışmayı deneyeceği anlamına gelir. Arka planda çalışıp çalışamayacağı, iznin etkinleştirilip etkinleştirilmediğine bağlıdır. AOSP tabanlı Android ROM'lar için lütfen bu uygulamada \"Pil Optimizasyonu\"nu devre dışı bırakın. MIUI / HyperOS için lütfen güç tasarrufu politikasını \"Sınırsız\" olarak değiştirin.",
@@ -180,6 +180,19 @@
"specifyDevTip": "Örneğin, ağ trafiği istatistikleri varsayılan olarak tüm cihazlar içindir. Burada belirli bir cihaz belirtebilirsiniz.", "specifyDevTip": "Örneğin, ağ trafiği istatistikleri varsayılan olarak tüm cihazlar içindir. Burada belirli bir cihaz belirtebilirsiniz.",
"speed": "Hız", "speed": "Hız",
"spentTime": "Harcanan süre: {time}", "spentTime": "Harcanan süre: {time}",
"sshConfigAllExist": "Tüm sunucular zaten mevcut ({duplicateCount} kopya bulundu)",
"sshConfigDuplicatesSkipped": "{duplicateCount} kopya atlanacak",
"sshConfigFound": "Sisteminizde SSH yapılandırması bulduk",
"sshConfigFoundServers": "{totalCount} sunucu bulundu",
"sshConfigImport": "SSH Yapılandırma İçe Aktarma",
"sshConfigImportHelp": "Yalnızca temel bilgiler içe aktarılabilir, örneğin: IP/Port.",
"sshConfigImportPermission": "~/.ssh/config dosyasını okumak ve sunucu ayarlarını otomatik olarak içe aktarmak için izin vermek ister misiniz?",
"sshConfigImportTip": "İlk sunucu oluşturulurken ~/.ssh/config okuma istemi",
"sshConfigImported": "SSH yapılandırmasından {count} sunucu içe aktarıldı",
"sshConfigManualSelect": "SSH yapılandırma dosyasını manuel olarak seçmek ister misiniz?",
"sshConfigNoServers": "SSH yapılandırmasında sunucu bulunamadı",
"sshConfigPermissionDenied": "macOS izinleri nedeniyle SSH yapılandırma dosyasına erişilemiyor.",
"sshConfigServersToImport": "{importCount} sunucu içe aktarılacak",
"sshTermHelp": "Terminal kaydırılabilir olduğunda, yatay olarak sürüklemek metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapar. Dosya simgesi mevcut yolu SFTP'de açar. Pano düğmesi, metin seçiliyken içeriği kopyalar ve metin seçili değilken panoda içerik varsa terminale yapıştırır. Kod simgesi, kod parçacıklarını terminale yapıştırır ve yürütür.", "sshTermHelp": "Terminal kaydırılabilir olduğunda, yatay olarak sürüklemek metni seçebilir. Klavye düğmesine tıklamak klavyeyi açar/kapar. Dosya simgesi mevcut yolu SFTP'de açar. Pano düğmesi, metin seçiliyken içeriği kopyalar ve metin seçili değilken panoda içerik varsa terminale yapıştırır. Kod simgesi, kod parçacıklarını terminale yapıştırır ve yürütür.",
"sshTip": "Bu işlev şu anda deneysel aşamada.\n\nLütfen hataları {url} adresinde bildirin veya geliştirmemize katılın.", "sshTip": "Bu işlev şu anda deneysel aşamada.\n\nLütfen hataları {url} adresinde bildirin veya geliştirmemize katılın.",
"sshVirtualKeyAutoOff": "Sanal tuşların otomatik geçişi", "sshVirtualKeyAutoOff": "Sanal tuşların otomatik geçişi",

View File

@@ -11,15 +11,15 @@
"autoConnect": "Авто підключення", "autoConnect": "Авто підключення",
"autoRun": "Авто запуск", "autoRun": "Авто запуск",
"autoUpdateHomeWidget": "Автоматичне оновлення віджетів на головному екрані", "autoUpdateHomeWidget": "Автоматичне оновлення віджетів на головному екрані",
"backupTip": "Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.",
"backupVersionNotMatch": "Версія резервного копіювання не збіглася.",
"backupPassword": "Пароль резервного копіювання",
"backupPasswordTip": "Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.",
"backupPasswordWrong": "Неправильний пароль резервного копіювання",
"backupEncrypted": "Резервна копія зашифрована", "backupEncrypted": "Резервна копія зашифрована",
"backupNotEncrypted": "Резервна копія не зашифрована", "backupNotEncrypted": "Резервна копія не зашифрована",
"backupPasswordSet": "Пароль резервного копіювання встановлено", "backupPassword": "Пароль резервного копіювання",
"backupPasswordRemoved": "Пароль резервного копіювання видалено", "backupPasswordRemoved": "Пароль резервного копіювання видалено",
"backupPasswordSet": "Пароль резервного копіювання встановлено",
"backupPasswordTip": "Встановіть пароль для шифрування файлів резервного копіювання. Залиште порожнім для відключення шифрування.",
"backupPasswordWrong": "Неправильний пароль резервного копіювання",
"backupTip": "Експортовані дані можуть бути зашифровані паролем. \nБудь ласка, зберігайте їх у безпеці.",
"backupVersionNotMatch": "Версія резервного копіювання не збіглася.",
"battery": "Акумулятор", "battery": "Акумулятор",
"bgRun": "Запуск у фоновому режимі", "bgRun": "Запуск у фоновому режимі",
"bgRunTip": "Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \"Оптимізацію акумулятора\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \"Нескінченна\".", "bgRunTip": "Цей перемикач лише вказує на те, що програма намагатиметься працювати у фоновому режимі. Чи може вона працювати у фоновому режимі, залежить від прав доступу. Для AOSP-орієнтованих Android ROM, будь ласка, вимкніть \"Оптимізацію акумулятора\" в цьому додатку. Для MIUI / HyperOS, будь ласка, змініть політику економії енергії на \"Нескінченна\".",
@@ -180,6 +180,19 @@
"specifyDevTip": "Наприклад, статистика мережевого трафіку за замовчуванням є для всіх пристроїв. Ви можете вказати певний пристрій тут.", "specifyDevTip": "Наприклад, статистика мережевого трафіку за замовчуванням є для всіх пристроїв. Ви можете вказати певний пристрій тут.",
"speed": "Швидкість", "speed": "Швидкість",
"spentTime": "Витрачений час: {time}", "spentTime": "Витрачений час: {time}",
"sshConfigAllExist": "Всі сервери вже існують (знайдено {duplicateCount} дублікатів)",
"sshConfigDuplicatesSkipped": "{duplicateCount} дублікатів буде пропущено",
"sshConfigFound": "Ми знайшли SSH-конфігурацію у вашій системі",
"sshConfigFoundServers": "Знайдено {totalCount} серверів",
"sshConfigImport": "Імпорт SSH Конфігурації",
"sshConfigImportHelp": "Можна імпортувати лише базову інформацію, наприклад: IP/порт.",
"sshConfigImportPermission": "Чи хочете ви надати дозвіл на читання ~/.ssh/config та автоматичний імпорт налаштувань сервера?",
"sshConfigImportTip": "Пропозиція прочитати ~/.ssh/config при створенні першого сервера",
"sshConfigImported": "Імпортовано {count} серверів з SSH-конфігурації",
"sshConfigManualSelect": "Чи хочете ви вручну вибрати файл конфігурації SSH?",
"sshConfigNoServers": "Сервери не знайдені в SSH-конфігурації",
"sshConfigPermissionDenied": "Неможливо отримати доступ до файлу конфігурації SSH через дозволи macOS.",
"sshConfigServersToImport": "{importCount} серверів буде імпортовано",
"sshTermHelp": "Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.", "sshTermHelp": "Коли термінал прокрутний, горизонтальне проведення вибирає текст. Натискання кнопки клавіатури вмикає/вимикає клавіатуру. Іконка файлу відкриває поточний шлях SFTP. Кнопка буфера обміну копіює вміст, коли текст вибрано, і вставляє вміст з буфера обміну в термінал, коли текст не вибрано і є вміст у буфері обміну. Іконка коду вставляє фрагменти коду в термінал і виконує їх.",
"sshTip": "Ця функція наразі в експериментальній стадії. Будь ласка, повідомте про помилки за адресою {url} або приєднуйтеся до нашої розробки.", "sshTip": "Ця функція наразі в експериментальній стадії. Будь ласка, повідомте про помилки за адресою {url} або приєднуйтеся до нашої розробки.",
"sshVirtualKeyAutoOff": "Автоматичне переключення віртуальних клавіш", "sshVirtualKeyAutoOff": "Автоматичне переключення віртуальних клавіш",

View File

@@ -11,15 +11,15 @@
"autoConnect": "自动连接", "autoConnect": "自动连接",
"autoRun": "自动运行", "autoRun": "自动运行",
"autoUpdateHomeWidget": "自动更新桌面小部件", "autoUpdateHomeWidget": "自动更新桌面小部件",
"backupTip": "导出数据可通过密码加密,请妥善保管。",
"backupVersionNotMatch": "备份版本不兼容,无法恢复",
"backupPassword": "备份密码",
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
"backupPasswordWrong": "备份密码错误",
"backupEncrypted": "备份已加密", "backupEncrypted": "备份已加密",
"backupNotEncrypted": "备份未加密", "backupNotEncrypted": "备份未加密",
"backupPasswordSet": "备份密码已设置", "backupPassword": "备份密码",
"backupPasswordRemoved": "备份密码已移除", "backupPasswordRemoved": "备份密码已移除",
"backupPasswordSet": "备份密码已设置",
"backupPasswordTip": "设置密码以加密备份文件。留空则禁用加密。",
"backupPasswordWrong": "备份密码错误",
"backupTip": "导出数据可通过密码加密,请妥善保管。",
"backupVersionNotMatch": "备份版本不兼容,无法恢复",
"battery": "电池", "battery": "电池",
"bgRun": "后台运行", "bgRun": "后台运行",
"bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”MIUI / HyperOS 请将省电策略改为“无限制”。", "bgRunTip": "此开关只代表程序会尝试在后台运行,具体能否后台运行取决于是否开启了权限。原生 Android 请关闭本 App 的“电池优化”MIUI / HyperOS 请将省电策略改为“无限制”。",
@@ -180,6 +180,19 @@
"specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备", "specifyDevTip": "例如网络流量统计默认是所有设备,你可以在这里指定特定的设备",
"speed": "速度", "speed": "速度",
"spentTime": "耗时:{time}", "spentTime": "耗时:{time}",
"sshConfigAllExist": "所有服务器已存在(发现 {duplicateCount} 个重复项)",
"sshConfigDuplicatesSkipped": "{duplicateCount} 个重复项将被跳过",
"sshConfigFound": "我们在您的系统中发现了 SSH 配置。",
"sshConfigFoundServers": "发现 {totalCount} 个服务器",
"sshConfigImport": "SSH 配置导入",
"sshConfigImportHelp": "只能导入基础信息例如IP/端口",
"sshConfigImportPermission": "是否允许读取 ~/.ssh/config 并自动导入服务器设置?",
"sshConfigImportTip": "首次创建服务器时提示读取 ~/.ssh/config",
"sshConfigImported": "从 SSH 配置导入了 {count} 个服务器",
"sshConfigManualSelect": "是否要手动选择 SSH 配置文件?",
"sshConfigNoServers": "SSH 配置中未找到服务器",
"sshConfigPermissionDenied": "由于 macOS 权限限制,无法访问 SSH 配置文件。",
"sshConfigServersToImport": "{importCount} 个服务器将被导入",
"sshTermHelp": "在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。", "sshTermHelp": "在终端可滚动时,横向拖动可以选中文字。点击键盘按钮可以开启/关闭键盘。文件图标会打开当前路径 SFTP。剪切板按钮会在有选中文字时复制内容在未选中并且剪切板有内容时粘贴内容到终端。代码图标会粘贴代码片段到终端并执行。",
"sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。", "sshTip": "该功能目前处于测试阶段。\n\n请在 {url} 反馈问题,或者加入我们开发。",
"sshVirtualKeyAutoOff": "虚拟按键自动切换", "sshVirtualKeyAutoOff": "虚拟按键自动切换",

View File

@@ -11,15 +11,15 @@
"autoConnect": "自動連線", "autoConnect": "自動連線",
"autoRun": "自動執行", "autoRun": "自動執行",
"autoUpdateHomeWidget": "自動更新桌面小工具", "autoUpdateHomeWidget": "自動更新桌面小工具",
"backupTip": "匯出的資料可透過密碼加密,請妥善保管。",
"backupVersionNotMatch": "備份版本不相容,無法還原",
"backupPassword": "備份密碼",
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
"backupPasswordWrong": "備份密碼錯誤",
"backupEncrypted": "備份已加密", "backupEncrypted": "備份已加密",
"backupNotEncrypted": "備份未加密", "backupNotEncrypted": "備份未加密",
"backupPasswordSet": "備份密碼已設定", "backupPassword": "備份密碼",
"backupPasswordRemoved": "備份密碼已移除", "backupPasswordRemoved": "備份密碼已移除",
"backupPasswordSet": "備份密碼已設定",
"backupPasswordTip": "設定密碼來加密備份檔案。留空則停用加密。",
"backupPasswordWrong": "備份密碼錯誤",
"backupTip": "匯出的資料可透過密碼加密,請妥善保管。",
"backupVersionNotMatch": "備份版本不相容,無法還原",
"battery": "電池", "battery": "電池",
"bgRun": "背景執行", "bgRun": "背景執行",
"bgRunTip": "此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。", "bgRunTip": "此開關僅代表程式會嘗試於背景執行,能否成功取決於系統權限。在原生 Android 上,請關閉本應用的「電池最佳化」;在 MIUI / HyperOS 上,請將省電策略調整為「無限制」。",
@@ -180,6 +180,19 @@
"specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。", "specifyDevTip": "例如網路流量統計預設是所有裝置,你可以在這裡指定特定的裝置。",
"speed": "速度", "speed": "速度",
"spentTime": "耗時:{time}", "spentTime": "耗時:{time}",
"sshConfigAllExist": "所有伺服器均已存在(發現{duplicateCount}個重複項)",
"sshConfigDuplicatesSkipped": "將跳過{duplicateCount}個重複項",
"sshConfigFound": "我們在您的系統中發現了SSH設定",
"sshConfigFoundServers": "發現{totalCount}個伺服器",
"sshConfigImport": "匯入SSH設定",
"sshConfigImportHelp": "只能匯入基礎資訊例如IP/端口。",
"sshConfigImportPermission": "您是否希望允許讀取 ~/.ssh/config 並自動匯入伺服器設定?",
"sshConfigImportTip": "在建立第一個伺服器時提示讀取 ~/.ssh/config",
"sshConfigImported": "已從SSH設定匯入{count}個伺服器",
"sshConfigManualSelect": "是否要手動選擇 SSH 設定檔案?",
"sshConfigNoServers": "SSH設定中未找到伺服器",
"sshConfigPermissionDenied": "由於 macOS 權限限制,無法存取 SSH 設定檔案。",
"sshConfigServersToImport": "將匯入{importCount}個伺服器",
"sshTermHelp": "在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。", "sshTermHelp": "在終端機可捲動時,橫向拖動可以選中文字。點擊鍵盤按鈕可以開啟/關閉鍵盤。檔案圖示會打開目前路徑 SFTP。剪貼簿按鈕會在有選中文字時複製內容在未選中並且剪貼簿有內容時貼上內容到終端機。程式碼圖示會貼上程式碼片段到終端機並執行。",
"sshTip": "該功能目前處於測試階段。\n\n請在 {url} 回饋問題,或者加入我們開發。", "sshTip": "該功能目前處於測試階段。\n\n請在 {url} 回饋問題,或者加入我們開發。",
"sshVirtualKeyAutoOff": "虛擬按鍵自動切換", "sshVirtualKeyAutoOff": "虛擬按鍵自動切換",

View File

@@ -1,12 +1,16 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:io';
import 'package:choice/choice.dart'; import 'package:choice/choice.dart';
import 'package:file_picker/file_picker.dart';
import 'package:fl_lib/fl_lib.dart'; import 'package:fl_lib/fl_lib.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:icons_plus/icons_plus.dart'; import 'package:icons_plus/icons_plus.dart';
import 'package:server_box/core/extension/context/locale.dart'; import 'package:server_box/core/extension/context/locale.dart';
import 'package:server_box/core/route.dart'; import 'package:server_box/core/route.dart';
import 'package:server_box/core/utils/server_dedup.dart';
import 'package:server_box/core/utils/ssh_config.dart';
import 'package:server_box/data/model/app/scripts/cmd_types.dart'; import 'package:server_box/data/model/app/scripts/cmd_types.dart';
import 'package:server_box/data/model/server/custom.dart'; import 'package:server_box/data/model/server/custom.dart';
import 'package:server_box/data/model/server/server_private_info.dart'; import 'package:server_box/data/model/server/server_private_info.dart';
@@ -14,6 +18,7 @@ import 'package:server_box/data/model/server/system.dart';
import 'package:server_box/data/model/server/wol_cfg.dart'; import 'package:server_box/data/model/server/wol_cfg.dart';
import 'package:server_box/data/provider/private_key.dart'; import 'package:server_box/data/provider/private_key.dart';
import 'package:server_box/data/provider/server.dart'; import 'package:server_box/data/provider/server.dart';
import 'package:server_box/data/res/store.dart';
import 'package:server_box/data/store/server.dart'; import 'package:server_box/data/store/server.dart';
import 'package:server_box/view/page/private_key/edit.dart'; import 'package:server_box/view/page/private_key/edit.dart';
@@ -121,7 +126,7 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
} }
Widget _buildForm() { Widget _buildForm() {
final topItems = [_buildWriteScriptTip(), if (isMobile) _buildQrScan()]; final topItems = [_buildWriteScriptTip(), if (isMobile) _buildQrScan(), if (isDesktop) _buildSSHImport()];
final children = [ final children = [
Row(mainAxisAlignment: MainAxisAlignment.center, children: topItems.joinWith(UIs.width13).toList()), Row(mainAxisAlignment: MainAxisAlignment.center, children: topItems.joinWith(UIs.width13).toList()),
Input( Input(
@@ -235,27 +240,27 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
return ListTile( return ListTile(
contentPadding: const EdgeInsets.only(left: 10, right: 15), contentPadding: const EdgeInsets.only(left: 10, right: 15),
leading: Radio<int>(value: index), leading: Radio<int>(value: index),
title: Text(e.id, textAlign: TextAlign.start), title: Text(e.id, textAlign: TextAlign.start),
subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey), subtitle: Text(e.type ?? l10n.unknown, textAlign: TextAlign.start, style: UIs.textGrey),
trailing: Btn.icon( trailing: Btn.icon(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit),
onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: e)), onTap: () => PrivateKeyEditPage.route.go(context, args: PrivateKeyEditPageArgs(pki: e)),
),
onTap: () => _keyIdx.value = index,
);
});
tiles.add(
ListTile(
title: Text(libL10n.add),
contentPadding: const EdgeInsets.only(left: 23, right: 23),
trailing: const Icon(Icons.add),
onTap: () => PrivateKeyEditPage.route.go(context),
), ),
onTap: () => _keyIdx.value = index,
); );
return RadioGroup<int>( });
onChanged: (val) => _keyIdx.value = val, tiles.add(
child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx, ListTile(
); title: Text(libL10n.add),
contentPadding: const EdgeInsets.only(left: 23, right: 23),
trailing: const Icon(Icons.add),
onTap: () => PrivateKeyEditPage.route.go(context),
),
);
return RadioGroup<int>(
onChanged: (val) => _keyIdx.value = val,
child: _keyIdx.listenVal((_) => Column(children: tiles)).cardx,
);
} }
Widget _buildEnvs() { Widget _buildEnvs() {
@@ -486,7 +491,10 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
Widget _buildJumpServer() { Widget _buildJumpServer() {
const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7); const padding = EdgeInsets.only(left: 13, right: 13, bottom: 7);
final srvs = ref.watch(serverNotifierProvider).servers.values final srvs = ref
.watch(serverNotifierProvider)
.servers
.values
.where((e) => e.jumpId == null) .where((e) => e.jumpId == null)
.where((e) => e.id != spi?.id) .where((e) => e.id != spi?.id)
.toList(); .toList();
@@ -560,6 +568,16 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
); );
} }
Widget _buildSSHImport() {
return Btn.tile(
text: l10n.sshConfigImport,
icon: const Icon(Icons.settings, color: Colors.grey),
onTap: _onTapSSHImport,
textStyle: UIs.textGrey,
mainAxisSize: MainAxisSize.min,
);
}
Widget _buildDelBtn() { Widget _buildDelBtn() {
return IconButton( return IconButton(
onPressed: () { onPressed: () {
@@ -584,11 +602,115 @@ class _ServerEditPageState extends ConsumerState<ServerEditPage> with AfterLayou
void afterFirstLayout(BuildContext context) { void afterFirstLayout(BuildContext context) {
if (spi != null) { if (spi != null) {
_initWithSpi(spi!); _initWithSpi(spi!);
} else {
// Only for new servers, check SSH config import on first time
_checkSSHConfigImport();
} }
} }
} }
extension on _ServerEditPageState { extension _Actions on _ServerEditPageState {
void _onTapSSHImport() async {
try {
final servers = await SSHConfig.parseConfig();
if (servers.isEmpty) {
context.showSnackBar(l10n.sshConfigNoServers);
return;
}
dprint('Parsed ${servers.length} servers from SSH config');
await _processSSHServers(servers);
dprint('Finished processing SSH config servers');
} catch (e, s) {
_handleImportSSHCfgPermissionIssue(e, s);
}
}
void _handleImportSSHCfgPermissionIssue(Object e, StackTrace s) async {
dprint('Error importing SSH config: $e');
// Check if it's a permission error and offer file picker as fallback
if (e is PathAccessException || e.toString().contains('Operation not permitted')) {
final useFilePicker = await context.showRoundDialog<bool>(
title: l10n.sshConfigImport,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.sshConfigPermissionDenied),
const SizedBox(height: 8),
Text(l10n.sshConfigManualSelect),
],
),
actions: Btnx.cancelOk,
);
if (useFilePicker == true) {
await _onTapSSHImportWithFilePicker();
}
} else {
context.showErrDialog(e, s);
}
}
Future<void> _processSSHServers(List<Spi> servers) async {
final deduplicated = ServerDeduplication.deduplicateServers(servers);
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
final summary = ServerDeduplication.getImportSummary(servers, resolved);
if (!summary.hasItemsToImport) {
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
return;
}
final shouldImport = await context.showRoundDialog<bool>(
title: l10n.sshConfigImport,
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.sshConfigFoundServers('${summary.total}')),
if (summary.hasDuplicates)
Text(l10n.sshConfigDuplicatesSkipped('${summary.duplicates}'), style: UIs.textGrey),
Text(l10n.sshConfigServersToImport('${summary.toImport}')),
const SizedBox(height: 16),
...resolved.map((s) => Text('${s.name} (${s.user}@${s.ip}:${s.port})')),
],
),
),
actions: Btnx.cancelOk,
);
if (shouldImport == true) {
for (final server in resolved) {
ref.read(serverNotifierProvider.notifier).addServer(server);
}
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
}
}
Future<void> _onTapSSHImportWithFilePicker() async {
try {
final result = await FilePicker.platform.pickFiles(
type: FileType.any,
allowMultiple: false,
dialogTitle: 'SSH ${libL10n.select}',
);
if (result?.files.single.path case final path?) {
final servers = await SSHConfig.parseConfig(path);
if (servers.isEmpty) {
context.showSnackBar(l10n.sshConfigNoServers);
return;
}
await _processSSHServers(servers);
}
} catch (e, s) {
context.showErrDialog(e, s);
}
}
void _onTapCustomItem() async { void _onTapCustomItem() async {
final res = await KvEditor.route.go(context, KvEditorArgs(data: _customCmds.value)); final res = await KvEditor.route.go(context, KvEditorArgs(data: _customCmds.value));
if (res == null) return; if (res == null) return;
@@ -604,52 +726,6 @@ extension on _ServerEditPageState {
await _showCmdTypesDialog(allCmdTypes); await _showCmdTypesDialog(allCmdTypes);
} }
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
return context.showRoundDialog(
title: '${libL10n.disabled} ${l10n.cmd}',
child: SizedBox(
width: 270,
child: _disabledCmdTypes.listenVal((disabled) {
return ListView.builder(
itemCount: allCmdTypes.length,
itemExtent: 50,
itemBuilder: (context, index) {
final cmdType = allCmdTypes.elementAtOrNull(index);
if (cmdType == null) return UIs.placeholder;
final display = cmdType.displayName;
return ListTile(
leading: Icon(cmdType.sysType.icon, size: 20),
title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),
trailing: Checkbox(
value: disabled.contains(display),
onChanged: (value) {
if (value == null) return;
if (value) {
_disabledCmdTypes.value.add(display);
} else {
_disabledCmdTypes.value.remove(display);
}
_disabledCmdTypes.notify();
},
),
onTap: () {
final isDisabled = disabled.contains(display);
if (isDisabled) {
_disabledCmdTypes.value.remove(display);
} else {
_disabledCmdTypes.value.add(display);
}
_disabledCmdTypes.notify();
},
);
},
);
}),
),
actions: Btnx.oks,
);
}
void _onSave() async { void _onSave() async {
if (_ipController.text.isEmpty) { if (_ipController.text.isEmpty) {
context.showSnackBar('${libL10n.empty} ${l10n.host}'); context.showSnackBar('${libL10n.empty} ${l10n.host}');
@@ -705,7 +781,9 @@ extension on _ServerEditPageState {
port: int.parse(_portController.text), port: int.parse(_portController.text),
user: _usernameController.text, user: _usernameController.text,
pwd: _passwordController.text.selfNotEmptyOrNull, pwd: _passwordController.text.selfNotEmptyOrNull,
keyId: _keyIdx.value != null ? ref.read(privateKeyNotifierProvider).keys.elementAt(_keyIdx.value!).id : null, keyId: _keyIdx.value != null
? ref.read(privateKeyNotifierProvider).keys.elementAt(_keyIdx.value!).id
: null,
tags: _tags.value.isEmpty ? null : _tags.value.toList(), tags: _tags.value.isEmpty ? null : _tags.value.toList(),
alterUrl: _altUrlController.text.selfNotEmptyOrNull, alterUrl: _altUrlController.text.selfNotEmptyOrNull,
autoConnect: _autoConnect.value, autoConnect: _autoConnect.value,
@@ -731,6 +809,111 @@ extension on _ServerEditPageState {
context.pop(); context.pop();
} }
}
extension _Utils on _ServerEditPageState {
void _checkSSHConfigImport() async {
final prop = Stores.setting.firstTimeReadSSHCfg;
// Only check if it's first time and user hasn't disabled it
if (!prop.fetch()) return;
try {
// Check if SSH config exists
final (_, configExists) = SSHConfig.configExists();
if (!configExists) return;
// Ask for permission
final hasPermission = await context.showRoundDialog<bool>(
title: l10n.sshConfigImport,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(l10n.sshConfigFound),
UIs.height7,
Text(l10n.sshConfigImportPermission),
UIs.height7,
Text(l10n.sshConfigImportHelp, style: UIs.textGrey),
],
),
actions: Btnx.cancelOk,
);
prop.put(false);
if (hasPermission == true) {
// Parse and import SSH config
final servers = await SSHConfig.parseConfig();
if (servers.isEmpty) {
context.showSnackBar(l10n.sshConfigNoServers);
return;
}
final deduplicated = ServerDeduplication.deduplicateServers(servers);
final resolved = ServerDeduplication.resolveNameConflicts(deduplicated);
final summary = ServerDeduplication.getImportSummary(servers, resolved);
if (!summary.hasItemsToImport) {
context.showSnackBar(l10n.sshConfigAllExist('${summary.duplicates}'));
return;
}
// Import without asking again since user already gave permission
for (final server in resolved) {
ref.read(serverNotifierProvider.notifier).addServer(server);
}
context.showSnackBar(l10n.sshConfigImported('${resolved.length}'));
}
} catch (e, s) {
_handleImportSSHCfgPermissionIssue(e, s);
}
}
Future<void> _showCmdTypesDialog(Set<ShellCmdType> allCmdTypes) {
return context.showRoundDialog(
title: '${libL10n.disabled} ${l10n.cmd}',
child: SizedBox(
width: 270,
child: _disabledCmdTypes.listenVal((disabled) {
return ListView.builder(
itemCount: allCmdTypes.length,
itemExtent: 50,
itemBuilder: (context, index) {
final cmdType = allCmdTypes.elementAtOrNull(index);
if (cmdType == null) return UIs.placeholder;
final display = cmdType.displayName;
return ListTile(
leading: Icon(cmdType.sysType.icon, size: 20),
title: Text(cmdType.name, style: const TextStyle(fontSize: 16)),
trailing: Checkbox(
value: disabled.contains(display),
onChanged: (value) {
if (value == null) return;
if (value) {
_disabledCmdTypes.value.add(display);
} else {
_disabledCmdTypes.value.remove(display);
}
_disabledCmdTypes.notify();
},
),
onTap: () {
final isDisabled = disabled.contains(display);
if (isDisabled) {
_disabledCmdTypes.value.remove(display);
} else {
_disabledCmdTypes.value.add(display);
}
_disabledCmdTypes.notify();
},
);
},
);
}),
),
actions: Btnx.oks,
);
}
void _initWithSpi(Spi spi) { void _initWithSpi(Spi spi) {
_nameController.text = spi.name; _nameController.text = spi.name;

View File

@@ -180,6 +180,7 @@ extension _Server on _AppSettingsPageState {
_buildDoubleColumnServersPage(), _buildDoubleColumnServersPage(),
_buildUpdateInterval(), _buildUpdateInterval(),
_buildMaxRetry(), _buildMaxRetry(),
_buildSSHConfigImport(),
], ],
); );
} }
@@ -306,4 +307,12 @@ extension _Server on _AppSettingsPageState {
trailing: StoreSwitch(prop: Stores.setting.serverTabPreferDiskAmount), trailing: StoreSwitch(prop: Stores.setting.serverTabPreferDiskAmount),
); );
} }
Widget _buildSSHConfigImport() {
return ListTile(
title: Text(l10n.sshConfigImport),
subtitle: Text(l10n.sshConfigImportTip, style: UIs.textGrey),
trailing: StoreSwitch(prop: _setting.firstTimeReadSSHCfg),
);
}
} }

View File

@@ -26,6 +26,10 @@
<true/> <true/>
<key>com.apple.security.files.user-selected.read-write</key> <key>com.apple.security.files.user-selected.read-write</key>
<true/> <true/>
<key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
<array>
<string>.ssh/</string>
</array>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>
<array/> <array/>
</dict> </dict>

View File

@@ -32,5 +32,9 @@
<string>MainMenu</string> <string>MainMenu</string>
<key>NSPrincipalClass</key> <key>NSPrincipalClass</key>
<string>NSApplication</string> <string>NSApplication</string>
<key>NSFileProviderExtensionAccessControlPolicy</key>
<string>NSFileProviderExtensionAccessControlPolicyRequiresExplicitAccess</string>
<key>NSFileProviderExtensionUsageDescription</key>
<string>This app needs to access SSH configuration files to import server connection settings from ~/.ssh/config. This allows users to quickly add their existing SSH servers without manual configuration.</string>
</dict> </dict>
</plist> </plist>

View File

@@ -24,6 +24,10 @@
<true/> <true/>
<key>com.apple.security.files.user-selected.read-write</key> <key>com.apple.security.files.user-selected.read-write</key>
<true/> <true/>
<key>com.apple.security.temporary-exception.files.home-relative-path.read-only</key>
<array>
<string>.ssh/</string>
</array>
<key>keychain-access-groups</key> <key>keychain-access-groups</key>
<array/> <array/>
</dict> </dict>

View File

@@ -497,8 +497,8 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." path: "."
ref: "v1.0.343" ref: "v1.0.344"
resolved-ref: a65b7447ac2cc5c25e5a96dc559b1b67a40b2c82 resolved-ref: "9c7dd603b125fa3ca7a65d466c8fb41383997bd3"
url: "https://github.com/lppcg/fl_lib" url: "https://github.com/lppcg/fl_lib"
source: git source: git
version: "0.0.1" version: "0.0.1"

View File

@@ -63,7 +63,7 @@ dependencies:
fl_lib: fl_lib:
git: git:
url: https://github.com/lppcg/fl_lib url: https://github.com/lppcg/fl_lib
ref: v1.0.343 ref: v1.0.344
flutter_gbk2utf8: ^1.0.1 flutter_gbk2utf8: ^1.0.1
dependency_overrides: dependency_overrides:

414
test/server_dedup_test.dart Normal file
View File

@@ -0,0 +1,414 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/core/utils/server_dedup.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
// Mock functions to test the deduplication logic without relying on ServerStore
List<Spi> _mockDeduplicateServers(List<Spi> importedServers, List<Spi> existingServers) {
final deduplicated = <Spi>[];
for (final imported in importedServers) {
// Check against existing servers
if (!_mockIsDuplicate(imported, existingServers)) {
// Also check against already processed imported servers
if (!_mockIsDuplicate(imported, deduplicated)) {
deduplicated.add(imported);
}
}
}
return deduplicated;
}
bool _mockIsDuplicate(Spi imported, List<Spi> existing) {
for (final existingSpi in existing) {
// Check for exact match on ip:port@user combination
if (existingSpi.ip == imported.ip &&
existingSpi.port == imported.port &&
existingSpi.user == imported.user) {
return true;
}
}
return false;
}
List<Spi> _mockResolveNameConflicts(List<Spi> importedServers, List<String> existingNames) {
final existingNamesSet = existingNames.toSet();
final processedNames = <String>{};
final result = <Spi>[];
for (final server in importedServers) {
String newName = server.name;
int suffix = 2;
// Check against both existing servers and already processed servers
while (existingNamesSet.contains(newName) || processedNames.contains(newName)) {
newName = '${server.name} ($suffix)';
suffix++;
}
processedNames.add(newName);
if (newName != server.name) {
result.add(server.copyWith(name: newName));
} else {
result.add(server);
}
}
return result;
}
void main() {
group('ServerDeduplication Tests', () {
late List<Spi> existingServers;
late List<Spi> importedServers;
setUp(() {
// Set up some existing servers for testing
existingServers = [
const Spi(
name: 'production-web',
ip: '192.168.1.100',
port: 22,
user: 'root',
id: 'existing1',
),
const Spi(
name: 'staging-db',
ip: '192.168.1.200',
port: 22,
user: 'postgres',
id: 'existing2',
),
const Spi(
name: 'dev-server',
ip: '192.168.1.50',
port: 2222,
user: 'developer',
id: 'existing3',
),
];
});
test('deduplicateServers removes exact duplicates', () {
importedServers = [
const Spi(
name: 'new-server-1',
ip: '192.168.1.100',
port: 22,
user: 'root', // Same as existing1
),
const Spi(
name: 'new-server-2',
ip: '192.168.1.300',
port: 22,
user: 'admin', // New server
),
const Spi(
name: 'new-server-3',
ip: '192.168.1.200',
port: 22,
user: 'postgres', // Same as existing2
),
];
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
expect(deduplicated, hasLength(1));
expect(deduplicated.first.name, 'new-server-2');
expect(deduplicated.first.ip, '192.168.1.300');
});
test('deduplicateServers considers port and user in deduplication', () {
importedServers = [
const Spi(
name: 'same-ip-diff-port',
ip: '192.168.1.100',
port: 2222, // Different port
user: 'root',
),
const Spi(
name: 'same-ip-diff-user',
ip: '192.168.1.100',
port: 22,
user: 'admin', // Different user
),
const Spi(
name: 'exact-duplicate',
ip: '192.168.1.100',
port: 22,
user: 'root', // Exact duplicate
),
];
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
expect(deduplicated, hasLength(2));
expect(deduplicated.any((s) => s.name == 'same-ip-diff-port'), isTrue);
expect(deduplicated.any((s) => s.name == 'same-ip-diff-user'), isTrue);
expect(deduplicated.any((s) => s.name == 'exact-duplicate'), isFalse);
});
test('deduplicateServers handles empty existing servers list', () {
importedServers = [
const Spi(
name: 'server1',
ip: '192.168.1.100',
port: 22,
user: 'root',
),
const Spi(
name: 'server2',
ip: '192.168.1.200',
port: 22,
user: 'admin',
),
];
final deduplicated = _mockDeduplicateServers(importedServers, []);
expect(deduplicated, hasLength(2));
expect(deduplicated, equals(importedServers));
});
test('deduplicateServers handles empty imported servers list', () {
final deduplicated = _mockDeduplicateServers([], existingServers);
expect(deduplicated, isEmpty);
});
test('resolveNameConflicts appends numbers to conflicting names', () {
importedServers = [
const Spi(
name: 'production-web', // Conflicts with existing
ip: '192.168.1.300',
port: 22,
user: 'root',
),
const Spi(
name: 'dev-server', // Conflicts with existing
ip: '192.168.1.400',
port: 22,
user: 'root',
),
const Spi(
name: 'unique-name', // No conflict
ip: '192.168.1.500',
port: 22,
user: 'root',
),
];
final resolved = _mockResolveNameConflicts(
importedServers,
existingServers.map((s) => s.name).toList(),
);
expect(resolved, hasLength(3));
expect(resolved[0].name, 'production-web (2)');
expect(resolved[1].name, 'dev-server (2)');
expect(resolved[2].name, 'unique-name');
});
test('resolveNameConflicts handles multiple conflicts with same base name', () {
importedServers = [
const Spi(
name: 'server',
ip: '192.168.1.100',
port: 22,
user: 'root',
),
const Spi(
name: 'server',
ip: '192.168.1.200',
port: 22,
user: 'admin',
),
const Spi(
name: 'server',
ip: '192.168.1.300',
port: 2222,
user: 'root',
),
];
final existingNames = ['server', 'server (2)'];
final resolved = _mockResolveNameConflicts(importedServers, existingNames);
expect(resolved, hasLength(3));
expect(resolved[0].name, 'server (3)');
expect(resolved[1].name, 'server (4)');
expect(resolved[2].name, 'server (5)');
});
test('resolveNameConflicts handles empty input', () {
final resolved = _mockResolveNameConflicts([], ['existing1', 'existing2']);
expect(resolved, isEmpty);
});
test('getImportSummary calculates correct statistics', () {
final originalList = [
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
const Spi(name: 'server3', ip: '192.168.1.300', port: 22, user: 'root'),
const Spi(name: 'duplicate', ip: '192.168.1.100', port: 22, user: 'root'), // Duplicate of server1
];
final deduplicatedList = [
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
const Spi(name: 'server3', ip: '192.168.1.300', port: 22, user: 'root'),
];
final summary = ServerDeduplication.getImportSummary(
originalList,
deduplicatedList,
);
expect(summary.total, 4);
expect(summary.duplicates, 2); // server1 and duplicate
expect(summary.toImport, 2);
});
test('getImportSummary handles case with no duplicates', () {
final originalList = [
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
];
final summary = ServerDeduplication.getImportSummary(
originalList,
originalList,
);
expect(summary.total, 2);
expect(summary.duplicates, 0);
expect(summary.toImport, 2);
});
test('getImportSummary handles case with all duplicates', () {
final originalList = [
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
];
final summary = ServerDeduplication.getImportSummary(
originalList,
[],
);
expect(summary.total, 2);
expect(summary.duplicates, 2);
expect(summary.toImport, 0);
});
test('getImportSummary handles empty lists', () {
final summary = ServerDeduplication.getImportSummary([], []);
expect(summary.total, 0);
expect(summary.duplicates, 0);
expect(summary.toImport, 0);
});
test('complete deduplication workflow', () {
// Simulate a complete import workflow
importedServers = [
const Spi(
name: 'production-web', // Name conflicts with existing
ip: '192.168.1.400', // Different IP, so not a duplicate
port: 22,
user: 'root',
),
const Spi(
name: 'new-staging',
ip: '192.168.1.100', // Same as existing1, should be removed
port: 22,
user: 'root',
),
const Spi(
name: 'unique-server',
ip: '192.168.1.500', // Unique server
port: 22,
user: 'admin',
),
];
// Step 1: Remove duplicates
final deduplicated = _mockDeduplicateServers(importedServers, existingServers);
expect(deduplicated, hasLength(2)); // new-staging should be removed
// Step 2: Resolve name conflicts
final resolved = _mockResolveNameConflicts(
deduplicated,
existingServers.map((s) => s.name).toList(),
);
expect(resolved, hasLength(2));
expect(resolved.any((s) => s.name == 'production-web (2)'), isTrue);
expect(resolved.any((s) => s.name == 'unique-server'), isTrue);
// Step 3: Get summary
final summary = ServerDeduplication.getImportSummary(
importedServers,
resolved,
);
expect(summary.total, 3);
expect(summary.duplicates, 1);
expect(summary.toImport, 2);
});
test('deduplication key generation is consistent', () {
const server1 = Spi(
name: 'test1',
ip: '192.168.1.100',
port: 22,
user: 'root',
);
const server2 = Spi(
name: 'test2', // Different name
ip: '192.168.1.100', // Same IP
port: 22, // Same port
user: 'root', // Same user
);
final servers = [server1, server2];
final deduplicated = _mockDeduplicateServers(servers, []);
// First server should be kept, second should be removed since it has same ip:port@user
expect(deduplicated, hasLength(1));
expect(deduplicated.first.name, 'test1');
});
test('ImportSummary properties work correctly', () {
final summary = ServerDeduplication.getImportSummary(
[
const Spi(name: 'server1', ip: '192.168.1.100', port: 22, user: 'root'),
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
],
[
const Spi(name: 'server2', ip: '192.168.1.200', port: 22, user: 'admin'),
],
);
expect(summary.total, 2);
expect(summary.duplicates, 1);
expect(summary.toImport, 1);
expect(summary.hasDuplicates, isTrue);
expect(summary.hasItemsToImport, isTrue);
});
test('ImportSummary with no duplicates or imports', () {
final summary = ServerDeduplication.getImportSummary([], []);
expect(summary.total, 0);
expect(summary.duplicates, 0);
expect(summary.toImport, 0);
expect(summary.hasDuplicates, isFalse);
expect(summary.hasItemsToImport, isFalse);
});
});
}

View File

@@ -0,0 +1,317 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/data/model/server/server_private_info.dart';
void main() {
group('Server Edit Page Logic Tests', () {
test('SSH import should only be available on desktop platforms', () {
final desktopPlatforms = ['linux', 'macos', 'windows'];
final mobilePlatforms = ['android', 'ios', 'fuchsia'];
for (final platform in desktopPlatforms) {
final isDesktop = desktopPlatforms.contains(platform);
expect(isDesktop, isTrue, reason: '$platform should support SSH import');
}
for (final platform in mobilePlatforms) {
final isDesktop = desktopPlatforms.contains(platform);
expect(isDesktop, isFalse, reason: '$platform should not support SSH import');
}
});
test('permission prompt conditions are correct', () {
// Test the conditions for showing permission prompt
// Should prompt when: firstTimeReadSSHCfg=true, sshConfigExists=true, isNewServer=true
bool shouldPrompt(bool firstTime, bool configExists, bool isNew) {
return firstTime && configExists && isNew;
}
expect(shouldPrompt(true, true, true), isTrue); // All conditions met
expect(shouldPrompt(false, true, true), isFalse); // Setting disabled
expect(shouldPrompt(true, false, true), isFalse); // No config file
expect(shouldPrompt(true, true, false), isFalse); // Editing existing server
expect(shouldPrompt(false, false, false), isFalse); // No conditions met
});
test('server validation logic works correctly', () {
// Test server validation without actual form widgets
// Valid server
const validServer = Spi(
name: 'test-server',
ip: '192.168.1.100',
port: 22,
user: 'root',
);
expect(validServer.name.isNotEmpty, isTrue);
expect(validServer.ip.isNotEmpty, isTrue);
expect(validServer.port > 0 && validServer.port <= 65535, isTrue);
expect(validServer.user.isNotEmpty, isTrue);
// Invalid cases
expect(''.isNotEmpty, isFalse); // Empty name
expect(0 > 0, isFalse); // Invalid port
expect(65536 <= 65535, isFalse); // Port too high
});
test('server form data processing is correct', () {
// Test data processing logic
final Map<String, dynamic> formData = {
'name': 'my-server',
'ip': '192.168.1.100',
'port': '2222',
'user': 'admin',
};
// Process form data into server object
final server = Spi(
name: formData['name'] as String,
ip: formData['ip'] as String,
port: int.parse(formData['port'] as String),
user: formData['user'] as String,
);
expect(server.name, 'my-server');
expect(server.ip, '192.168.1.100');
expect(server.port, 2222);
expect(server.user, 'admin');
});
test('SSH key handling is correct', () {
// Test SSH key field handling
const serverWithKey = Spi(
name: 'key-server',
ip: '192.168.1.100',
port: 22,
user: 'root',
keyId: '~/.ssh/id_rsa',
);
expect(serverWithKey.keyId, '~/.ssh/id_rsa');
expect(serverWithKey.keyId?.isNotEmpty, isTrue);
const serverWithoutKey = Spi(
name: 'pwd-server',
ip: '192.168.1.100',
port: 22,
user: 'root',
pwd: 'password123',
);
expect(serverWithoutKey.keyId, isNull);
expect(serverWithoutKey.pwd, 'password123');
});
test('server editing vs creation logic', () {
// Test logic for distinguishing between editing and creating servers
const existingServer = Spi(
name: 'existing',
ip: '192.168.1.100',
port: 22,
user: 'root',
id: 'server123',
);
// Existing server has non-empty ID
final isEditing = existingServer.id.isNotEmpty;
final isCreating = !isEditing;
expect(isEditing, isTrue);
expect(isCreating, isFalse);
const newServer = Spi(
name: 'new-server',
ip: '192.168.1.100',
port: 22,
user: 'root',
id: '',
);
final isCreatingNew = newServer.id.isEmpty;
final isEditingExisting = !isCreatingNew;
expect(isCreatingNew, isTrue);
expect(isEditingExisting, isFalse);
});
test('form field population from imported server', () {
// Test that imported server data correctly populates form fields
const importedServer = Spi(
name: 'imported-prod-web',
ip: '10.0.1.100',
port: 2222,
user: 'deploy',
keyId: '~/.ssh/production.pem',
);
// Simulate form field population
final formFields = {
'name': importedServer.name,
'ip': importedServer.ip,
'port': importedServer.port.toString(),
'user': importedServer.user,
'keyId': importedServer.keyId,
};
expect(formFields['name'], 'imported-prod-web');
expect(formFields['ip'], '10.0.1.100');
expect(formFields['port'], '2222');
expect(formFields['user'], 'deploy');
expect(formFields['keyId'], '~/.ssh/production.pem');
});
test('import summary display logic', () {
// Test import summary formatting
const totalFound = 5;
const duplicatesRemoved = 2;
const serversToImport = 3;
final summary = {
'total': totalFound,
'duplicates': duplicatesRemoved,
'toImport': serversToImport,
};
expect(summary['total'], 5);
expect(summary['duplicates'], 2);
expect(summary['toImport'], 3);
// Summary validation
expect(summary['duplicates']! + summary['toImport']!, summary['total']);
// Format summary message (simplified)
final message = 'Found ${summary['total']} servers, '
'${summary['duplicates']} duplicates removed, '
'${summary['toImport']} will be imported.';
expect(message, 'Found 5 servers, 2 duplicates removed, 3 will be imported.');
});
test('error handling logic', () {
// Test error handling scenarios
final errors = <String>[];
// Validation errors
void validateServer(Spi server) {
if (server.name.isEmpty) {
errors.add('Server name is required');
}
if (server.ip.isEmpty) {
errors.add('Server IP is required');
}
if (server.port <= 0 || server.port > 65535) {
errors.add('Port must be between 1 and 65535');
}
if (server.user.isEmpty) {
errors.add('Username is required');
}
}
// Test with invalid server
const invalidServer = Spi(
name: '',
ip: '',
port: 0,
user: '',
);
validateServer(invalidServer);
expect(errors.length, 4);
expect(errors.contains('Server name is required'), isTrue);
expect(errors.contains('Server IP is required'), isTrue);
expect(errors.contains('Port must be between 1 and 65535'), isTrue);
expect(errors.contains('Username is required'), isTrue);
// Test with valid server
errors.clear();
const validServer = Spi(
name: 'valid',
ip: '192.168.1.1',
port: 22,
user: 'root',
);
validateServer(validServer);
expect(errors.isEmpty, isTrue);
});
test('name conflict resolution logic', () {
// Test name conflict resolution during import
final existingNames = ['server1', 'server2', 'server3'];
String resolveNameConflict(String proposedName, List<String> existing) {
if (!existing.contains(proposedName)) {
return proposedName;
}
int suffix = 2;
String newName;
do {
newName = '$proposedName ($suffix)';
suffix++;
} while (existing.contains(newName));
return newName;
}
// Test with no conflict
expect(resolveNameConflict('unique-name', existingNames), 'unique-name');
// Test with conflict
expect(resolveNameConflict('server1', existingNames), 'server1 (2)');
// Test with multiple conflicts
final extendedNames = [...existingNames, 'server1 (2)'];
expect(resolveNameConflict('server1', extendedNames), 'server1 (3)');
});
test('SSH config import button visibility logic', () {
// Test when SSH import button should be visible
bool shouldShowSSHImport({
required bool isDesktop,
required bool firstTimeReadSSHCfg,
required bool isNewServer,
}) {
return isDesktop && (firstTimeReadSSHCfg || !isNewServer);
}
// Desktop, first time, new server - should show
expect(shouldShowSSHImport(
isDesktop: true,
firstTimeReadSSHCfg: true,
isNewServer: true,
), isTrue);
// Desktop, not first time, new server - should not show auto import but manual import available
expect(shouldShowSSHImport(
isDesktop: true,
firstTimeReadSSHCfg: false,
isNewServer: true,
), isFalse);
// Desktop, editing existing server - should show manual import
expect(shouldShowSSHImport(
isDesktop: true,
firstTimeReadSSHCfg: false,
isNewServer: false,
), isTrue);
// Mobile - should never show
expect(shouldShowSSHImport(
isDesktop: false,
firstTimeReadSSHCfg: true,
isNewServer: true,
), isFalse);
});
});
}

355
test/ssh_config_test.dart Normal file
View File

@@ -0,0 +1,355 @@
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:server_box/core/utils/ssh_config.dart';
void main() {
group('SSHConfig Tests', () {
late Directory tempDir;
late File configFile;
setUp(() async {
// Create temporary directory for test SSH config files
tempDir = await Directory.systemTemp.createTemp('ssh_config_test');
configFile = File('${tempDir.path}/config');
});
tearDown(() async {
// Clean up temporary files
if (tempDir.existsSync()) {
await tempDir.delete(recursive: true);
}
});
test('configExists returns false for non-existent file', () async {
final (_, exists) = SSHConfig.configExists('/non/existent/path');
expect(exists, false);
});
test('configExists returns true for existing file', () async {
await configFile.writeAsString('Host example\n HostName example.com\n');
final (_, exists) = SSHConfig.configExists(configFile.path);
expect(exists, true);
});
test('parseConfig handles empty file', () async {
await configFile.writeAsString('');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, isEmpty);
});
test('parseConfig handles file with only comments', () async {
await configFile.writeAsString('''
# This is a comment
# Another comment
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, isEmpty);
});
test('parseConfig parses single host correctly', () async {
await configFile.writeAsString('''
Host myserver
HostName 192.168.1.100
User admin
Port 2222
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'myserver');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin');
expect(server.port, 2222);
});
test('parseConfig handles missing HostName', () async {
await configFile.writeAsString('''
Host myserver
User admin
Port 2222
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, isEmpty); // Should skip hosts without HostName
});
test('parseConfig uses defaults for missing optional fields', () async {
await configFile.writeAsString('''
Host simple
HostName example.com
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'simple');
expect(server.ip, 'example.com');
expect(server.user, 'root'); // default user
expect(server.port, 22); // default port
});
test('parseConfig handles multiple hosts', () async {
await configFile.writeAsString('''
Host server1
HostName 192.168.1.100
User alice
Port 22
Host server2
HostName 192.168.1.200
User bob
Port 2222
Host server3
HostName example.com
User charlie
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(3));
expect(servers[0].name, 'server1');
expect(servers[0].ip, '192.168.1.100');
expect(servers[0].user, 'alice');
expect(servers[0].port, 22);
expect(servers[1].name, 'server2');
expect(servers[1].ip, '192.168.1.200');
expect(servers[1].user, 'bob');
expect(servers[1].port, 2222);
expect(servers[2].name, 'server3');
expect(servers[2].ip, 'example.com');
expect(servers[2].user, 'charlie');
expect(servers[2].port, 22);
});
test('parseConfig handles case insensitive keywords', () async {
await configFile.writeAsString('''
host myserver
hostname 192.168.1.100
user admin
port 2222
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'myserver');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin');
expect(server.port, 2222);
});
test('parseConfig handles comments and empty lines', () async {
await configFile.writeAsString('''
# Global settings
Host *
ServerAliveInterval 60
# My development server
Host devserver
HostName 192.168.1.50
User developer # development user
Port 22
# Empty line below
Host prodserver
HostName 10.0.0.100
User production
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(2));
expect(servers[0].name, 'devserver');
expect(servers[0].ip, '192.168.1.50');
expect(servers[0].user, 'developer');
expect(servers[1].name, 'prodserver');
expect(servers[1].ip, '10.0.0.100');
expect(servers[1].user, 'production');
});
test('parseConfig handles wildcard hosts', () async {
await configFile.writeAsString('''
Host *
User defaultuser
Port 2222
Host myserver
HostName 192.168.1.100
User admin
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1)); // Only named hosts, not wildcards
final server = servers.first;
expect(server.name, 'myserver');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin');
expect(server.port, 22); // Uses default, not wildcard setting
});
test('parseConfig handles IdentityFile', () async {
await configFile.writeAsString('''
Host keyserver
HostName 192.168.1.100
User admin
IdentityFile ~/.ssh/special_key
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.keyId, '~/.ssh/special_key');
});
test('parseConfig handles quoted values', () async {
await configFile.writeAsString('''
Host "server with spaces"
HostName "192.168.1.100"
User "admin user"
IdentityFile "~/.ssh/key with spaces"
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'server with spaces');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin user');
expect(server.keyId, '~/.ssh/key with spaces');
});
test('parseConfig handles invalid port values', () async {
await configFile.writeAsString('''
Host badport
HostName 192.168.1.100
Port notanumber
Host goodserver
HostName 192.168.1.200
Port 2222
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(2));
// First server should use default port due to invalid port
expect(servers[0].name, 'badport');
expect(servers[0].port, 22); // default port
// Second server should use specified port
expect(servers[1].name, 'goodserver');
expect(servers[1].port, 2222);
});
test('parseConfig skips hosts with multiple host patterns', () async {
await configFile.writeAsString('''
Host server1 server2
HostName 192.168.1.100
Host singleserver
HostName 192.168.1.200
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1)); // Only single host patterns
expect(servers[0].name, 'singleserver');
});
test('parseConfig handles ProxyJump (ignored)', () async {
await configFile.writeAsString('''
Host jumpserver
HostName 192.168.1.100
User admin
ProxyJump bastion.example.com
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(1));
final server = servers.first;
expect(server.name, 'jumpserver');
expect(server.ip, '192.168.1.100');
expect(server.user, 'admin');
// ProxyJump is ignored in current implementation
});
test('parseConfig returns empty list for non-existent file', () async {
final servers = await SSHConfig.parseConfig('/non/existent/path');
expect(servers, isEmpty);
});
test('parseConfig handles real-world SSH config example', () async {
await configFile.writeAsString('''
# Default settings for all hosts
Host *
ServerAliveInterval 60
ServerAliveCountMax 3
TCPKeepAlive yes
# Production servers
Host prod-web-01
HostName 10.0.1.100
User deploy
Port 22
IdentityFile ~/.ssh/production.pem
Host prod-db-01
HostName 10.0.1.200
User ubuntu
Port 2222
IdentityFile ~/.ssh/production.pem
# Development environment
Host dev
HostName dev.example.com
User developer
Port 22
# Jump host configuration
Host bastion
HostName bastion.example.com
User ec2-user
IdentityFile ~/.ssh/bastion.pem
Host internal-server
HostName 172.16.0.50
User admin
ProxyJump bastion
''');
final servers = await SSHConfig.parseConfig(configFile.path);
expect(servers, hasLength(5));
// Check specific servers
final prodWeb = servers.firstWhere((s) => s.name == 'prod-web-01');
expect(prodWeb.ip, '10.0.1.100');
expect(prodWeb.user, 'deploy');
expect(prodWeb.port, 22);
expect(prodWeb.keyId, '~/.ssh/production.pem');
final prodDb = servers.firstWhere((s) => s.name == 'prod-db-01');
expect(prodDb.ip, '10.0.1.200');
expect(prodDb.user, 'ubuntu');
expect(prodDb.port, 2222);
final dev = servers.firstWhere((s) => s.name == 'dev');
expect(dev.ip, 'dev.example.com');
expect(dev.user, 'developer');
expect(dev.port, 22);
expect(dev.keyId, isNull);
});
});
}