diff --git a/src/actions/account.js b/src/actions/account.js index 7049337b8..98aed4eef 100644 --- a/src/actions/account.js +++ b/src/actions/account.js @@ -1,3 +1,4 @@ +import i18next from 'i18next'; import actionTypes from '../constants/actions'; import { setSecondPassphrase, send } from '../utils/api/account'; import { registerDelegate } from '../utils/api/delegate'; @@ -62,7 +63,7 @@ export const secondPassphraseRegistered = ({ activePeer, secondPassphrase, accou type: transactionTypes.setSecondPassphrase, })); }).catch((error) => { - const text = (error && error.message) ? error.message : 'An error occurred while registering your second passphrase. Please try again.'; + const text = (error && error.message) ? error.message : i18next.t('An error occurred while registering your second passphrase. Please try again.'); dispatch(errorAlertDialogDisplayed({ text })); }); dispatch(passphraseUsed(account.passphrase)); @@ -88,7 +89,7 @@ export const delegateRegistered = ({ })); }) .catch((error) => { - const text = error && error.message ? `${error.message}.` : 'An error occurred while registering as delegate.'; + const text = error && error.message ? `${error.message}.` : i18next.t('An error occurred while registering as delegate.'); const actionObj = errorAlertDialogDisplayed({ text }); dispatch(actionObj); }); @@ -113,7 +114,7 @@ export const sent = ({ activePeer, account, recipientId, amount, passphrase, sec })); }) .catch((error) => { - const text = error && error.message ? `${error.message}.` : 'An error occurred while creating the transaction.'; + const text = error && error.message ? `${error.message}.` : i18next.t('An error occurred while creating the transaction.'); dispatch(errorAlertDialogDisplayed({ text })); }); dispatch(passphraseUsed(passphrase)); diff --git a/src/actions/transactions.js b/src/actions/transactions.js index bf88a67c2..04c24bfa9 100644 --- a/src/actions/transactions.js +++ b/src/actions/transactions.js @@ -10,6 +10,15 @@ export const transactionAdded = data => ({ type: actionTypes.transactionAdded, }); +/** + * An action to dispatch transactionsFailed + * + */ +export const transactionsFailed = data => ({ + data, + type: actionTypes.transactionsFailed, +}); + /** * An action to dispatch transactionsUpdated * diff --git a/src/actions/transactions.test.js b/src/actions/transactions.test.js index fcc9f87f5..ab9378407 100644 --- a/src/actions/transactions.test.js +++ b/src/actions/transactions.test.js @@ -1,7 +1,7 @@ import { expect } from 'chai'; import sinon from 'sinon'; import actionTypes from '../constants/actions'; -import { transactionAdded, transactionsUpdated, +import { transactionAdded, transactionsUpdated, transactionsFailed, transactionsLoaded, transactionsRequested } from './transactions'; import * as accountApi from '../utils/api/account'; @@ -20,6 +20,20 @@ describe('actions: transactions', () => { }); }); + describe('transactionsFailed', () => { + it('should create an action to transactionsFailed', () => { + const data = { + id: 'dummy', + }; + const expectedAction = { + data, + type: actionTypes.transactionsFailed, + }; + + expect(transactionsFailed(data)).to.be.deep.equal(expectedAction); + }); + }); + describe('transactionsUpdated', () => { it('should create an action to transactionsUpdated', () => { const data = { diff --git a/src/components/passphraseInput/index.js b/src/components/passphraseInput/index.js index edbe19a1f..6c8c8c654 100644 --- a/src/components/passphraseInput/index.js +++ b/src/components/passphraseInput/index.js @@ -21,7 +21,7 @@ class PassphraseInput extends React.Component { let error; if (!value) { - error = 'Required'; + error = this.props.t('Required'); } else if (!isValidPassphrase(value)) { error = this.getPassphraseValidationError(value); } else if (this.hasExtraWhitespace(value)) { @@ -35,7 +35,7 @@ class PassphraseInput extends React.Component { getPassphraseValidationError(passphrase) { const mnemonic = passphrase.trim().split(' '); if (mnemonic.length < 12) { - return `Passphrase should have 12 words, entered passphrase has ${mnemonic.length}`; + return this.props.t('Passphrase should have 12 words, entered passphrase has {{length}}', { length: mnemonic.length }); } const invalidWord = mnemonic.find(word => !inDictionary(word.toLowerCase())); @@ -43,12 +43,12 @@ class PassphraseInput extends React.Component { if (invalidWord.length >= 2 && invalidWord.length <= 8) { const validWord = findSimilarWord(invalidWord); if (validWord) { - return `Word "${invalidWord}" is not on the passphrase Word List. Most similar word on the list is "${findSimilarWord(invalidWord)}"`; + return this.props.t('Word "{{invalidWord}}" is not on the passphrase Word List. Most similar word on the list is "{{similarWord}}"', { invalidWord, similarWord: findSimilarWord(invalidWord) }); } } - return `Word "${invalidWord}" is not on the passphrase Word List.`; + return this.props.t('Word "{{invalidWord}}" is not on the passphrase Word List.', { invalidWord }); } - return 'Passphrase is not valid'; + return this.props.t('Passphrase is not valid'); } // eslint-disable-next-line class-methods-use-this @@ -60,11 +60,11 @@ class PassphraseInput extends React.Component { // eslint-disable-next-line class-methods-use-this getPassphraseWhitespaceError(passphrase) { if (passphrase.replace(/^\s+/, '') !== passphrase) { - return 'Passphrase contains unnecessary whitespace at the beginning'; + return this.props.t('Passphrase contains unnecessary whitespace at the beginning'); } else if (passphrase.replace(/\s+$/, '') !== passphrase) { - return 'Passphrase contains unnecessary whitespace at the end'; + return this.props.t('Passphrase contains unnecessary whitespace at the end'); } else if (passphrase.replace(/\s+/g, ' ') !== passphrase) { - return 'Passphrase contains extra whitespace between words'; + return this.props.t('Passphrase contains extra whitespace between words'); } return null; diff --git a/src/components/proxyDialog/index.js b/src/components/proxyDialog/index.js index dbcc209ef..1406a8747 100644 --- a/src/components/proxyDialog/index.js +++ b/src/components/proxyDialog/index.js @@ -1,5 +1,6 @@ import { Input, Button } from 'react-toolbox'; import React from 'react'; +import { translate } from 'react-i18next'; class ProxyDialog extends React.Component { @@ -19,7 +20,7 @@ class ProxyDialog extends React.Component { this.setState({ [name]: { value, - error: value === '' ? 'Required' : '', + error: value === '' ? this.props.t('Required') : '', }, }); } @@ -39,12 +40,12 @@ class ProxyDialog extends React.Component { To connect to Lisk network, you need to enter a username and password for proxy {this.props.authInfo.host}

- - + label={this.props.t('Submit')} type='submit' /> ); } } -export default ProxyDialog; +export default translate()(ProxyDialog); + diff --git a/src/components/proxyDialog/index.test.js b/src/components/proxyDialog/index.test.js index 8c7cf18c4..1a227d9f2 100644 --- a/src/components/proxyDialog/index.test.js +++ b/src/components/proxyDialog/index.test.js @@ -2,7 +2,7 @@ import React from 'react'; import { expect } from 'chai'; import { mount } from 'enzyme'; import { spy, mock } from 'sinon'; - +import i18n from '../../i18n'; import ProxyDialog from './'; describe('ProxyDialog', () => { @@ -14,6 +14,7 @@ describe('ProxyDialog', () => { callback: spy(), closeDialog: spy(), authInfo: { host: 'someProxy.com' }, + i18n, }; wrapper = mount(); }); diff --git a/src/components/verifyMessage/index.js b/src/components/verifyMessage/index.js index d3c504d39..25ce0ceb5 100644 --- a/src/components/verifyMessage/index.js +++ b/src/components/verifyMessage/index.js @@ -64,7 +64,7 @@ class VerifyMessage extends React.Component { onChange={this.handleChange.bind(this, 'signature')} /> {this.state.result ? - : + : null } diff --git a/src/constants/actions.js b/src/constants/actions.js index 657e590b1..a7446e622 100644 --- a/src/constants/actions.js +++ b/src/constants/actions.js @@ -24,6 +24,7 @@ const actionTypes = { loadingStarted: 'LOADING_STARTED', loadingFinished: 'LOADING_FINISHED', transactionAdded: 'TRANSACTION_ADDED', + transactionsFailed: 'TRANSACTIONS_FAILED', transactionsUpdated: 'TRANSACTIONS_UPDATED', transactionsLoaded: 'TRANSACTIONS_LOADED', transactionsReset: 'TRANSACTIONS_RESET', diff --git a/src/locales/de/common.json b/src/locales/de/common.json index 06ad44990..eaec7ce3d 100644 --- a/src/locales/de/common.json +++ b/src/locales/de/common.json @@ -1,15 +1,132 @@ { + " Make sure that you are using the latest version of Lisk Nano.": "Stelle sicher, dass du die neuste Version von Lisk Nano benutzt.", + "Add vote to": "Stimme für", + "Address": "Adresse", + "Address copied to clipboard": "Die Adresse wurde in die Zwischenablage kopiert", + "Amount": "Betrag", + "Approval": "Zustimmung", + "Back": "Zurück", + "Balance": "Guthaben", + "Becoming a delegate requires registration. You may choose your own delegate name, which can be used to promote your delegate. Only the top 101 delegates are eligible to forge. All fees are shared equally between the top 101 delegates.": "Damit du Delegierter werden kannst, musst du dich registrieren. Dabei kannst du dir einen eigenen Delegierten-Namen geben, mit dem du für dich werben kannst. Nur die Top 101 Delegierten haben die Möglichkeit neue Blöcke zu forgen. Alle Gebühren werden gleichmäßig unter den Top 101 Delegierten verteilt.", + "Block Id": "Block Id", + "Block height": "Blockhöhe", + "Blockchain Application Registration": "Registrierung für Blockchain Anwendungen", + "Cancel": "Abbrechen", + "Click to send all funds": "Klicken, um das gesamte Guthaben zu verschicken", + "Confirm": "Bestätigen", + "Connection re-established": "Verbindung wiederhergestellt", + "Copy address to clipboard": "Adresse in die Zwischenablage kopieren", + "Custom Node": "Individueller Node", + "Delegate": "Delegierter", + "Delegate Registration": "Registrierung als Delegierte", + "Delegate name": "Delegierten Name", + "Delegate registration was successfully submitted with username: \"{{username}}\". It can take several seconds before it is processed.": "Delegierten Registrierung ist erfolgreich versendet mit dem Benutzernamen: \"{{username}}\". Es kann einige Sekunden dauern bis es bestätigt ist.", + "Enter the missing word": "Gebe das fehlende Wort ein", + "Enter your passphrase": "Gebe deine Passphrase ein", + "Entered passphrase does not belong to the active account": "Die eingegebene Passphrase gehört nicht zum aktiven Konto", + "Failed to connect to node {{address}}": "Verbindung zum Node {{address}} fehlgeschlagen", + "Failed to connect: Node {{address}} is not active": "Verbindung fehlgeschlagen: Node {{address}} ist nicht aktiv", + "Fee": "Gebühr", + "Fee: LSK": "Gebühr: LSK", + "Fee: {{amount}} LSK": "Gebühr: {{amount}} LSK", + "Forget this account": "Dieses Konto vergessen", + "Forging": "Forging", + "From / To": "Von / An", + "Hide passphrase": "Passphrase ausblenden", + "Insufficient funds": "Nicht genügend Guthaben", + "Insufficient funds for {{amount}} LSK fee": "Nicht genügend Guthaben für {{amount}} LSK Gebühren", + "Invalid": "Ungültig", + "LSK Earned": "LSK verdient", + "LSK received": "LSK erhalten", + "Language": "Sprache", + "Last 24 hours": "Die letzten 24 Stunden", + "Lisk Address": "Lisk Adresse", + "Login": "Einloggen", + "Losing access to this passphrase will mean no funds can be sent from this account.": "Das Verlieren der Passphrase führt dazu, dass kein Geld mehr von diesem Konto versendet werden kann.", + "Mainnet": "Mainnet", + "Message": "Nachricht", + "Move your mouse to generate random bytes": "Bewege die Maustaste, um zufällige Bytes zu erzeugen", + "Multisignature Creation": "Multisignatur-Erstellung", + "Name": "Name", + "New Account": "Neues Konto", + "Next": "Weiter", + "No delegates found": "Keine Delegierten gefunden", + "Node address": "Node-Adresse", + "Note: After registration completes,": "Hinweis: Nachdem die Registrierung abgeschlossen ist,", + "Note: Digital Signatures and signed messages are not encrypted!": "Beachte: Digitale Signaturen und signierte Nachrichten sind nicht verschlüsselt!", + "Passphrase": "Passphrase", + "Passphrase of the account is saved till the end of the session.": "Die Passphrase für das Konto wird bis zum Ende der Sitzung gespeichert.", + "Passphrase of the account will be required to perform any transaction.": "Die Passphrase für das Konto wird benötigt um Transaktionen jeglicher Art durchzuführen.", + "Peer": "Peer", + "Please click Next, then move around your mouse randomly to generate a random passphrase.": "Bitte klicke auf \\\"Weiter\\\" und bewege dann deine Maus willkürlich über den Bildschirm, um eine zufällige Passphrase zu erzeugen.", + "Please keep it safe!": "Bitte sicher aufbewahren!", + "Press #{key} to copy": "Zum Kopieren #{key} drücken", + "Public Key": "öffentlicher Schlüssel", + "Rank": "Rang", + "Receive LSK": "LSK erhalten", + "Recipient Address": "Empfängeradresse", + "Register": "Registrieren", + "Register Second Passphrase": "Zweite Passphrase registrieren", + "Register as delegate": "Als Delegierter registrieren", + "Register second passphrase": "Eine zweite Passphrase registrieren", + "Remember this account": "Dieses Konto merken", + "Remove vote from": "Stimme entfernen", + "Repeat the transaction": "Transaktion wiederholen", + "Required": "Benötigt", + "Result": "Ergebnis", + "Result copied to clipboard": "Ergebnis wurde in die Zwischenablage kopiert", + "Reward": "Belohnung", + "Save account": "Konto speichern", + "Save your passphrase in a safe place": "Bewahre deine Passphrase an einem sicheren Ort auf", + "Search": "Suche", + "Search by username": "Suche nach Nutzername", + "Second Passphrase": "Zweite Passphrase", + "Second Signature Creation": "Zweite Signatur: Erstellung", + "Second passphrase registration was successfully submitted. It can take several seconds before it is processed.": "Die Registrierung der zweiten Passphrase wurde erfolgreich eingereicht. Es kann einige Sekunden dauern, bis sie verarbeitet wird.", + "Select a network": "Wähle ein Netzwerk", + "Send": "Senden", + "Send Lisk from Blockchain Application": "Lisk von einer Blockchain Anwendung verschicken", + "Send Lisk to Blockchain Application": "Lisk an eine Blockchain Anwendung schicken", + "Send to this address": "Zu dieser Adresse senden", + "Set maximum amount": "Bestimme die maximale Menge", + "Settings": "Einstellungen", + "Show passphrase": "Passphrase anzeigen", + "Sign and copy result to clipboard": "Unterzeichne und kopiere das Ergebnis in die Zwischenablage", + "Sign message": "Nachricht unterzeichnen", + "Signature": "Signatur", + "Signing a message with this tool indicates ownership of a privateKey (secret) and provides a level of proof that you are the owner of the key. Its important to bear in mind that this is not a 100% proof as computer systems can be compromised, but is still an effective tool for proving ownership of a particular publicKey/address pair.": "Das Signieren einer Nachricht impliziert den Besitz eines privaten Schlüssels (geheim) und ermöglicht dadurch den Beweis, dass man der Besitzer des Schlüssels ist. Es ist allerdings wichtig zu bedenken, dass dies kein 100% iger Beweis sein kann, da Computersysteme manipuliert sein können, doch es ist immernoch ein effektives Mittel um den Besitz eines bestimmten publicKey/Adress-Paares zu bestätigen.", + "Testnet": "Testnet", + "There are no transactions, yet.": "Bisher noch keine Transaktionen.", + "This account is protected by a second passphrase": "Dieses Konto ist durch eine zweite Passphrase geschützt", + "This passphrase is not recoverable and if you lose it, you will lose access to your account forever.": "Diese Passphrase ist nicht wiederherstellbar und sobald du sie verlierst, hast du auch den Zugang zu deinem Konto für immer verloren.", + "This will save public key of your account on this device, so next time it will launch without the need to log in. However, you will be prompted to enter the passphrase once you want to do any transaction.": "Hiermit wird der öffentliche Schlüssel deines Accounts auf diesem Gerät gespeichert, so dass der Client das nächste Mal starten wird, ohne dass man sich einloggen muss. Die Passphrase wird allerdings immer wieder abgefragt, wenn du eine Transaktion tätigen möchtest.", + "Time": "Zeit", + "Timestamp": "Zeitpunkt", + "Total fee": "Gesamtgebühr", + "Transaction Amount": "Transaktionsbetrag", + "Transaction ID": "Transaktions ID", + "Transactions": "Transaktionen", + "Unable to connect to the node": "Verbindung zum Node nicht möglich", + "Uptime": "Laufzeit", + "Verify message": "Nachricht verifizieren", + "Vote": "Stimme", + "Vote for delegates": "Für Delegierte stimmen", + "Voting": "Abstimmung", + "When you have the signature, you only need the publicKey of the signer in order to verify that the message came from the right private/publicKey pair. Be aware, everybody knowing the signature and the publicKey can verify the message. If ever there is a dispute, everybody can take the publicKey and signature to a judge and prove that the message is coming from the specific private/publicKey pair.": "Wenn du eine Signatur hast, brauchst du nur noch den öffentlichen Schlüssel des Unterzeichners, um zu überprüfen, ob die Nachricht von dem korrekten privaten/öffentlichen Schlüssel-Paar kommt. Bedenke dass jeder, der den öffentlichen Schlüssel besitzt, die Nachricht verifizieren kann. Wenn es eine Streitigkeit geben sollte, kann jeder den öffentlichen Schlüssel und die Signatur zu einem Richter bringen, und dort beweisen, dass die Nachricht von diesem speziellen privaten/öffentlichen Schlüssel-Paar kommt.", + "Yes! It's safe": "Ja! Es ist sicher", + "You can select up to {{count}} delegates in one voting turn.": "Du kannst für bis zu {{count}} Delegierte gleichzeitig abstimmen.", + "You can vote for up to {{count}} delegates in total.": "Du kannst insgesamt für {{count}} Delegierte stimmen.", + "You have not forged any blocks yet": "Du hast noch keine Blöcke geforged", + "You need to become a delegate to start forging. If you already registered to become a delegate, your registration hasn't been processed, yet.": "Um Blocks forgen (erstellen) zu können, musst du Delegierter werden. Wenn du dich schon als Delegierter registriert hast, wurde deine Registrierung noch nicht verarbeitet.", + "You've received {{value}} LSK.": "Du hast {{value}} LSK erhalten.", + "Your transaction of {{amount}} LSK to {{recipientAddress}} was accepted and will be processed in a few seconds.": "Deine Transaktion von {{amount}} LSK zu {{recipientAddress}} wurde akzeptiert und wird in wenigen Sekunden verarbeitet.", + "Your votes were successfully submitted. It can take several seconds before they are processed.": "Deine Stimmen wurden erfolgreich abgegeben. Es kann einige Sekunden dauern, bis sie verarbeitet sind.", + "Zero not allowed": "Null ist nicht erlaubt", + "confirmation": "Bestätigung", + "confirmations": "Bestätigungen", + "logout": "Abmelden", + "my votes": "Meine Abstimmungen", "send": "senden", - "logout": "Ausloggen", - "Register as delegate": "Register as delegate", - "Sign message": "Sign message", - "Verify message": "Verify message", - "register-second-passphrase": "Register second passphrase", - "peer": "Peer", - "balance": "Balance", - "address": "Address", - "delegate": "Delegate", - "transactions": "Transactions", - "voting": "Voting 1", - "forging": "Forging" + "your passphrase will be required for logging in to your account.": "Deine Passphrase wird benötigt, damit du auf dein Konto zugreifen kannst.", + "your second passphrase will be required for all transactions sent from this account": "Deine zweite Passphrase wird für alle Transaktionen benötigt, die von diesem Konto gesendet werden" } diff --git a/src/locales/en/common.json b/src/locales/en/common.json index ce0c19b50..db69d473d 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -1,9 +1,14 @@ { " Make sure that you are using the latest version of Lisk Nano.": " Make sure that you are using the latest version of Lisk Nano.", + "Account saved": "Account saved", + "Account was successfully forgotten.": "Account was successfully forgotten.", "Add vote to": "Add vote to", "Address": "Address", "Address copied to clipboard": "Address copied to clipboard", "Amount": "Amount", + "An error occurred while creating the transaction.": "An error occurred while creating the transaction.", + "An error occurred while registering as delegate.": "An error occurred while registering as delegate.", + "An error occurred while registering your second passphrase. Please try again.": "An error occurred while registering your second passphrase. Please try again.", "Approval": "Approval", "Back": "Back", "Balance": "Balance", @@ -61,14 +66,22 @@ "Note: After registration completes,": "Note: After registration completes,", "Note: Digital Signatures and signed messages are not encrypted!": "Note: Digital Signatures and signed messages are not encrypted!", "Ok": "Ok", + "Original Message": "Original Message", "Passphrase": "Passphrase", + "Passphrase contains extra whitespace between words": "Passphrase contains extra whitespace between words", + "Passphrase contains unnecessary whitespace at the beginning": "Passphrase contains unnecessary whitespace at the beginning", + "Passphrase contains unnecessary whitespace at the end": "Passphrase contains unnecessary whitespace at the end", + "Passphrase is not valid": "Passphrase is not valid", "Passphrase of the account is saved till the end of the session.": "Passphrase of the account is saved till the end of the session.", "Passphrase of the account will be required to perform any transaction.": "Passphrase of the account will be required to perform any transaction.", + "Passphrase should have 12 words, entered passphrase has {{length}}": "Passphrase should have 12 words, entered passphrase has {{length}}", + "Password": "Password", "Peer": "Peer", "Please click Next, then move around your mouse randomly to generate a random passphrase.": "Please click Next, then move around your mouse randomly to generate a random passphrase.", "Please keep it safe!": "Please keep it safe!", "Press #{key} to copy": "Press #{key} to copy", "Processing delegate names: ": "Processing delegate names: ", + "Proxy Authentication": "Proxy Authentication", "Public Key": "Public Key", "Rank": "Rank", "Receive LSK": "Receive LSK", @@ -103,8 +116,10 @@ "Sign message": "Sign message", "Signature": "Signature", "Signing a message with this tool indicates ownership of a privateKey (secret) and provides a level of proof that you are the owner of the key. Its important to bear in mind that this is not a 100% proof as computer systems can be compromised, but is still an effective tool for proving ownership of a particular publicKey/address pair.": "Signing a message with this tool indicates ownership of a privateKey (secret) and provides a level of proof that you are the owner of the key. Its important to bear in mind that this is not a 100% proof as computer systems can be compromised, but is still an effective tool for proving ownership of a particular publicKey/address pair.", + "Submit": "Submit", "Success": "Success", "Testnet": "Testnet", + "The URL was invalid": "The URL was invalid", "There are no transactions, yet.": "There are no transactions, yet.", "This account is protected by a second passphrase": "This account is protected by a second passphrase", "This passphrase is not recoverable and if you lose it, you will lose access to your account forever.": "This passphrase is not recoverable and if you lose it, you will lose access to your account forever.", @@ -117,14 +132,18 @@ "Transaction Amount": "Transaction Amount", "Transaction ID": "Transaction ID", "Transactions": "Transactions", + "URL is invalid": "URL is invalid", "Unable to connect to the node": "Unable to connect to the node", "Uptime": "Uptime", "Upvotes:": "Upvotes:", + "Username": "Username", "Verify message": "Verify message", "Vote": "Vote", "Vote for delegates": "Vote for delegates", "Voting": "Voting", "When you have the signature, you only need the publicKey of the signer in order to verify that the message came from the right private/publicKey pair. Be aware, everybody knowing the signature and the publicKey can verify the message. If ever there is a dispute, everybody can take the publicKey and signature to a judge and prove that the message is coming from the specific private/publicKey pair.": "When you have the signature, you only need the publicKey of the signer in order to verify that the message came from the right private/publicKey pair. Be aware, everybody knowing the signature and the publicKey can verify the message. If ever there is a dispute, everybody can take the publicKey and signature to a judge and prove that the message is coming from the specific private/publicKey pair.", + "Word \"{{invalidWord}}\" is not on the passphrase Word List.": "Word \"{{invalidWord}}\" is not on the passphrase Word List.", + "Word \"{{invalidWord}}\" is not on the passphrase Word List. Most similar word on the list is \"{{similarWord}}\"": "Word \"{{invalidWord}}\" is not on the passphrase Word List. Most similar word on the list is \"{{similarWord}}\"", "Yes! It's safe": "Yes! It's safe", "You can select up to {{count}} delegates in one voting turn.": "You can select up to {{count}} delegates in one voting turn.", "You can select up to {{count}} delegates in one voting turn._plural": "You can select up to {{count}} delegates in one voting turn.", diff --git a/src/store/middlewares/addedTransaction.js b/src/store/middlewares/addedTransaction.js deleted file mode 100644 index 5667846c8..000000000 --- a/src/store/middlewares/addedTransaction.js +++ /dev/null @@ -1,24 +0,0 @@ -import i18next from 'i18next'; -import actionTypes from '../../constants/actions'; -import { successAlertDialogDisplayed } from '../../actions/dialog'; -import { fromRawLsk } from '../../utils/lsk'; -import transactionTypes from '../../constants/transactionTypes'; - -const addedTransactionMiddleware = store => next => (action) => { - next(action); - if (action.type === actionTypes.transactionAdded) { - const texts = { - [transactionTypes.setSecondPassphrase]: i18next.t('Second passphrase registration was successfully submitted. It can take several seconds before it is processed.'), - [transactionTypes.registerDelegate]: i18next.t('Delegate registration was successfully submitted with username: "{{username}}". It can take several seconds before it is processed.', - { username: action.data.username }), - [transactionTypes.vote]: i18next.t('Your votes were successfully submitted. It can take several seconds before they are processed.'), - [transactionTypes.send]: i18next.t('Your transaction of {{amount}} LSK to {{recipientAddress}} was accepted and will be processed in a few seconds.', - { amount: fromRawLsk(action.data.amount), recipientAddress: action.data.recipientId }), - }; - const text = texts[action.data.type]; - const newAction = successAlertDialogDisplayed({ text }); - store.dispatch(newAction); - } -}; - -export default addedTransactionMiddleware; diff --git a/src/store/middlewares/addedTransaction.test.js b/src/store/middlewares/addedTransaction.test.js deleted file mode 100644 index 93150c9a2..000000000 --- a/src/store/middlewares/addedTransaction.test.js +++ /dev/null @@ -1,58 +0,0 @@ -import { expect } from 'chai'; -import { spy, stub } from 'sinon'; -import i18next from 'i18next'; -import { successAlertDialogDisplayed } from '../../actions/dialog'; -import middleware from './addedTransaction'; -import actionTypes from '../../constants/actions'; - -describe('addedTransaction middleware', () => { - let store; - let next; - - beforeEach(() => { - store = stub(); - store.getState = () => ({ - peers: { - data: {}, - }, - account: {}, - }); - store.dispatch = spy(); - next = spy(); - }); - - it('should passes the action to next middleware', () => { - const givenAction = { - type: 'TEST_ACTION', - }; - - middleware(store)(next)(givenAction); - expect(next).to.have.been.calledWith(givenAction); - }); - - it('fire success dialog action with appropriate text ', () => { - const givenAction = { - type: actionTypes.transactionAdded, - data: { - username: 'test', - amount: 1e8, - recipientId: '16313739661670634666L', - }, - }; - - const expectedMessages = [ - 'Your transaction of 1 LSK to 16313739661670634666L was accepted and will be processed in a few seconds.', - 'Second passphrase registration was successfully submitted. It can take several seconds before it is processed.', - 'Delegate registration was successfully submitted with username: "test". It can take several seconds before it is processed.', - 'Your votes were successfully submitted. It can take several seconds before they are processed.', - ]; - - for (let i = 0; i < 4; i++) { - givenAction.data.type = i; - middleware(store)(next)(givenAction); - const expectedAction = successAlertDialogDisplayed({ text: i18next.t(expectedMessages[i]) }); - expect(store.dispatch).to.have.been.calledWith(expectedAction); - } - }); -}); - diff --git a/src/store/middlewares/index.js b/src/store/middlewares/index.js index 41a5f13fe..a732e0146 100644 --- a/src/store/middlewares/index.js +++ b/src/store/middlewares/index.js @@ -2,7 +2,7 @@ import thunk from 'redux-thunk'; import metronomeMiddleware from './metronome'; import accountMiddleware from './account'; import loginMiddleware from './login'; -import addedTransactionMiddleware from './addedTransaction'; +import transactionsMiddleware from './transactions'; import loadingBarMiddleware from './loadingBar'; import offlineMiddleware from './offline'; import notificationMiddleware from './notification'; @@ -11,7 +11,7 @@ import savedAccountsMiddleware from './savedAccounts'; export default [ thunk, - addedTransactionMiddleware, + transactionsMiddleware, loginMiddleware, metronomeMiddleware, accountMiddleware, diff --git a/src/store/middlewares/savedAccounts.js b/src/store/middlewares/savedAccounts.js index 23d94ecb8..5144136d9 100644 --- a/src/store/middlewares/savedAccounts.js +++ b/src/store/middlewares/savedAccounts.js @@ -1,3 +1,4 @@ +import i18next from 'i18next'; import actionTypes from '../../constants/actions'; import { successToastDisplayed } from '../../actions/toaster'; @@ -5,10 +6,10 @@ const savedAccountsMiddleware = store => next => (action) => { next(action); switch (action.type) { case actionTypes.accountSaved: - store.dispatch(successToastDisplayed({ label: 'Account saved' })); + store.dispatch(successToastDisplayed({ label: i18next.t('Account saved') })); break; case actionTypes.accountRemoved: - store.dispatch(successToastDisplayed({ label: 'Account was successfully forgotten.' })); + store.dispatch(successToastDisplayed({ label: i18next.t('Account was successfully forgotten.') })); break; default: break; diff --git a/src/store/middlewares/transactions.js b/src/store/middlewares/transactions.js new file mode 100644 index 000000000..3980afbce --- /dev/null +++ b/src/store/middlewares/transactions.js @@ -0,0 +1,48 @@ +import i18next from 'i18next'; + +import { fromRawLsk } from '../../utils/lsk'; +import { unconfirmedTransactions } from '../../utils/api/account'; +import { successAlertDialogDisplayed } from '../../actions/dialog'; +import { transactionsFailed } from '../../actions/transactions'; +import actionTypes from '../../constants/actions'; +import transactionTypes from '../../constants/transactionTypes'; + +const transactionAdded = (store, action) => { + const texts = { + [transactionTypes.setSecondPassphrase]: i18next.t('Second passphrase registration was successfully submitted. It can take several seconds before it is processed.'), + [transactionTypes.registerDelegate]: i18next.t('Delegate registration was successfully submitted with username: "{{username}}". It can take several seconds before it is processed.', + { username: action.data.username }), + [transactionTypes.vote]: i18next.t('Your votes were successfully submitted. It can take several seconds before they are processed.'), + [transactionTypes.send]: i18next.t('Your transaction of {{amount}} LSK to {{recipientAddress}} was accepted and will be processed in a few seconds.', + { amount: fromRawLsk(action.data.amount), recipientAddress: action.data.recipientId }), + }; + const text = texts[action.data.type]; + const newAction = successAlertDialogDisplayed({ text }); + store.dispatch(newAction); +}; + +const transactionsUpdated = (store) => { + const { transactions, account, peers } = store.getState(); + if (transactions.pending.length) { + unconfirmedTransactions(peers.data, account.address) + .then(response => store.dispatch(transactionsFailed({ + failed: transactions.pending.filter(tx => + response.transactions.filter(unconfirmedTx => tx.id === unconfirmedTx.id).length === 0), + }))); + } +}; + +const transactionsMiddleware = store => next => (action) => { + next(action); + switch (action.type) { + case actionTypes.transactionAdded: + transactionAdded(store, action); + break; + case actionTypes.transactionsUpdated: + transactionsUpdated(store, action); + break; + default: break; + } +}; + +export default transactionsMiddleware; diff --git a/src/store/middlewares/transactions.test.js b/src/store/middlewares/transactions.test.js new file mode 100644 index 000000000..5c464bc3f --- /dev/null +++ b/src/store/middlewares/transactions.test.js @@ -0,0 +1,107 @@ +import { expect } from 'chai'; +import { spy, stub, mock } from 'sinon'; +import i18next from 'i18next'; +import * as accountApi from '../../utils/api/account'; +import { successAlertDialogDisplayed } from '../../actions/dialog'; +import { transactionsFailed } from '../../actions/transactions'; +import middleware from './transactions'; +import actionTypes from '../../constants/actions'; + +describe('transaction middleware', () => { + let store; + let next; + let state; + let accountApiMock; + const mockTransaction = { + username: 'test', + amount: 1e8, + recipientId: '16313739661670634666L', + }; + + beforeEach(() => { + store = stub(); + state = { + peers: { + data: {}, + }, + account: { + address: '8096217735672704724L', + }, + transactions: { + pending: [], + }, + }; + store.getState = () => (state); + store.dispatch = spy(); + next = spy(); + accountApiMock = mock(accountApi); + }); + + afterEach(() => { + accountApiMock.restore(); + }); + + it('should passes the action to next middleware', () => { + const givenAction = { + type: 'TEST_ACTION', + }; + + middleware(store)(next)(givenAction); + expect(next).to.have.been.calledWith(givenAction); + }); + + it('should fire success dialog action with appropriate text if action.type is transactionAdded', () => { + const givenAction = { + type: actionTypes.transactionAdded, + data: mockTransaction, + }; + + const expectedMessages = [ + 'Your transaction of 1 LSK to 16313739661670634666L was accepted and will be processed in a few seconds.', + 'Second passphrase registration was successfully submitted. It can take several seconds before it is processed.', + 'Delegate registration was successfully submitted with username: "test". It can take several seconds before it is processed.', + 'Your votes were successfully submitted. It can take several seconds before they are processed.', + ]; + + for (let i = 0; i < 4; i++) { + givenAction.data.type = i; + middleware(store)(next)(givenAction); + const expectedAction = successAlertDialogDisplayed({ text: i18next.t(expectedMessages[i]) }); + expect(store.dispatch).to.have.been.calledWith(expectedAction); + } + }); + + it('should do nothing if state.transactions.pending.length === 0 and action.type is transactionsUpdated', () => { + const givenAction = { + type: actionTypes.transactionsUpdated, + data: [mockTransaction], + }; + + middleware(store)(next)(givenAction); + expect(store.dispatch).to.not.have.been.calledWith(); + }); + + it('should call unconfirmedTransactions and then dispatch transactionsFailed if state.transactions.pending.length > 0 and action.type is transactionsUpdated', () => { + const transactions = [ + mockTransaction, + ]; + accountApiMock.expects('unconfirmedTransactions') + .withExactArgs(state.peers.data, state.account.address) + .returnsPromise().resolves({ transactions }); + store.getState = () => ({ + ...state, + transactions: { + pending: transactions, + }, + }); + const givenAction = { + type: actionTypes.transactionsUpdated, + data: [], + }; + + middleware(store)(next)(givenAction); + const expectedAction = transactionsFailed({ failed: [] }); + expect(store.dispatch).to.have.been.calledWith(expectedAction); + }); +}); + diff --git a/src/store/reducers/transactions.js b/src/store/reducers/transactions.js index 84dac0442..27e2815e0 100644 --- a/src/store/reducers/transactions.js +++ b/src/store/reducers/transactions.js @@ -11,6 +11,13 @@ const transactions = (state = { pending: [], confirmed: [], count: null }, actio return Object.assign({}, state, { pending: [action.data, ...state.pending], }); + case actionTypes.transactionsFailed: + return Object.assign({}, state, { + // Filter any failed transaction from pending + pending: state.pending.filter( + pendingTransaction => action.data.failed.filter( + transaction => transaction.id === pendingTransaction.id).length === 0), + }); case actionTypes.transactionsLoaded: return Object.assign({}, state, { confirmed: [ diff --git a/src/store/reducers/transactions.test.js b/src/store/reducers/transactions.test.js index 8aa4e43df..cd72e0c5b 100644 --- a/src/store/reducers/transactions.test.js +++ b/src/store/reducers/transactions.test.js @@ -3,6 +3,10 @@ import transactions from './transactions'; import actionTypes from '../../constants/actions'; describe('Reducer: transactions(state, action)', () => { + const defaultState = { + pending: [], + confirmed: [], + }; const mockTransactions = [{ amount: 100000000000, id: '16295820046284152875', @@ -19,8 +23,8 @@ describe('Reducer: transactions(state, action)', () => { it('should prepend action.data to state.pending if action.type = actionTypes.transactionAdded', () => { const state = { + ...defaultState, pending: [mockTransactions[1]], - confirmed: [], }; const action = { type: actionTypes.transactionAdded, @@ -31,10 +35,7 @@ describe('Reducer: transactions(state, action)', () => { }); it('should concat action.data to state.confirmed if action.type = actionTypes.transactionsLoaded', () => { - const state = { - pending: [], - confirmed: [], - }; + const state = { ...defaultState }; const action = { type: actionTypes.transactionsLoaded, data: { @@ -43,7 +44,7 @@ describe('Reducer: transactions(state, action)', () => { }, }; const expectedState = { - pending: [], + ...defaultState, confirmed: action.data.confirmed, count: action.data.count, }; @@ -53,6 +54,7 @@ describe('Reducer: transactions(state, action)', () => { it('should prepend newer transactions from action.data to state.confirmed and remove from state.pending if action.type = actionTypes.transactionsUpdated', () => { const state = { + ...defaultState, pending: [mockTransactions[0]], confirmed: [mockTransactions[1], mockTransactions[2]], count: mockTransactions[1].length + mockTransactions[2].length, @@ -66,7 +68,7 @@ describe('Reducer: transactions(state, action)', () => { }; const changedState = transactions(state, action); expect(changedState).to.deep.equal({ - pending: [], + ...defaultState, confirmed: mockTransactions, count: mockTransactions.length, }); @@ -74,8 +76,7 @@ describe('Reducer: transactions(state, action)', () => { it('should action.data to state.confirmed if state.confirmed is empty and action.type = actionTypes.transactionsUpdated', () => { const state = { - pending: [], - confirmed: [], + ...defaultState, }; const action = { type: actionTypes.transactionsUpdated, @@ -86,7 +87,7 @@ describe('Reducer: transactions(state, action)', () => { }; const changedState = transactions(state, action); expect(changedState).to.deep.equal({ - pending: [], + ...defaultState, confirmed: mockTransactions, count: mockTransactions.length, }); @@ -94,6 +95,7 @@ describe('Reducer: transactions(state, action)', () => { it('should reset all data if action.type = actionTypes.accountLoggedOut', () => { const state = { + ...defaultState, pending: [{ amount: 110000000000, id: '16295820046284152275', @@ -104,8 +106,7 @@ describe('Reducer: transactions(state, action)', () => { const action = { type: actionTypes.accountLoggedOut }; const changedState = transactions(state, action); expect(changedState).to.deep.equal({ - pending: [], - confirmed: [], + ...defaultState, count: 0, }); }); diff --git a/src/utils/api/account.js b/src/utils/api/account.js index 0ffdfdb42..20ed14460 100644 --- a/src/utils/api/account.js +++ b/src/utils/api/account.js @@ -35,6 +35,15 @@ export const transactions = (activePeer, address, limit = 20, offset = 0, orderB orderBy, }); +export const unconfirmedTransactions = (activePeer, address, limit = 20, offset = 0, orderBy = 'timestamp:desc') => + requestToActivePeer(activePeer, 'transactions/unconfirmed', { + senderId: address, + recipientId: address, + limit, + offset, + orderBy, + }); + export const extractPublicKey = passphrase => Lisk.crypto.getKeys(passphrase).publicKey; diff --git a/src/utils/api/account.test.js b/src/utils/api/account.test.js index 24de50fb6..46816e5af 100644 --- a/src/utils/api/account.test.js +++ b/src/utils/api/account.test.js @@ -1,6 +1,6 @@ import { expect } from 'chai'; import { mock } from 'sinon'; -import { getAccount, setSecondPassphrase, send, transactions, +import { getAccount, setSecondPassphrase, send, transactions, unconfirmedTransactions, extractPublicKey, extractAddress } from './account'; import { activePeerSet } from '../../actions/peers'; @@ -82,6 +82,13 @@ describe('Utils: Account', () => { }); }); + describe('unconfirmedTransactions', () => { + it('should return a promise', () => { + const promise = unconfirmedTransactions(); + expect(typeof promise.then).to.be.equal('function'); + }); + }); + describe('extractPublicKey', () => { it('should return a Hex string from any given string', () => { const passphrase = 'field organ country moon fancy glare pencil combine derive fringe security pave'; diff --git a/src/utils/externalLinks.js b/src/utils/externalLinks.js index 57778eb32..430db59c0 100644 --- a/src/utils/externalLinks.js +++ b/src/utils/externalLinks.js @@ -1,3 +1,4 @@ +import i18next from 'i18next'; import history from '../history'; import routesReg from './routes'; import { errorToastDisplayed } from '../actions/toaster'; @@ -14,7 +15,7 @@ export default { if (route !== undefined) { history.push(normalizedUrl); } else { - store.dispatch(errorToastDisplayed({ label: 'The URL was invalid' })); + store.dispatch(errorToastDisplayed({ label: i18next.t('The URL was invalid') })); } }); } diff --git a/src/utils/login.js b/src/utils/login.js index 863a39029..eb409df9d 100644 --- a/src/utils/login.js +++ b/src/utils/login.js @@ -1,10 +1,12 @@ +import i18next from 'i18next'; + const validateUrl = (value) => { const addHttp = (url) => { const reg = /^(?:f|ht)tps?:\/\//i; return reg.test(url) ? url : `http://${url}`; }; - const errorMessage = 'URL is invalid'; + const errorMessage = i18next.t('URL is invalid'); const isValidLocalhost = url => url.hostname === 'localhost' && url.port.length > 1; const isValidRemote = url => /(([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|((\d{1,3}\.){3}\d{1,3})/.test(url.hostname); diff --git a/src/utils/proxyLogin.js b/src/utils/proxyLogin.js index 033676773..2ed3e22f6 100644 --- a/src/utils/proxyLogin.js +++ b/src/utils/proxyLogin.js @@ -1,3 +1,4 @@ +import i18next from 'i18next'; import { dialogDisplayed } from '../actions/dialog'; import ProxyDialog from '../components/proxyDialog'; import store from '../store'; @@ -9,7 +10,7 @@ export default { if (ipc) { ipc.on('proxyLogin', (action, authInfo) => { store.dispatch(dialogDisplayed({ - title: 'Proxy Authentication', + title: i18next.t('Proxy Authentication'), childComponent: ProxyDialog, childComponentProps: { authInfo,