diff --git a/src/main/java/com/sparrowwallet/sparrow/AppController.java b/src/main/java/com/sparrowwallet/sparrow/AppController.java index a95a3f48..fe271af0 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppController.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppController.java @@ -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,9 @@ public class AppController implements Initializable { @FXML private ToggleGroup theme; + @FXML + private ToggleGroup language; + @FXML private CheckMenuItem openWalletsInNewWindows; private static final BooleanProperty openWalletsInNewWindowsProperty = new SimpleBooleanProperty(); @@ -373,6 +378,15 @@ public class AppController implements Initializable { selectedThemeToggle.ifPresent(toggle -> theme.selectToggle(toggle)); setTheme(null); + Language configLanguage = Config.get().getLanguage(); + if(configLanguage == null) { + configLanguage = LanguagesManager.DEFAULT_LANGUAGE; + Config.get().setLanguage(configLanguage); + } + final Language selectedLanguage = configLanguage; + Optional selectedLanguageToggle = language.getToggles().stream().filter(toggle -> selectedLanguage.equals(toggle.getUserData())).findFirst(); + selectedLanguageToggle.ifPresent(toggle -> language.selectToggle(toggle)); + openWalletsInNewWindowsProperty.set(Config.get().isOpenWalletsInNewWindows()); openWalletsInNewWindows.selectedProperty().bindBidirectional(openWalletsInNewWindowsProperty); hideEmptyUsedAddressesProperty.set(Config.get().isHideEmptyUsedAddresses()); @@ -488,7 +502,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 optionalMode = welcomeDialog.showAndWait(); if(optionalMode.isPresent() && optionalMode.get().equals(Mode.ONLINE)) { @@ -996,13 +1010,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); } @@ -1760,7 +1780,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(); @@ -2337,6 +2357,15 @@ public class AppController implements Initializable { EventManager.get().post(new ThemeChangedEvent(selectedTheme)); } + public void setLanguage(ActionEvent event) { + Language selectedLanguage = (Language)language.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) { @@ -3135,4 +3164,4 @@ public class AppController implements Initializable { } } } -} +} \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/AppServices.java b/src/main/java/com/sparrowwallet/sparrow/AppServices.java index cd4418e3..fa75e3e1 100644 --- a/src/main/java/com/sparrowwallet/sparrow/AppServices.java +++ b/src/main/java/com/sparrowwallet/sparrow/AppServices.java @@ -14,6 +14,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; @@ -551,7 +552,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(); diff --git a/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java b/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java index 8002db82..f2b7c7aa 100644 --- a/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java +++ b/src/main/java/com/sparrowwallet/sparrow/SparrowDesktop.java @@ -5,6 +5,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; @@ -53,7 +55,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 optionalMode = welcomeDialog.showAndWait(); if(optionalMode.isPresent()) { mode = optionalMode.get(); @@ -87,6 +89,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; diff --git a/src/main/java/com/sparrowwallet/sparrow/WelcomeController.java b/src/main/java/com/sparrowwallet/sparrow/WelcomeController.java index 1a00b2ad..6b996735 100644 --- a/src/main/java/com/sparrowwallet/sparrow/WelcomeController.java +++ b/src/main/java/com/sparrowwallet/sparrow/WelcomeController.java @@ -1,22 +1,30 @@ package com.sparrowwallet.sparrow; +import com.google.common.eventbus.Subscribe; 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.fxml.FXMLLoader; +import javafx.scene.control.ComboBox; +import javafx.scene.control.DialogPane; 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.io.IOException; +import java.util.Arrays; +import java.util.List; + public class WelcomeController { @FXML private VBox welcomeBox; @@ -36,10 +44,13 @@ public class WelcomeController { @FXML private StatusBar serverStatus; + @FXML + private ComboBox 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 +61,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 +117,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 +129,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 +139,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 +154,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 +166,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 +176,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 getLanguagesList() { + List languages = Arrays.asList(Language.values()); + return FXCollections.observableList(languages); + } +} \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/WelcomeDialog.java b/src/main/java/com/sparrowwallet/sparrow/WelcomeDialog.java index 3b2702a0..d653d564 100644 --- a/src/main/java/com/sparrowwallet/sparrow/WelcomeDialog.java +++ b/src/main/java/com/sparrowwallet/sparrow/WelcomeDialog.java @@ -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 { - 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 { 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 { 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); + } + } } diff --git a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java index 6f1ba858..21ae185b 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/CoinTreeTable.java @@ -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 { 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 { 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(); diff --git a/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java b/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java index dfc9fe71..a100aab2 100644 --- a/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java +++ b/src/main/java/com/sparrowwallet/sparrow/control/TransactionsTreeTable.java @@ -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 dateCol = new TreeTableColumn<>("Date"); + TreeTableColumn dateCol = new TreeTableColumn<>(LanguagesManager.getMessage("wallet.transactions.table.date")); dateCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { return new ReadOnlyObjectWrapper<>(param.getValue().getValue()); }); @@ -23,7 +24,7 @@ public class TransactionsTreeTable extends CoinTreeTable { dateCol.setSortable(true); getColumns().add(dateCol); - TreeTableColumn labelCol = new TreeTableColumn<>("Label"); + TreeTableColumn labelCol = new TreeTableColumn<>(LanguagesManager.getMessage("wallet.transactions.table.label")); labelCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { return param.getValue().getValue().labelProperty(); }); @@ -31,7 +32,7 @@ public class TransactionsTreeTable extends CoinTreeTable { labelCol.setSortable(true); getColumns().add(labelCol); - TreeTableColumn amountCol = new TreeTableColumn<>("Value"); + TreeTableColumn amountCol = new TreeTableColumn<>(LanguagesManager.getMessage("wallet.transactions.table.value")); amountCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { return new ReadOnlyObjectWrapper<>(param.getValue().getValue().getValue()); }); @@ -39,7 +40,7 @@ public class TransactionsTreeTable extends CoinTreeTable { amountCol.setSortable(true); getColumns().add(amountCol); - TreeTableColumn balanceCol = new TreeTableColumn<>("Balance"); + TreeTableColumn balanceCol = new TreeTableColumn<>(LanguagesManager.getMessage("wallet.transactions.table.balance")); balanceCol.setCellValueFactory((TreeTableColumn.CellDataFeatures param) -> { return param.getValue().getValue() instanceof TransactionEntry ? ((TransactionEntry)param.getValue().getValue()).balanceProperty() : new ReadOnlyObjectWrapper<>(null); }); diff --git a/src/main/java/com/sparrowwallet/sparrow/event/LanguageChangedInWelcomeEvent.java b/src/main/java/com/sparrowwallet/sparrow/event/LanguageChangedInWelcomeEvent.java new file mode 100644 index 00000000..0d101956 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/event/LanguageChangedInWelcomeEvent.java @@ -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; + } +} diff --git a/src/main/java/com/sparrowwallet/sparrow/i18n/Language.java b/src/main/java/com/sparrowwallet/sparrow/i18n/Language.java new file mode 100644 index 00000000..64221982 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/i18n/Language.java @@ -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; + } +} \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/i18n/LanguagesManager.java b/src/main/java/com/sparrowwallet/sparrow/i18n/LanguagesManager.java new file mode 100644 index 00000000..95c63420 --- /dev/null +++ b/src/main/java/com/sparrowwallet/sparrow/i18n/LanguagesManager.java @@ -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); } +} \ No newline at end of file diff --git a/src/main/java/com/sparrowwallet/sparrow/io/Config.java b/src/main/java/com/sparrowwallet/sparrow/io/Config.java index ce515122..c4fd1921 100644 --- a/src/main/java/com/sparrowwallet/sparrow/io/Config.java +++ b/src/main/java/com/sparrowwallet/sparrow/io/Config.java @@ -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; } diff --git a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java index c5a49272..6e355148 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/BatchedElectrumServerRpc.java @@ -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 getScriptHashHistory(Transport transport, Wallet wallet, Map pathScriptHashes, boolean failOnError) { PagedBatchRequestBuilder 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 subscribeScriptHashes(Transport transport, Wallet wallet, Map pathScriptHashes) { PagedBatchRequestBuilder 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 getBlockHeaders(Transport transport, Wallet wallet, Set blockHeights) { PagedBatchRequestBuilder 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 getTransactions(Transport transport, Wallet wallet, Set txids) { PagedBatchRequestBuilder 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); diff --git a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java index f407ea5f..ad33bd20 100644 --- a/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java +++ b/src/main/java/com/sparrowwallet/sparrow/net/SimpleElectrumServerRpc.java @@ -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 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(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 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(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 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(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 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(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()); diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java index 5c236c8a..e191805c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/TransactionsController.java @@ -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; @@ -96,7 +97,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); } @@ -109,7 +110,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); @@ -118,7 +119,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(); } @@ -212,7 +213,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(); } diff --git a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java index b8cc2149..7e946c4c 100644 --- a/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java +++ b/src/main/java/com/sparrowwallet/sparrow/wallet/WalletController.java @@ -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(); diff --git a/src/main/resources/com/sparrowwallet/sparrow/app.fxml b/src/main/resources/com/sparrowwallet/sparrow/app.fxml index 68123ac5..7bd511f5 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/app.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/app.fxml @@ -9,6 +9,7 @@ + @@ -64,6 +65,9 @@ + + + @@ -113,6 +117,20 @@ + + + + + + + + + + + + + + diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml index 27e46a7d..6d0a70f1 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/transactions.fxml @@ -33,21 +33,21 @@
-
- +
+ - + diff --git a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.fxml b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.fxml index 0cbdeeb2..0be3a1dd 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/wallet/wallet.fxml @@ -11,7 +11,7 @@ - + diff --git a/src/main/resources/com/sparrowwallet/sparrow/welcome.fxml b/src/main/resources/com/sparrowwallet/sparrow/welcome.fxml index efe2f22c..8ea07327 100644 --- a/src/main/resources/com/sparrowwallet/sparrow/welcome.fxml +++ b/src/main/resources/com/sparrowwallet/sparrow/welcome.fxml @@ -15,7 +15,7 @@ - @@ -24,44 +24,48 @@ - - - - - diff --git a/src/main/resources/i18n/messages_en.properties b/src/main/resources/i18n/messages_en.properties new file mode 100644 index 00000000..c5a632d7 --- /dev/null +++ b/src/main/resources/i18n/messages_en.properties @@ -0,0 +1,57 @@ +#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 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 + +# settings +language=Language +language.en=English +language.es=Spanish \ No newline at end of file diff --git a/src/main/resources/i18n/messages_es.properties b/src/main/resources/i18n/messages_es.properties new file mode 100644 index 00000000..bfa87dec --- /dev/null +++ b/src/main/resources/i18n/messages_es.properties @@ -0,0 +1,57 @@ +#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 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 + +# settings +language=Idioma +language.en=Inglés +language.es=Español \ No newline at end of file