This commit is contained in:
Anurag Lint 2024-12-28 16:15:36 +01:00 committed by GitHub
commit 2b040e769c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 604 additions and 137 deletions

View file

@ -18,6 +18,8 @@ import com.sparrowwallet.hummingbird.registry.CryptoPSBT;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.i18n.Language;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.io.*;
import com.sparrowwallet.sparrow.io.bbqr.BBQR;
import com.sparrowwallet.sparrow.io.bbqr.BBQRType;
@ -138,6 +140,12 @@ public class AppController implements Initializable {
@FXML
private ToggleGroup theme;
@FXML
private ToggleGroup languages;
@FXML
private Menu languagesMenu;
@FXML
private CheckMenuItem openWalletsInNewWindows;
private static final BooleanProperty openWalletsInNewWindowsProperty = new SimpleBooleanProperty();
@ -373,6 +381,8 @@ public class AppController implements Initializable {
selectedThemeToggle.ifPresent(toggle -> theme.selectToggle(toggle));
setTheme(null);
prepareLanguages();
openWalletsInNewWindowsProperty.set(Config.get().isOpenWalletsInNewWindows());
openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty);
hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses());
@ -430,6 +440,27 @@ public class AppController implements Initializable {
setNetworkLabel();
}
private void prepareLanguages() {
Language[] languagesList = Language.values();
for(Language language : languagesList) {
RadioMenuItem languageItem = new RadioMenuItem(LanguagesManager.getMessage("language." + language.getCode()));
languageItem.setMnemonicParsing(false);
languageItem.setToggleGroup(languages);
languageItem.setOnAction(this::setLanguage);
languageItem.setUserData(language);
languagesMenu.getItems().add(languageItem);
}
Language configLanguage = Config.get().getLanguage();
if(configLanguage == null) {
configLanguage = LanguagesManager.DEFAULT_LANGUAGE;
Config.get().setLanguage(configLanguage);
}
final Language selectedLanguage = configLanguage;
Optional<Toggle> selectedLanguageToggle = languages.getToggles().stream().filter(toggle -> selectedLanguage.equals(toggle.getUserData())).findFirst();
selectedLanguageToggle.ifPresent(toggle -> languages.selectToggle(toggle));
}
private void registerShortcuts() {
OsType osType = OsType.getCurrent();
if(osType == OsType.MACOS) {
@ -488,7 +519,7 @@ public class AppController implements Initializable {
}
public void showIntroduction(ActionEvent event) {
WelcomeDialog welcomeDialog = new WelcomeDialog();
WelcomeDialog welcomeDialog = new WelcomeDialog(false);
welcomeDialog.initOwner(rootStack.getScene().getWindow());
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) {
@ -997,13 +1028,19 @@ public class AppController implements Initializable {
}
}
public void restart(ActionEvent event) {
restart(event, (Network) null);
}
public void restart(ActionEvent event, Network network) {
if(System.getProperty(JPACKAGE_APP_PATH) == null) {
throw new IllegalStateException("Property " + JPACKAGE_APP_PATH + " is not present");
}
Args args = getRestartArgs();
args.network = network;
if(network != null) {
args.network = network;
}
restart(event, args);
}
@ -1761,7 +1798,7 @@ public class AppController implements Initializable {
subTabLabel.setTooltip(new Tooltip(label));
}
subTab.setGraphic(subTabLabel);
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"));
FXMLLoader walletLoader = new FXMLLoader(getClass().getResource("wallet/wallet.fxml"), LanguagesManager.getResourceBundle());
subTab.setContent(walletLoader.load());
WalletController controller = walletLoader.getController();
@ -2338,6 +2375,15 @@ public class AppController implements Initializable {
EventManager.get().post(new ThemeChangedEvent(selectedTheme));
}
public void setLanguage(ActionEvent event) {
Language selectedLanguage = (Language)languages.getSelectedToggle().getUserData();
if(Config.get().getLanguage() != selectedLanguage) {
Config.get().setLanguage(selectedLanguage);
}
restart(event);
}
private void serverToggleStartAnimation() {
Node thumbArea = serverToggle.lookup(".thumb-area");
if(thumbArea != null) {

View file

@ -15,6 +15,7 @@ import com.sparrowwallet.drongo.policy.PolicyType;
import com.sparrowwallet.drongo.wallet.*;
import com.sparrowwallet.sparrow.control.WalletPasswordDialog;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.net.Auth47;
import com.sparrowwallet.drongo.protocol.BlockHeader;
import com.sparrowwallet.drongo.protocol.ScriptType;
@ -552,7 +553,7 @@ public class AppServices {
public static AppController newAppWindow(Stage stage) {
try {
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"));
FXMLLoader appLoader = new FXMLLoader(AppServices.class.getResource("app.fxml"), LanguagesManager.getResourceBundle());
Parent root = appLoader.load();
AppController appController = appLoader.getController();

View file

@ -6,6 +6,8 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.control.WalletIcon;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5Brands;
import com.sparrowwallet.sparrow.i18n.Language;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.PublicElectrumServer;
@ -50,7 +52,7 @@ public class SparrowDesktop extends Application {
boolean createNewWallet = false;
Mode mode = Config.get().getMode();
if(mode == null) {
WelcomeDialog welcomeDialog = new WelcomeDialog();
WelcomeDialog welcomeDialog = new WelcomeDialog(true);
Optional<Mode> optionalMode = welcomeDialog.showAndWait();
if(optionalMode.isPresent()) {
mode = optionalMode.get();
@ -84,6 +86,13 @@ public class SparrowDesktop extends Application {
mainStage.setHeight(Config.get().getAppHeight());
}
Language configLanguage = Config.get().getLanguage();
if(configLanguage == null) {
configLanguage = LanguagesManager.DEFAULT_LANGUAGE;
Config.get().setLanguage(configLanguage);
}
LanguagesManager.loadLanguage(configLanguage.getCode());
AppController appController = AppServices.newAppWindow(stage);
final boolean showNewWallet = createNewWallet;

View file

@ -1,22 +1,26 @@
package com.sparrowwallet.sparrow;
import com.sparrowwallet.sparrow.control.UnlabeledToggleSwitch;
import com.sparrowwallet.sparrow.event.LanguageChangedInWelcomeEvent;
import com.sparrowwallet.sparrow.i18n.Language;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.io.Config;
import javafx.animation.PauseTransition;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.event.ActionEvent;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.fxml.FXML;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Tooltip;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.VBox;
import javafx.util.Duration;
import javafx.util.StringConverter;
import org.controlsfx.control.StatusBar;
import java.util.Arrays;
import java.util.List;
public class WelcomeController {
@FXML
private VBox welcomeBox;
@ -36,10 +40,13 @@ public class WelcomeController {
@FXML
private StatusBar serverStatus;
@FXML
private ComboBox<Language> languages;
@FXML
private UnlabeledToggleSwitch serverToggle;
public void initializeView() {
public void initializeView(boolean isFirstExecution) {
step1.managedProperty().bind(step1.visibleProperty());
step2.managedProperty().bind(step2.visibleProperty());
step3.managedProperty().bind(step3.visibleProperty());
@ -50,14 +57,51 @@ public class WelcomeController {
step4.setVisible(false);
welcomeBox.getStyleClass().add("offline");
serverStatus.setText("Offline");
serverStatus.setText(LanguagesManager.getMessage("welcome.offline"));
serverToggle.addEventFilter(MouseEvent.MOUSE_RELEASED, Event::consume);
Tooltip tooltip = new Tooltip("Demonstration only - you are not connected!");
Tooltip tooltip = new Tooltip(LanguagesManager.getMessage("welcome.offline.tooltip"));
tooltip.setShowDelay(Duration.ZERO);
serverToggle.setTooltip(tooltip);
serverToggle.selectedProperty().addListener((observable, oldValue, newValue) -> {
serverStatus.setText(newValue ? "Connected (demonstration only)" : "Offline");
serverStatus.setText(newValue ? LanguagesManager.getMessage("welcome.server-status.online") : LanguagesManager.getMessage("welcome.offline"));
});
if(isFirstExecution) {
languages.setItems(getLanguagesList());
languages.setConverter(new StringConverter<>() {
@Override
public String toString(Language language) {
if(language != null) {
return LanguagesManager.getMessage("language." + language.getCode());
} else {
return null;
}
}
@Override
public Language fromString(String code) {
return Language.getFromCode(code);
}
});
Language configuredLanguage = Config.get().getLanguage();
if(configuredLanguage != null) {
languages.setValue(configuredLanguage);
} else {
languages.setValue(LanguagesManager.DEFAULT_LANGUAGE);
}
languages.valueProperty().addListener((observable, oldValue, newValue) -> {
if(Config.get().getLanguage() != newValue) {
Config.get().setLanguage(newValue);
LanguagesManager.loadLanguage(newValue.getCode());
EventManager.get().post(new LanguageChangedInWelcomeEvent(newValue));
}
});
} else {
languages.setVisible(false);
}
}
public boolean next() {
@ -69,7 +113,7 @@ public class WelcomeController {
PauseTransition wait = new PauseTransition(Duration.millis(200));
wait.setOnFinished((e) -> {
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Public Server (demonstration only)");
serverStatus.setText(LanguagesManager.getMessage("welcome.server-status.online.public-server"));
});
wait.play();
return true;
@ -81,7 +125,7 @@ public class WelcomeController {
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("bitcoin-core");
serverToggle.setSelected(true);
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
serverStatus.setText(LanguagesManager.getMessage("welcome.server-status.online.bitcoin-core"));
return true;
}
@ -91,7 +135,7 @@ public class WelcomeController {
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("private-electrum");
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Private Electrum Server (demonstration only)");
serverStatus.setText(LanguagesManager.getMessage("welcome.server-status.online.private-electrum"));
}
return false;
@ -106,7 +150,7 @@ public class WelcomeController {
PauseTransition wait = new PauseTransition(Duration.millis(200));
wait.setOnFinished((e) -> {
serverToggle.setSelected(false);
serverStatus.setText("Offline");
serverStatus.setText(LanguagesManager.getMessage("welcome.offline"));
});
wait.play();
return false;
@ -118,7 +162,7 @@ public class WelcomeController {
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("public-electrum");
serverToggle.setSelected(true);
serverStatus.setText("Connected to a Public Server (demonstration only)");
serverStatus.setText(LanguagesManager.getMessage("welcome.server-status.online.public-server"));
return true;
}
@ -128,10 +172,15 @@ public class WelcomeController {
welcomeBox.getStyleClass().clear();
welcomeBox.getStyleClass().add("bitcoin-core");
serverToggle.setSelected(true);
serverStatus.setText("Connected to Bitcoin Core (demonstration only)");
serverStatus.setText(LanguagesManager.getMessage("welcome.server-status.online.bitcoin-core"));
return true;
}
return false;
}
private ObservableList<Language> getLanguagesList() {
List<Language> languages = Arrays.asList(Language.values());
return FXCollections.observableList(languages);
}
}

View file

@ -1,5 +1,8 @@
package com.sparrowwallet.sparrow;
import com.google.common.eventbus.Subscribe;
import com.sparrowwallet.sparrow.event.LanguageChangedInWelcomeEvent;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import javafx.event.ActionEvent;
import javafx.fxml.FXMLLoader;
import javafx.scene.control.*;
@ -9,16 +12,22 @@ import javafx.scene.image.ImageView;
import java.io.IOException;
public class WelcomeDialog extends Dialog<Mode> {
public WelcomeDialog() {
private WelcomeController welcomeController;
public WelcomeDialog(boolean isFirstExecution) {
super();
final DialogPane dialogPane = getDialogPane();
AppServices.setStageIcon(dialogPane.getScene().getWindow());
AppServices.onEscapePressed(dialogPane.getScene(), this::close);
EventManager.get().register(this);
try {
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"));
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"), LanguagesManager.getResourceBundle());
dialogPane.setContent(welcomeLoader.load());
WelcomeController welcomeController = welcomeLoader.getController();
welcomeController.initializeView();
welcomeController = welcomeLoader.getController();
welcomeController.initializeView(isFirstExecution);
dialogPane.setPrefWidth(600);
dialogPane.setPrefHeight(520);
@ -27,10 +36,10 @@ public class WelcomeDialog extends Dialog<Mode> {
dialogPane.getStylesheets().add(AppServices.class.getResource("welcome.css").toExternalForm());
final ButtonType nextButtonType = new javafx.scene.control.ButtonType("Next", ButtonBar.ButtonData.OK_DONE);
final ButtonType backButtonType = new javafx.scene.control.ButtonType("Back", ButtonBar.ButtonData.LEFT);
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType("Configure Server", ButtonBar.ButtonData.APPLY);
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType(AppServices.isConnected() ? "Done" : "Later or Offline Mode", ButtonBar.ButtonData.CANCEL_CLOSE);
final ButtonType nextButtonType = new javafx.scene.control.ButtonType(LanguagesManager.getMessage("welcome.next"), ButtonBar.ButtonData.OK_DONE);
final ButtonType backButtonType = new javafx.scene.control.ButtonType(LanguagesManager.getMessage("welcome.back"), ButtonBar.ButtonData.LEFT);
final ButtonType onlineButtonType = new javafx.scene.control.ButtonType(LanguagesManager.getMessage("welcome.configure-server"), ButtonBar.ButtonData.APPLY);
final ButtonType offlineButtonType = new javafx.scene.control.ButtonType(AppServices.isConnected() ? LanguagesManager.getMessage("welcome.done") : LanguagesManager.getMessage("welcome.configure-later"), ButtonBar.ButtonData.CANCEL_CLOSE);
dialogPane.getButtonTypes().addAll(nextButtonType, backButtonType, onlineButtonType, offlineButtonType);
Button nextButton = (Button)dialogPane.lookupButton(nextButtonType);
@ -69,4 +78,18 @@ public class WelcomeDialog extends Dialog<Mode> {
throw new RuntimeException(e);
}
}
@Subscribe
public void languageChanged(LanguageChangedInWelcomeEvent event) {
final DialogPane dialogPane = getDialogPane();
try {
FXMLLoader welcomeLoader = new FXMLLoader(AppServices.class.getResource("welcome.fxml"), LanguagesManager.getResourceBundle());
dialogPane.setContent(welcomeLoader.load());
welcomeController = welcomeLoader.getController();
welcomeController.initializeView(true);
} catch(IOException e) {
throw new RuntimeException(e);
}
}
}

View file

@ -9,6 +9,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletAddressesChangedEvent;
import com.sparrowwallet.sparrow.event.WalletDataChangedEvent;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.ServerType;
@ -86,12 +87,12 @@ public class CoinTreeTable extends TreeTableView<Entry> {
if(entry != null && event.getWallet() != null && entry.getWallet() == event.getWallet()) {
Platform.runLater(() -> {
if(event.getErrorMessage() != null) {
setPlaceholder(new Label("Error loading transactions: " + event.getErrorMessage()));
setPlaceholder(new Label(LanguagesManager.getMessage("wallet.transactions.table.loading.error")+ " " + event.getErrorMessage()));
} else if(event.isLoading()) {
if(event.getStatusMessage() != null) {
setPlaceholder(new Label(event.getStatusMessage() + "..."));
} else {
setPlaceholder(new Label("Loading transactions..."));
setPlaceholder(new Label(LanguagesManager.getMessage("wallet.transactions.table.loading")));
}
} else {
setPlaceholder(getDefaultPlaceholder(event.getWallet()));
@ -103,7 +104,7 @@ public class CoinTreeTable extends TreeTableView<Entry> {
protected Node getDefaultPlaceholder(Wallet wallet) {
StackPane stackPane = new StackPane();
stackPane.getChildren().add(AppServices.isConnecting() ? new Label("Loading transactions...") : new Label("No transactions"));
stackPane.getChildren().add(AppServices.isConnecting() ? new Label(LanguagesManager.getMessage("wallet.transactions.table.loading")) : new Label(LanguagesManager.getMessage("wallet.transactions.table.no-transactions")));
if(Config.get().getServerType() == ServerType.BITCOIN_CORE && !AppServices.isConnecting()) {
Hyperlink hyperlink = new Hyperlink();

View file

@ -15,6 +15,7 @@ import com.sparrowwallet.sparrow.event.ExcludeUtxoEvent;
import com.sparrowwallet.sparrow.event.ReplaceChangeAddressEvent;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.glyphfont.GlyphUtils;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
import javafx.beans.property.BooleanProperty;
@ -806,7 +807,7 @@ public class TransactionDiagram extends GridPane {
txPane.setAlignment(Pos.CENTER);
txPane.getChildren().add(createSpacer());
String txDesc = "Transaction";
String txDesc = LanguagesManager.getMessage("diagram.transaction");
Label txLabel = new Label(txDesc);
boolean isFinalized = walletTx.getTransaction().hasScriptSigs() || walletTx.getTransaction().hasWitnesses();
Tooltip tooltip = new Tooltip(walletTx.getTransaction().getLength() + " bytes\n"

View file

@ -1,5 +1,6 @@
package com.sparrowwallet.sparrow.control;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.wallet.Entry;
import com.sparrowwallet.sparrow.wallet.TransactionEntry;
import com.sparrowwallet.sparrow.wallet.WalletTransactionsEntry;
@ -15,7 +16,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
updateAll(rootEntry);
setShowRoot(false);
TreeTableColumn<Entry, Entry> dateCol = new TreeTableColumn<>("Date");
TreeTableColumn<Entry, Entry> dateCol = new TreeTableColumn<>(LanguagesManager.getMessage("wallet.transactions.table.date"));
dateCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Entry> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue());
});
@ -23,7 +24,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
dateCol.setSortable(true);
getColumns().add(dateCol);
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>("Label");
TreeTableColumn<Entry, String> labelCol = new TreeTableColumn<>(LanguagesManager.getMessage("wallet.transactions.table.label"));
labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, String> param) -> {
return param.getValue().getValue().labelProperty();
});
@ -31,7 +32,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
labelCol.setSortable(true);
getColumns().add(labelCol);
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>("Value");
TreeTableColumn<Entry, Number> amountCol = new TreeTableColumn<>(LanguagesManager.getMessage("wallet.transactions.table.value"));
amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue());
});
@ -39,7 +40,7 @@ public class TransactionsTreeTable extends CoinTreeTable {
amountCol.setSortable(true);
getColumns().add(amountCol);
TreeTableColumn<Entry, Number> balanceCol = new TreeTableColumn<>("Balance");
TreeTableColumn<Entry, Number> balanceCol = new TreeTableColumn<>(LanguagesManager.getMessage("wallet.transactions.table.balance"));
balanceCol.setCellValueFactory((TreeTableColumn.CellDataFeatures<Entry, Number> param) -> {
return param.getValue().getValue() instanceof TransactionEntry ? ((TransactionEntry)param.getValue().getValue()).balanceProperty() : new ReadOnlyObjectWrapper<>(null);
});

View file

@ -0,0 +1,15 @@
package com.sparrowwallet.sparrow.event;
import com.sparrowwallet.sparrow.i18n.Language;
public class LanguageChangedInWelcomeEvent {
private final Language language;
public LanguageChangedInWelcomeEvent(Language language) {
this.language = language;
}
public Language getLanguage() {
return language;
}
}

View file

@ -0,0 +1,25 @@
package com.sparrowwallet.sparrow.i18n;
public enum Language {
ENGLISH("en"),
SPANISH("es");
private final String code;
Language(String code) {
this.code = code;
}
public String getCode() {
return code;
}
public static Language getFromCode(String code) {
for (Language l : Language.values()) {
if (l.code.equals(code)) {
return l;
}
}
return null;
}
}

View file

@ -0,0 +1,27 @@
package com.sparrowwallet.sparrow.i18n;
import java.util.*;
public class LanguagesManager {
public static final Language DEFAULT_LANGUAGE = Language.ENGLISH;
private static ResourceBundle bundle;
public static ResourceBundle getResourceBundle() {
if(bundle == null) {
loadLanguage(DEFAULT_LANGUAGE.getCode());
}
return bundle;
}
public static void loadLanguage(String language) {
Locale locale = new Locale(language);
if(bundle == null || !locale.equals(bundle.getLocale())) {
bundle = ResourceBundle.getBundle("i18n/messages", locale);
}
}
public static String getMessage(String key) { return bundle.getString(key); }
}

View file

@ -6,6 +6,7 @@ import com.sparrowwallet.sparrow.UnitFormat;
import com.sparrowwallet.sparrow.Mode;
import com.sparrowwallet.sparrow.Theme;
import com.sparrowwallet.sparrow.control.QRDensity;
import com.sparrowwallet.sparrow.i18n.Language;
import com.sparrowwallet.sparrow.net.*;
import com.sparrowwallet.sparrow.wallet.FeeRatesSelection;
import com.sparrowwallet.sparrow.wallet.OptimizationStrategy;
@ -43,6 +44,7 @@ public class Config {
private boolean notifyNewTransactions = true;
private boolean checkNewVersions = true;
private Theme theme;
private Language language;
private boolean openWalletsInNewWindows = false;
private boolean hideEmptyUsedAddresses = false;
private boolean showTransactionHex = true;
@ -277,6 +279,15 @@ public class Config {
flush();
}
public Language getLanguage() {
return language;
}
public void setLanguage(Language language) {
this.language = language;
flush();
}
public boolean isOpenWalletsInNewWindows() {
return openWalletsInNewWindows;
}

View file

@ -9,6 +9,7 @@ import com.sparrowwallet.drongo.protocol.Sha256Hash;
import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -82,7 +83,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@SuppressWarnings("unchecked")
public Map<String, ScriptHashTx[]> getScriptHashHistory(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes, boolean failOnError) {
PagedBatchRequestBuilder<String, ScriptHashTx[]> batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(String.class).returnType(ScriptHashTx[].class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, LanguagesManager.getMessage("wallet.transactions.loading-transactions") + " " + nodeRangesToString(pathScriptHashes.keySet())));
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.get_history", pathScriptHashes.get(path));
@ -137,7 +138,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@SuppressWarnings("unchecked")
public Map<String, String> subscribeScriptHashes(Transport transport, Wallet wallet, Map<String, String> pathScriptHashes) {
PagedBatchRequestBuilder<String, String> batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + nodeRangesToString(pathScriptHashes.keySet())));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, LanguagesManager.getMessage("wallet.transactions.finding-transactions") + " " + nodeRangesToString(pathScriptHashes.keySet())));
for(String path : pathScriptHashes.keySet()) {
batchRequest.add(path, "blockchain.scripthash.subscribe", pathScriptHashes.get(path));
@ -157,7 +158,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@SuppressWarnings("unchecked")
public Map<Integer, String> getBlockHeaders(Transport transport, Wallet wallet, Set<Integer> blockHeights) {
PagedBatchRequestBuilder<Integer, String> batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(Integer.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving " + blockHeights.size() + " block headers"));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, LanguagesManager.getMessage("wallet.transactions.retrieving") + " " + blockHeights.size() + " " + LanguagesManager.getMessage("wallet.transactions.retrieving-headers")));
for(Integer height : blockHeights) {
batchRequest.add(height, "blockchain.block.header", height);
@ -176,7 +177,7 @@ public class BatchedElectrumServerRpc implements ElectrumServerRpc {
@SuppressWarnings("unchecked")
public Map<String, String> getTransactions(Transport transport, Wallet wallet, Set<String> txids) {
PagedBatchRequestBuilder<String, String> batchRequest = PagedBatchRequestBuilder.create(transport, idCounter).keysType(String.class).returnType(String.class);
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving " + txids.size() + " transactions"));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, LanguagesManager.getMessage("wallet.transactions.retrieving") + " " + txids.size() + " " + LanguagesManager.getMessage("wallet.transactions.retrieving-transactions")));
for(String txid : txids) {
batchRequest.add(txid, "blockchain.transaction.get", txid);

View file

@ -10,6 +10,7 @@ import com.sparrowwallet.drongo.wallet.Wallet;
import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.event.WalletHistoryStatusEvent;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -76,7 +77,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, ScriptHashTx[]> result = new LinkedHashMap<>();
for(String path : pathScriptHashes.keySet()) {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Loading transactions for " + path));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, LanguagesManager.getMessage("wallet.transactions.loading-transactions") + " " + path));
try {
ScriptHashTx[] scriptHashTxes = new RetryLogic<ScriptHashTx[]>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(ScriptHashTx[].class).method("blockchain.scripthash.get_history").id(idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).execute());
@ -121,7 +122,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, String> result = new LinkedHashMap<>();
for(String path : pathScriptHashes.keySet()) {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Finding transactions for " + path));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, LanguagesManager.getMessage("wallet.transactions.finding-transactions") + " " + path));
try {
String scriptHash = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.scripthash.subscribe").id(idCounter.incrementAndGet()).params(pathScriptHashes.get(path)).executeNullable());
@ -141,7 +142,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<Integer, String> result = new LinkedHashMap<>();
for(Integer blockHeight : blockHeights) {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving block at height " + blockHeight));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, LanguagesManager.getMessage("wallet.transactions.retrieving-block-height") + " " + blockHeight));
try {
String blockHeader = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.block.header").id(idCounter.incrementAndGet()).params(blockHeight).execute());
@ -165,7 +166,7 @@ public class SimpleElectrumServerRpc implements ElectrumServerRpc {
Map<String, String> result = new LinkedHashMap<>();
for(String txid : txids) {
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, "Retrieving transaction [" + txid.substring(0, 6) + "]"));
EventManager.get().post(new WalletHistoryStatusEvent(wallet, true, LanguagesManager.getMessage("wallet.transactions.retrieving-transaction") + " [" + txid.substring(0, 6) + "]"));
try {
String rawTxHex = new RetryLogic<String>(MAX_RETRIES, RETRY_DELAY, List.of(IllegalStateException.class, IllegalArgumentException.class)).getResult(() ->
client.createRequest().returnAs(String.class).method("blockchain.transaction.get").id(idCounter.incrementAndGet()).params(txid).execute());

View file

@ -20,6 +20,7 @@ import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.glyphfont.FontAwesome5;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.Storage;
import com.sparrowwallet.sparrow.net.*;
@ -356,7 +357,7 @@ public class SendController extends WalletFormController implements Initializabl
});
utxoLabelSelectionProperty.addListener((observable, oldValue, newValue) -> {
clearButton.setText("Clear" + newValue);
clearButton.setText(LanguagesManager.getMessage("wallet.send.clear") + newValue);
});
utxoSelectorProperty.addListener((observable, oldValue, utxoSelector) -> {
@ -475,7 +476,7 @@ public class SendController extends WalletFormController implements Initializabl
Tab tab = new Tab(" " + (highestTabNo.isPresent() ? highestTabNo.getAsInt() + 1 : 1) + " ");
try {
FXMLLoader paymentLoader = new FXMLLoader(AppServices.class.getResource("wallet/payment.fxml"));
FXMLLoader paymentLoader = new FXMLLoader(AppServices.class.getResource("wallet/payment.fxml"), LanguagesManager.getResourceBundle());
tab.setContent(paymentLoader.load());
PaymentController controller = paymentLoader.getController();
controller.setSendController(this);
@ -900,8 +901,8 @@ public class SendController extends WalletFormController implements Initializabl
if(targetBlocksFeeRates.get(Integer.MAX_VALUE) != null) {
Double minFeeRate = targetBlocksFeeRates.get(Integer.MAX_VALUE);
if(minFeeRate > 1.0 && feeRateAmt < minFeeRate) {
feeRatePriority.setText("Below Minimum");
feeRatePriority.setTooltip(new Tooltip("Transactions at this fee rate are currently being purged from the default sized mempool"));
feeRatePriority.setText(LanguagesManager.getMessage("wallet.send.fee.rate.below-minimum"));
feeRatePriority.setTooltip(new Tooltip(LanguagesManager.getMessage("wallet.send.fee.rate.below-minimum.tooltip")));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #a0a1a7cc");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
return;
@ -909,8 +910,8 @@ public class SendController extends WalletFormController implements Initializabl
Double lowestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(TARGET_BLOCKS_RANGE.size() - 1));
if(lowestBlocksRate >= minFeeRate && feeRateAmt < (minFeeRate + ((lowestBlocksRate - minFeeRate) / 2)) && !isPayjoinTx()) {
feeRatePriority.setText("Try Then Replace");
feeRatePriority.setTooltip(new Tooltip("Send a transaction, verify it appears in the destination wallet, then RBF to get it confirmed or sent to another address"));
feeRatePriority.setText(LanguagesManager.getMessage("wallet.send.fee.rate.replace"));
feeRatePriority.setTooltip(new Tooltip(LanguagesManager.getMessage("wallet.send.fee.rate.replace.tooltip")));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #7eb7c9cc");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.PLUS_CIRCLE);
return;
@ -922,24 +923,24 @@ public class SendController extends WalletFormController implements Initializabl
Double maxFeeRate = FEE_RATES_RANGE.get(FEE_RATES_RANGE.size() - 1).doubleValue();
Double highestBlocksRate = targetBlocksFeeRates.get(TARGET_BLOCKS_RANGE.get(0));
if(highestBlocksRate < maxFeeRate && feeRateAmt > (highestBlocksRate + ((maxFeeRate - highestBlocksRate) / 10))) {
feeRatePriority.setText("Overpaid");
feeRatePriority.setTooltip(new Tooltip("Transaction fees at this rate are likely higher than necessary"));
feeRatePriority.setText(LanguagesManager.getMessage("wallet.send.fee.rate.overpaid"));
feeRatePriority.setTooltip(new Tooltip(LanguagesManager.getMessage("wallet.send.fee.rate.overpaid.tooltip")));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #c8416499");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.EXCLAMATION_CIRCLE);
} else {
feeRatePriority.setText("High Priority");
feeRatePriority.setTooltip(new Tooltip("Typically confirms within minutes"));
feeRatePriority.setText(LanguagesManager.getMessage("wallet.send.fee.rate.high-priority"));
feeRatePriority.setTooltip(new Tooltip(LanguagesManager.getMessage("wallet.send.fee.rate.high-priority.tooltip")));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #c8416499");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
}
} else if(targetBlocks < FeeRatesSource.BLOCKS_IN_HOUR) {
feeRatePriority.setText("Medium Priority");
feeRatePriority.setTooltip(new Tooltip("Typically confirms within an hour or two"));
feeRatePriority.setText(LanguagesManager.getMessage("wallet.send.fee.rate.medium-priority"));
feeRatePriority.setTooltip(new Tooltip(LanguagesManager.getMessage("wallet.send.fee.rate.medium-priority.tooltip")));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #fba71b99");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
} else {
feeRatePriority.setText("Low Priority");
feeRatePriority.setTooltip(new Tooltip("Typically confirms in a day or longer"));
feeRatePriority.setText(LanguagesManager.getMessage("wallet.send.fee.rate.low-priority"));
feeRatePriority.setTooltip(new Tooltip(LanguagesManager.getMessage("wallet.send.fee.rate.low-priority.tooltip")));
feeRatePriorityGlyph.setStyle("-fx-text-fill: #41a9c999");
feeRatePriorityGlyph.setIcon(FontAwesome5.Glyph.CIRCLE);
}
@ -967,11 +968,11 @@ public class SendController extends WalletFormController implements Initializabl
private void updateMaxClearButtons(UtxoSelector utxoSelector, TxoFilter txoFilter) {
if(utxoSelector instanceof PresetUtxoSelector presetUtxoSelector) {
int num = presetUtxoSelector.getPresetUtxos().size();
String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " selected)";
String selection = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " " + LanguagesManager.getMessage("common.selected") + ")";
utxoLabelSelectionProperty.set(selection);
} else if(txoFilter instanceof ExcludeTxoFilter excludeTxoFilter) {
int num = excludeTxoFilter.getExcludedTxos().size();
String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " excluded)";
String exclusion = " (" + num + " UTXO" + (num != 1 ? "s" : "") + " " + LanguagesManager.getMessage("common.excluded") + ")";
utxoLabelSelectionProperty.set(exclusion);
} else {
utxoLabelSelectionProperty.set("");
@ -1583,38 +1584,38 @@ public class SendController extends WalletFormController implements Initializabl
if(optimizationStrategy == OptimizationStrategy.PRIVACY) {
if(fakeMixPresent) {
addLabel("Appears as a two person coinjoin", getPlusGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.coinjoin"), getPlusGlyph());
} else {
if(mixedAddressTypes) {
addLabel("Cannot fake coinjoin due to mixed address types", getInfoGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.coinjoin.mixes-address-types"), getInfoGlyph());
} else if(userPayments.size() > 1) {
addLabel("Cannot fake coinjoin due to multiple payments", getInfoGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.coinjoin.multiple-payments"), getInfoGlyph());
} else if(payjoinPresent) {
addLabel("Cannot fake coinjoin due to payjoin", getInfoGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.coinjoin.payjoin"), getInfoGlyph());
} else {
if(utxoSelectorProperty().get() != null) {
addLabel("Cannot fake coinjoin due to coin control", getInfoGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.coinjoin.coincontrol"), getInfoGlyph());
} else {
addLabel("Cannot fake coinjoin due to insufficient funds", getInfoGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.coinjoin.insufficient-funds"), getInfoGlyph());
}
}
}
}
if(mixedAddressTypes) {
addLabel("Address types different to the wallet indicate external payments", getMinusGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.mixes-address-types"), getMinusGlyph());
}
if(roundPaymentAmounts && !fakeMixPresent) {
addLabel("Rounded payment amounts indicate external payments", getMinusGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.external-payments"), getMinusGlyph());
}
if(addressReuse) {
addLabel("Address reuse detected", getMinusGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.reuse"), getMinusGlyph());
}
if(!fakeMixPresent && !mixedAddressTypes && !roundPaymentAmounts) {
addLabel("Appears as a possible self transfer", getPlusGlyph());
addLabel(LanguagesManager.getMessage("wallet.send.optimize.self-transfer"), getPlusGlyph());
}
analysisLabels.sort(Comparator.comparingInt(o -> (Integer)o.getGraphic().getUserData()));

View file

@ -5,6 +5,7 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.*;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.io.Config;
import com.sparrowwallet.sparrow.io.WalletTransactions;
import com.sparrowwallet.sparrow.net.ExchangeSource;
@ -97,7 +98,7 @@ public class TransactionsController extends WalletFormController implements Init
});
transactionsMasterDetail.setShowDetailNode(Config.get().isShowLoadingLog());
loadingLog.appendText("Wallet loading history for " + getWalletForm().getWallet().getFullDisplayName());
loadingLog.appendText(LanguagesManager.getMessage("wallet.transactions.loading-history") + " " + getWalletForm().getWallet().getFullDisplayName());
loadingLog.setEditable(false);
}
@ -110,7 +111,7 @@ public class TransactionsController extends WalletFormController implements Init
Stage window = new Stage();
FileChooser fileChooser = new FileChooser();
fileChooser.setTitle("Export Transactions as CSV");
fileChooser.setTitle(LanguagesManager.getMessage("wallet.transactions.csv-export"));
fileChooser.setInitialFileName(wallet.getFullName() + "-transactions.csv");
AppServices.moveToActiveWindowScreen(window, 800, 450);
File file = fileChooser.showSaveDialog(window);
@ -119,7 +120,7 @@ public class TransactionsController extends WalletFormController implements Init
exportService.setOnFailed(failedEvent -> {
Throwable e = failedEvent.getSource().getException();
log.error("Error exporting transactions as CSV", e);
AppServices.showErrorDialog("Error exporting transactions as CSV", e.getMessage());
AppServices.showErrorDialog(LanguagesManager.getMessage("wallet.transactions.csv-export.error"), e.getMessage());
});
exportService.start();
}
@ -219,7 +220,7 @@ public class TransactionsController extends WalletFormController implements Init
String logMessage = event.getStatusMessage();
if(logMessage == null) {
if(event instanceof WalletHistoryFinishedEvent) {
logMessage = "Finished loading.";
logMessage = LanguagesManager.getMessage("wallet.transactions.loading-finish");
} else if(event instanceof WalletHistoryFailedEvent) {
logMessage = event.getErrorMessage();
}

View file

@ -8,6 +8,7 @@ import com.sparrowwallet.sparrow.AppServices;
import com.sparrowwallet.sparrow.EventManager;
import com.sparrowwallet.sparrow.control.ViewPasswordField;
import com.sparrowwallet.sparrow.event.*;
import com.sparrowwallet.sparrow.i18n.LanguagesManager;
import com.sparrowwallet.sparrow.io.Storage;
import javafx.application.Platform;
import javafx.beans.property.BooleanProperty;
@ -95,7 +96,7 @@ public class WalletController extends WalletFormController implements Initializa
throw new IllegalStateException("Cannot find wallet/" + function.toString().toLowerCase(Locale.ROOT) + ".fxml");
}
FXMLLoader functionLoader = new FXMLLoader(url);
FXMLLoader functionLoader = new FXMLLoader(url, LanguagesManager.getResourceBundle());
Node walletFunction = functionLoader.load();
walletFunction.setUserData(function);
WalletFormController controller = functionLoader.getController();

View file

@ -9,6 +9,7 @@
<?import com.sparrowwallet.drongo.BitcoinUnit?>
<?import com.sparrowwallet.sparrow.UnitFormat?>
<?import com.sparrowwallet.sparrow.Theme?>
<?import com.sparrowwallet.sparrow.i18n.Language?>
<?import impl.org.controlsfx.skin.DecorationPane?>
<VBox maxHeight="-Infinity" maxWidth="-Infinity" minHeight="200" minWidth="350" prefHeight="770.0" prefWidth="1070.0" fx:controller="com.sparrowwallet.sparrow.AppController" xmlns="http://javafx.com/javafx/10.0.2-internal" xmlns:fx="http://javafx.com/fxml/1">
@ -64,6 +65,9 @@
<fx:define>
<ToggleGroup fx:id="theme"/>
</fx:define>
<fx:define>
<ToggleGroup fx:id="languages"/>
</fx:define>
<Menu fx:id="viewMenu" mnemonicParsing="false" text="View">
<items>
<Menu mnemonicParsing="false" text="Bitcoin Unit">
@ -113,6 +117,7 @@
</RadioMenuItem>
</items>
</Menu>
<Menu fx:id="languagesMenu" mnemonicParsing="false" text="%language" />
<SeparatorMenuItem />
<CheckMenuItem fx:id="openWalletsInNewWindows" mnemonicParsing="false" text="Open Wallets In New Windows" onAction="#openWalletsInNewWindows"/>
<CheckMenuItem fx:id="hideEmptyUsedAddresses" mnemonicParsing="false" text="Hide Empty Used Addresses" onAction="#hideEmptyUsedAddresses"/>

View file

@ -30,24 +30,24 @@
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="2">
<Fieldset inputGrow="ALWAYS">
<Field text="Pay to:">
<Field text="%wallet.send.pay-to">
<StackPane>
<ComboBox fx:id="openWallets" />
<ComboBoxTextField fx:id="address" styleClass="address-text-field" comboProperty="$openWallets">
<tooltip>
<Tooltip text="Address, payment code or bitcoin: URI"/>
<Tooltip text="%wallet.send.pay-to.tooltip"/>
</tooltip>
</ComboBoxTextField>
</StackPane>
</Field>
<Field text="Label:">
<TextField fx:id="label" promptText="Required">
<Field text="%wallet.send.label">
<TextField fx:id="label" promptText="%wallet.send.label.required">
<tooltip>
<Tooltip text="Required to label the transaction (privately in this wallet)"/>
<Tooltip text="%wallet.send.label.tooltip"/>
</tooltip>
</TextField>
</Field>
<Field text="Amount:">
<Field text="%wallet.send.amount">
<TextField fx:id="amount" styleClass="amount-field" />
<ComboBox fx:id="amountUnit" styleClass="amount-unit">
<items>
@ -60,17 +60,17 @@
<Label style="-fx-pref-width: 15" />
<FiatLabel fx:id="fiatAmount" />
<Group managed="false" translateY="38">
<Label fx:id="amountStatus" text="Insufficient funds">
<Label fx:id="amountStatus" text="%wallet.send.amount.insuficient">
<tooltip>
<Tooltip text="Insufficient confirmed transaction value" />
<Tooltip text="%wallet.send.amount.insuficient.tootip" />
</tooltip>
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
</graphic>
</Label>
<Label fx:id="dustStatus" text="Amount too low">
<Label fx:id="dustStatus" text="%wallet.send.amount.dust">
<tooltip>
<Tooltip text="Amount below the Bitcoin network dust threshold for this address type" />
<Tooltip text="%wallet.send.amount.dust.tooltip" />
</tooltip>
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="EXCLAMATION_CIRCLE" styleClass="failure" />
@ -91,7 +91,7 @@
</graphic>
</Button>
<Region HBox.hgrow="ALWAYS" />
<Button fx:id="addPaymentButton" text="Add" onAction="#addPayment" prefHeight="30">
<Button fx:id="addPaymentButton" text="%wallet.send.pay-to.add-address" onAction="#addPayment" prefHeight="30">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="PLUS" />
</graphic>

View file

@ -45,7 +45,7 @@
<BorderPane GridPane.columnIndex="0" GridPane.rowIndex="0" GridPane.columnSpan="3">
<top>
<Form styleClass="title-form">
<Fieldset inputGrow="ALWAYS" text="Send"/>
<Fieldset inputGrow="ALWAYS" text="%wallet.send"/>
</Form>
</top>
<center>
@ -53,7 +53,7 @@
</center>
</BorderPane>
<Form styleClass="title-form" GridPane.columnIndex="0" GridPane.rowIndex="1">
<Fieldset inputGrow="ALWAYS" text="Fee"/>
<Fieldset inputGrow="ALWAYS" text="%wallet.send.fee"/>
</Form>
<HBox GridPane.columnIndex="1" GridPane.rowIndex="1" GridPane.columnSpan="2" GridPane.halignment="RIGHT" alignment="CENTER_RIGHT">
<padding>
@ -64,17 +64,17 @@
<ToggleGroup fx:id="feeSelectionToggleGroup" />
</toggleGroup>
<buttons>
<ToggleButton fx:id="targetBlocksToggle" text="Target Blocks" toggleGroup="$feeSelectionToggleGroup">
<ToggleButton fx:id="targetBlocksToggle" text="%wallet.send.fee.target-block" toggleGroup="$feeSelectionToggleGroup">
<tooltip>
<Tooltip text="Determine fee via estimated number of blocks"/>
<Tooltip text="%wallet.send.fee.target-block.tooltip"/>
</tooltip>
<userData>
<FeeRatesSelection fx:constant="BLOCK_TARGET"/>
</userData>
</ToggleButton>
<ToggleButton fx:id="mempoolSizeToggle" text="Mempool Size" toggleGroup="$feeSelectionToggleGroup">
<ToggleButton fx:id="mempoolSizeToggle" text="%wallet.send.fee.mempool-size" toggleGroup="$feeSelectionToggleGroup">
<tooltip>
<Tooltip text="Determine fee via current mempool size"/>
<Tooltip text="%wallet.send.fee.mempool-size.tooltip"/>
</tooltip>
<userData>
<FeeRatesSelection fx:constant="MEMPOOL_SIZE"/>
@ -85,13 +85,13 @@
</HBox>
<Form GridPane.columnIndex="0" GridPane.rowIndex="2">
<Fieldset inputGrow="SOMETIMES">
<Field fx:id="targetBlocksField" text="Blocks:">
<Field fx:id="targetBlocksField" text="%wallet.send.fee.blocks">
<Slider fx:id="targetBlocks" snapToTicks="true" showTickLabels="true" showTickMarks="true" />
</Field>
<Field fx:id="feeRangeField" text="Range:">
<Field fx:id="feeRangeField" text="%wallet.send.fee.range">
<FeeRangeSlider fx:id="feeRange" />
</Field>
<Field fx:id="feeRateField" text="Rate:">
<Field fx:id="feeRateField" text="%wallet.send.fee.rate">
<CopyableLabel fx:id="feeRate" />
<Label fx:id="cpfpFeeRate" text="CPFP">
<graphic>
@ -108,7 +108,7 @@
</graphic>
</Label>
</Field>
<Field text="Fee:">
<Field text="%wallet.send.fee.fee">
<TextField fx:id="fee" styleClass="amount-field"/>
<ComboBox fx:id="feeAmountUnit" styleClass="amount-unit">
<items>
@ -153,23 +153,23 @@
<Insets left="25.0" right="25.0" bottom="25.0" />
</padding>
<HBox AnchorPane.leftAnchor="5" alignment="CENTER_LEFT">
<Label text="Optimize:" styleClass="buttonRowLabel" />
<Label text="%wallet.send.optimize" styleClass="buttonRowLabel" />
<SegmentedButton>
<toggleGroup>
<ToggleGroup fx:id="optimizationToggleGroup" />
</toggleGroup>
<buttons>
<ToggleButton fx:id="efficiencyToggle" text="Efficiency" toggleGroup="$optimizationToggleGroup">
<ToggleButton fx:id="efficiencyToggle" text="%wallet.send.optimize.efficiency" toggleGroup="$optimizationToggleGroup">
<tooltip>
<Tooltip text="Smallest transaction size for lowest fees"/>
<Tooltip text="%wallet.send.optimize.efficiency.tootip"/>
</tooltip>
<userData>
<OptimizationStrategy fx:constant="EFFICIENCY"/>
</userData>
</ToggleButton>
<ToggleButton fx:id="privacyToggle" text="Privacy" toggleGroup="$optimizationToggleGroup">
<ToggleButton fx:id="privacyToggle" text="%wallet.send.optimize.privacy" toggleGroup="$optimizationToggleGroup">
<tooltip>
<Tooltip text="Higher entropy transactions that reduce probabilities in blockchain analysis"/>
<Tooltip text="%wallet.send.optimize.privacy.tooltip"/>
</tooltip>
<userData>
<OptimizationStrategy fx:constant="PRIVACY"/>
@ -177,22 +177,22 @@
</ToggleButton>
</buttons>
</SegmentedButton>
<HelpLabel fx:id="optimizationHelp" helpText="Determines whether to optimize the transaction for low fees or greater privacy" />
<Label fx:id="privacyAnalysis" graphicTextGap="5" text="Analysis..." styleClass="help-label">
<HelpLabel fx:id="optimizationHelp" helpText="%wallet.send.optimize.help" />
<Label fx:id="privacyAnalysis" graphicTextGap="5" text="%wallet.send.optimize.analysis" styleClass="help-label">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="11" icon="INFO_CIRCLE" />
</graphic>
</Label>
</HBox>
<HBox AnchorPane.rightAnchor="10">
<Button fx:id="clearButton" text="Clear" cancelButton="true" onAction="#clear" />
<Button fx:id="clearButton" text="%wallet.send.clear" cancelButton="true" onAction="#clear" />
<Region HBox.hgrow="ALWAYS" style="-fx-min-width: 20px" />
<Button fx:id="notificationButton" text="Broadcast Notification" contentDisplay="RIGHT" graphicTextGap="5" onAction="#broadcastNotification">
<Button fx:id="notificationButton" text="%wallet.send.broadcast-notification" contentDisplay="RIGHT" graphicTextGap="5" onAction="#broadcastNotification">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="SATELLITE_DISH" />
</graphic>
</Button>
<Button fx:id="createButton" text="Create Transaction" defaultButton="true" disable="true" contentDisplay="RIGHT" graphicTextGap="5" onAction="#createTransaction">
<Button fx:id="createButton" text="%wallet.send.create-transaction" defaultButton="true" disable="true" contentDisplay="RIGHT" graphicTextGap="5" onAction="#createTransaction">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="12" icon="ANGLE_DOUBLE_RIGHT" />
</graphic>

View file

@ -33,21 +33,21 @@
<RowConstraints />
</rowConstraints>
<Form GridPane.columnIndex="0" GridPane.rowIndex="0">
<Fieldset inputGrow="SOMETIMES" text="Transactions" styleClass="header">
<Field text="Balance:">
<Fieldset inputGrow="SOMETIMES" text="%wallet.transactions" styleClass="header">
<Field text="%wallet.transactions.balance">
<CopyableCoinLabel fx:id="balance"/><Region HBox.hgrow="ALWAYS"/><FiatLabel fx:id="fiatBalance" minWidth="110" />
</Field>
<Field text="Mempool:">
<CopyableCoinLabel fx:id="mempoolBalance" /><Region HBox.hgrow="ALWAYS"/><FiatLabel fx:id="fiatMempoolBalance" minWidth="110" />
</Field>
<Field text="Transactions:">
<Field text="%wallet.transactions.transactions">
<CopyableLabel fx:id="transactionCount" />
<Button fx:id="exportCsv" maxHeight="25" onAction="#exportCSV" translateY="-1" styleClass="icon-button">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" icon="ARROW_CIRCLE_DOWN" fontSize="12" />
</graphic>
<tooltip>
<Tooltip text="Export transactions as CSV" />
<Tooltip text="%wallet.transactions.csv-export" />
</tooltip>
</Button>
</Field>

View file

@ -11,7 +11,7 @@
<BorderPane stylesheets="@wallet.css, @../general.css" xmlns="http://javafx.com/javafx" xmlns:fx="http://javafx.com/fxml" fx:controller="com.sparrowwallet.sparrow.wallet.WalletController">
<left>
<VBox fx:id="walletMenuBox" styleClass="list-menu">
<ToggleButton VBox.vgrow="ALWAYS" text="Transactions" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity">
<ToggleButton VBox.vgrow="ALWAYS" text="%wallet.transactions" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity">
<toggleGroup>
<ToggleGroup fx:id="walletMenu" />
</toggleGroup>
@ -22,7 +22,7 @@
<Function fx:constant="TRANSACTIONS"/>
</userData>
</ToggleButton>
<ToggleButton VBox.vgrow="ALWAYS" text="Send" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$walletMenu">
<ToggleButton VBox.vgrow="ALWAYS" text="%wallet.send" contentDisplay="TOP" styleClass="list-item" maxHeight="Infinity" toggleGroup="$walletMenu">
<graphic>
<Glyph fontFamily="FontAwesome" icon="SEND" fontSize="20" />
</graphic>

View file

@ -15,7 +15,7 @@
<VBox spacing="20">
<HBox styleClass="title-area">
<HBox alignment="CENTER_LEFT">
<Label fx:id="title" text="Welcome to Sparrow" styleClass="title-label" />
<Label fx:id="title" text="%welcome.title" styleClass="title-label" />
</HBox>
<Region HBox.hgrow="ALWAYS"/>
<ImageView AnchorPane.rightAnchor="0">
@ -24,44 +24,48 @@
</HBox>
<VBox fx:id="welcomeBox" styleClass="content-area" spacing="20" prefHeight="370">
<VBox fx:id="step1" spacing="15">
<Label text="Introduction" styleClass="title-text">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="TOGGLE_OFF" styleClass="title-icon" />
</graphic>
</Label>
<Label text="Sparrow is a Bitcoin wallet with a focus on security and usability." wrapText="true" styleClass="content-text" />
<Label text="Sparrow can operate in both an online and offline mode. In the online mode it connects to a server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator." wrapText="true" styleClass="content-text" />
<Label text="The status bar at the bottom displays the connection status, as demonstrated below:" wrapText="true" styleClass="content-text" />
<HBox>
<Label text="%welcome.step1.introduction" styleClass="title-text">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="TOGGLE_OFF" styleClass="title-icon" />
</graphic>
</Label>
<Region HBox.hgrow="ALWAYS" />
<ComboBox fx:id="languages"/>
</HBox>
<Label text="%welcome.step1.label1" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step1.label2" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step1.label3" wrapText="true" styleClass="content-text" />
</VBox>
<VBox fx:id="step2" spacing="15">
<Label text="Connecting to a Public Server" styleClass="title-text">
<Label text="%welcome.step2.introduction" styleClass="title-text">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="TOGGLE_ON" styleClass="title-icon" />
</graphic>
</Label>
<Label text="If you are beginning your journey in self custody, or just storing a small amount, the easiest way to connect Sparrow to the Bitcoin blockchain is via one of the preconfigured public Electrum servers. " wrapText="true" styleClass="content-text" />
<Label text="However, although Sparrow only connects to servers that have a record of respecting privacy, it is still not ideal as you are sharing your transaction history and balance with them." wrapText="true" styleClass="content-text" />
<Label text="A yellow toggle means you are connected to a public server." wrapText="true" styleClass="content-text" />
<Label text="%welcome.step2.label1" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step2.label2" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step2.label3" wrapText="true" styleClass="content-text" />
</VBox>
<VBox fx:id="step3" spacing="15">
<Label text="Connecting to a Bitcoin Core node" styleClass="title-text">
<Label text="%welcome.step3.introduction" styleClass="title-text">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="TOGGLE_ON" styleClass="title-icon" />
</graphic>
</Label>
<Label text="If you are running your own Bitcoin Core node, you can configure Sparrow to connect to it directly. " wrapText="true" styleClass="content-text" />
<Label text="This means you are not sharing your transaction data, but be aware Bitcoin Core stores your balance, transactions and public keys unencrypted on that node, which is not ideal for true cold storage." wrapText="true" styleClass="content-text" />
<Label text="A green toggle means you are connected to a Bitcoin Core node." wrapText="true" styleClass="content-text" />
<Label text="%welcome.step3.label1" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step3.label2" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step3.label3" wrapText="true" styleClass="content-text" />
</VBox>
<VBox fx:id="step4" spacing="15">
<Label text="Connecting to a Private Electrum Server" styleClass="title-text">
<Label text="%welcome.step4.introduction" styleClass="title-text">
<graphic>
<Glyph fontFamily="Font Awesome 5 Free Solid" fontSize="20" icon="TOGGLE_ON" styleClass="title-icon" />
</graphic>
</Label>
<Label text="The most private way to connect is to your own Electrum server, which in turn connects to your Bitcoin Core node." wrapText="true" styleClass="content-text" />
<Label text="Because these servers index all Bitcoin transactions equally, your wallet transactions are never stored on the server in an identifiable way, and your server forgets your requests immediately after serving them." wrapText="true" styleClass="content-text" />
<Label text="A blue toggle means you are connected to a private Electrum server. You're now ready to configure a server and start using Sparrow!" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step4.label1" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step4.label2" wrapText="true" styleClass="content-text" />
<Label text="%welcome.step4.label3" wrapText="true" styleClass="content-text" />
</VBox>
<Region VBox.vgrow="ALWAYS" />
<StatusBar fx:id="serverStatus">

View file

@ -0,0 +1,122 @@
#common
common.selected=selected
common.excluded=excluded
#welcome
welcome.title=Welcome to Sparrow
welcome.step1.introduction=Introduction
welcome.step1.label1=Sparrow is a Bitcoin wallet with a focus on security and usability.
welcome.step1.label2=Sparrow can operate in both an online and offline mode. In the online mode it connects to a server to display transaction history. In the offline mode it is useful as a transaction editor and as an airgapped multisig coordinator.
welcome.step1.label3=The status bar at the bottom displays the connection status, as demonstrated below:
welcome.step2.introduction=Connecting to a Public Server
welcome.step2.label1=If you are beginning your journey in self custody, or just storing a small amount, the easiest way to connect Sparrow to the Bitcoin blockchain is via one of the preconfigured public Electrum servers.
welcome.step2.label2=However, although Sparrow only connects to servers that have a record of respecting privacy, it is still not ideal as you are sharing your transaction history and balance with them.
welcome.step2.label3=A yellow toggle means you are connected to a public server.
welcome.step3.introduction=Connecting to a Bitcoin Core node
welcome.step3.label1=If you are running your own Bitcoin Core node, you can configure Sparrow to connect to it directly.
welcome.step3.label2=This means you are not sharing your transaction data, but be aware Bitcoin Core stores your balance, transactions and public keys unencrypted on that node, which is not ideal for true cold storage.
welcome.step3.label3=A green toggle means you are connected to a Bitcoin Core node.
welcome.step4.introduction=Connecting to a Private Electrum Server
welcome.step4.label1=The most private way to connect is to your own Electrum server, which in turn connects to your Bitcoin Core node.
welcome.step4.label2=Because these servers index all Bitcoin transactions equally, your wallet transactions are never stored on the server in an identifiable way, and your server forgets your requests immediately after serving them.
welcome.step4.label3=A blue toggle means you are connected to a private Electrum server. You're now ready to configure a server and start using Sparrow!
welcome.offline=Offline
welcome.offline.tooltip=Demonstration only - you are not connected!
welcome.server-status.online=Connected (demonstration only)
welcome.server-status.online.public-server=Connected to a Public Server (demonstration only)
welcome.server-status.online.bitcoin-core=Connected to Bitcoin Core (demonstration only)
welcome.server-status.online.private-electrum=Connected to a Private Electrum Server (demonstration only)
welcome.next=Next
welcome.back=Back
welcome.configure-server=Configure Server
welcome.done=Done
welcome.configure-later=Later or Offline Mode
#Wallet common
wallet.deposit=Deposit
# wallet transactions
wallet.transactions=Transactions
wallet.transactions.balance=Balance:
wallet.transactions.transactions=Transactions:
wallet.transactions.csv-export=Export transactions as CSV
wallet.transactions.csv-export.error=Error exporting transactions as CSV
wallet.transactions.loading-history=Wallet loading history for
wallet.transactions.loading-finish=Finished loading.
wallet.transactions.loading-transactions=Loading transactions for
wallet.transactions.finding-transactions=Finding transactions for
wallet.transactions.retrieving=Retrieving
wallet.transactions.retrieving-transactions=transactions
wallet.transactions.retrieving-headers=block headers
wallet.transactions.retrieving-block-height=Retrieving block at height
wallet.transactions.retrieving-transaction=Retrieving transaction
wallet.transactions.table.date=Date
wallet.transactions.table.label=Label
wallet.transactions.table.value=Value
wallet.transactions.table.balance=Balance
wallet.transactions.table.loading=Loading transactions...
wallet.transactions.table.loading.error=Error loading transactions:
wallet.transactions.table.no-transactions=No transactions
# wallet send
wallet.send=Send
wallet.send.pay-to=Pay to:
wallet.send.pay-to.tooltip=Address, payment code or bitcoin: URI
wallet.send.pay-to.add-address=Add
wallet.send.label=Label:
wallet.send.label.tooltip=Required to label the transaction (privately in this wallet)
wallet.send.label.required=Required
wallet.send.amount=Amount:
wallet.send.amount.insuficient=Insufficient funds
wallet.send.amount.insuficient.tootip=Insufficient confirmed transaction value
wallet.send.amount.dust=Amount too low
wallet.send.amount.dust.tooltip=Amount below the Bitcoin network dust threshold for this address type
wallet.send.fee=Fee
wallet.send.fee.target-block=Target Blocks
wallet.send.fee.target-block.tooltip=Determine fee via estimated number of blocks
wallet.send.fee.mempool-size=Mempool Size
wallet.send.fee.mempool-size.tooltip=Determine fee via current mempool size
wallet.send.fee.blocks=Blocks:
wallet.send.fee.range=Range:
wallet.send.fee.rate=Rate:
wallet.send.fee.rate.below-minimum=Below Minimum
wallet.send.fee.rate.below-minimum.tooltip=Transactions at this fee rate are currently being purged from the default sized mempool
wallet.send.fee.rate.replace=Try Then Replace
wallet.send.fee.rate.replace.tooltip=Send a transaction, verify it appears in the destination wallet, then RBF to get it confirmed or sent to another address
wallet.send.fee.rate.overpaid=Overpaid
wallet.send.fee.rate.overpaid.tooltip=Transaction fees at this rate are likely higher than necessary
wallet.send.fee.rate.high-priority=High Priority
wallet.send.fee.rate.high-priority.tooltip=Typically confirms within minutes
wallet.send.fee.rate.medium-priority=Medium Priority
wallet.send.fee.rate.medium-priority.tooltip=Typically confirms within an hour or two
wallet.send.fee.rate.low-priority=Low Priority
wallet.send.fee.rate.low-priority.tooltip=Typically confirms in a day or longer
wallet.send.fee.fee=Fee:
wallet.send.optimize=Optimize:
wallet.send.optimize.efficiency=Efficiency
wallet.send.optimize.efficiency.tootip=Smallest transaction size for lowest fees
wallet.send.optimize.privacy=Privacy
wallet.send.optimize.privacy.tooltip=Higher entropy transactions that reduce probabilities in blockchain analysis
wallet.send.optimize.help=Determines whether to optimize the transaction for low fees or greater privacy
wallet.send.optimize.analysis=Analysis...
wallet.send.optimize.coinjoin=Appears as a two person coinjoin
wallet.send.optimize.coinjoin.mixes-address-types=Cannot fake coinjoin due to mixed address types
wallet.send.optimize.coinjoin.multiple-payments=Cannot fake coinjoin due to multiple payments
wallet.send.optimize.coinjoin.payjoin=Cannot fake coinjoin due to payjoin
wallet.send.optimize.coinjoin.coincontrol=Cannot fake coinjoin due to coin control
wallet.send.optimize.coinjoin.insufficient-funds=Cannot fake coinjoin due to insufficient funds
wallet.send.optimize.mixes-address-types=Address types different to the wallet indicate external payments
wallet.send.optimize.external-payments=Rounded payment amounts indicate external payments
wallet.send.optimize.reuse=Address reuse detected
wallet.send.optimize.self-transfer=Appears as a possible self transfer
wallet.send.clear=Clear
wallet.send.broadcast-notification=Broadcast Notification
wallet.send.create-transaction=Create Transaction
# Transaction diagram
diagram.transaction=Transaction
# settings
language=Language
language.en=English
language.es=Spanish

View file

@ -0,0 +1,122 @@
#common
common.selected=seleccionados
common.excluded=excluídos
#welcome
welcome.title=Bienvenido a Sparrow
welcome.step1.introduction=Introducción
welcome.step1.label1=Sparrow es una billetera de Bitcoin con un enfoque en la seguridad y la facilidad de uso.
welcome.step1.label2=Sparrow puede operar tanto en modo en línea como fuera de línea. En el modo en línea, se conecta a un servidor para mostrar el historial de transacciones. En el modo fuera de línea, es útil como editor de transacciones y como coordinador multisig desconectado.
welcome.step1.label3=La barra de estado en la parte inferior muestra el estado de la conexión, como se muestra a continuación:
welcome.step2.introduction=Conexión a un servidor público
welcome.step2.label1=Si estás comenzando tu camino en la auto custodia o solo almacenando una pequeña cantidad, la forma más fácil de conectar Sparrow a la blockchain de Bitcoin es a través de uno de los servidores públicos de Electrum preconfigurados.
welcome.step2.label2=Sin embargo, aunque Sparrow solo se conecta a servidores que tienen un historial de respetar la privacidad, aún no es lo ideal ya que estás compartiendo tu historial de transacciones y tu saldo con ellos.
welcome.step2.label3=Un interruptor amarillo significa que estás conectado a un servidor público.
welcome.step3.introduction=Conexión a un nodo de Bitcoin Core
welcome.step3.label1=Si estás ejecutando tu propio nodo de Bitcoin Core, puedes configurar Sparrow para que se conecte directamente a él.
welcome.step3.label2=Esto significa que no estás compartiendo tus datos de transacciones, pero ten en cuenta que Bitcoin Core almacena tu saldo, transacciones y claves públicas sin cifrar en ese nodo, lo cual no es ideal para un almacenamiento en frío verdadero.
welcome.step3.label3=Un interruptor verde significa que estás conectado a un nodo de Bitcoin Core.
welcome.step4.introduction=Conexión a un servidor privado de Electrum
welcome.step4.label1=La forma más privada de conectarse es a tu propio servidor de Electrum, que a su vez se conecta a tu nodo de Bitcoin Core.
welcome.step4.label2=Dado que estos servidores indexan todas las transacciones de Bitcoin por igual, las transacciones de tu billetera nunca se almacenan en el servidor de manera identificable, y tu servidor olvida tus solicitudes inmediatamente después de servirlas.
welcome.step4.label3=Un interruptor azul significa que estás conectado a un servidor privado de Electrum. ¡Ahora estás listo para configurar un servidor y empezar a usar Sparrow!
welcome.offline=Desconectado
welcome.offline.tooltip=Sólo demostración - ¡no estás conectado!
welcome.server-status.online=Conectado (sólo demostración)
welcome.server-status.online.public-server=Conectado a un Servidor Público (sólo demostración)
welcome.server-status.online.bitcoin-core=Conectado a Bitcoin Core (sólo demostración)
welcome.server-status.online.private-electrum=Conectado a un Servidor Privado Electrum (sólo demostración)
welcome.next=Siguiente
welcome.back=Anterior
welcome.configure-server=Configurar Servidor
welcome.done=Hecho
welcome.configure-later=Después o Modo Sin Conexción
#Wallet common
wallet.deposit=Depósito
# wallet transactions
wallet.transactions=Transacciones
wallet.transactions.balance=Saldo:
wallet.transactions.transactions=Transacciones:
wallet.transactions.csv-export=Exportar transacciones en CSV
wallet.transactions.csv-export.error=Error exportando transacciones a CSV
wallet.transactions.loading-history=Cargando historial de carga para la billetera
wallet.transactions.loading-finish=Carga terminada.
wallet.transactions.loading-transactions=Cargando transacciones para
wallet.transactions.finding-transactions=Encontrando transacciones para
wallet.transactions.retrieving=Recuperando
wallet.transactions.retrieving-transactions=transacciones
wallet.transactions.retrieving-headers=cabeceras de bloque
wallet.transactions.retrieving-block-height=Recuperando bloque en altura
wallet.transactions.retrieving-transaction=Recuperando transacción
wallet.transactions.table.date=Fecha
wallet.transactions.table.label=Etiqueta
wallet.transactions.table.value=Valor
wallet.transactions.table.balance=Saldo
wallet.transactions.table.loading=Cargando transacciones...
wallet.transactions.table.loading.error=Error cargando transacciones:
wallet.transactions.table.no-transactions=Sin transacciones
# wallet send
wallet.send=Enviar
wallet.send.pay-to=Pagar a:
wallet.send.pay-to.tooltip=Dirección, código de pago o bitcoin: URI
wallet.send.pay-to.add-address=Añadir
wallet.send.label=Etiqueta:
wallet.send.label.tooltip=Obligatorio etiquetar la transacción (privado para esta wallet)
wallet.send.label.required=Obligatorio
wallet.send.amount=Cantidad:
wallet.send.amount.insuficient=Fondos insuficientes
wallet.send.amount.insuficient.tootip=Valor de transacción confirmado insuficiente
wallet.send.amount.dust=Cantidad demasiado baja
wallet.send.amount.dust.tooltip=Cantidad por debajo del umbral de polvo de la red Bitcoin para este tipo de dirección
wallet.send.fee=Comisión
wallet.send.fee.target-block=Bloques objetivo
wallet.send.fee.target-block.tooltip=Determina la comisión en función del número estimado de bloques
wallet.send.fee.mempool-size=Tamaño Mempool
wallet.send.fee.mempool-size.tooltip=Determina la comisión en función del tamaño actual de la mempool
wallet.send.fee.blocks=Bloques:
wallet.send.fee.range=Rango:
wallet.send.fee.rate=Ratio:
wallet.send.fee.rate.below-minimum=Por debajo del mínimo
wallet.send.fee.rate.below-minimum.tooltip=Las transacciones con esta tasa de comisión están siendo eliminadas del mempool con tamaño predeterminado
wallet.send.fee.rate.replace=Probar y luego Reemplazar
wallet.send.fee.rate.replace.tooltip=Envía una transacción, verifica que aparezca en la billetera de destino, luego usa RBF para que se confirme o se envíe a otra dirección
wallet.send.fee.rate.overpaid=Sobrepago
wallet.send.fee.rate.overpaid.tooltip=Las comisiones de transacción a esta tasa probablemente son más altas de lo necesario
wallet.send.fee.rate.high-priority=Alta Prioridad
wallet.send.fee.rate.high-priority.tooltip=Normalmente se confirma en minutos
wallet.send.fee.rate.medium-priority=Prioridad Media
wallet.send.fee.rate.medium-priority.tooltip=Normalmente se confirma en una o dos horas
wallet.send.fee.rate.low-priority=Baja Prioridad
wallet.send.fee.rate.low-priority.tooltip=Normalmente se confirma en un día o más
wallet.send.fee.fee=Comisión:
wallet.send.optimize=Optimizar:
wallet.send.optimize.efficiency=Eficiencia
wallet.send.optimize.efficiency.tootip=Tamaño de transacción más pequeño para tarifas más bajas
wallet.send.optimize.privacy=Privacidad
wallet.send.optimize.privacy.tooltip=Transacciones de mayor entropía que reducen las probabilidades en el análisis de la cadena de bloques
wallet.send.optimize.help=Determina si se debe optimizar la transacción para obtener tarifas bajas o mayor privacidad
wallet.send.optimize.analysis=Análisis...
wallet.send.optimize.coinjoin=Aparece como un coinjoin de dos personas
wallet.send.optimize.coinjoin.mixes-address-types=No se puede simular coinjoin debido a tipos de direcciones mixtas
wallet.send.optimize.coinjoin.multiple-payments=No se puede simular coinjoin debido a pagos múltiples
wallet.send.optimize.coinjoin.payjoin=No se puede simular coinjoin debido a payjoin
wallet.send.optimize.coinjoin.coincontrol=No se puede simular coinjoin debido al control de monedas
wallet.send.optimize.coinjoin.insufficient-funds=No se puede simular coinjoin debido a fondos insuficientes
wallet.send.optimize.mixes-address-types=Los tipos de direcciones diferentes a las de la billetera indican pagos externos
wallet.send.optimize.external-payments=Las cantidades redondeadas de pagos indican pagos externos
wallet.send.optimize.reuse=Se ha detectado reutilización de direcciones
wallet.send.optimize.self-transfer=Aparece como una posible auto-transferencia
wallet.send.clear=Limpiar
wallet.send.broadcast-notification=Retransmitir Notificación
wallet.send.create-transaction=Crear Transacción
# Transaction diagram
diagram.transaction=Transacción
# settings
language=Idioma
language.en=Inglés
language.es=Español