diff --git a/core/src/apps/binance/layout.py b/core/src/apps/binance/layout.py index 48fddb6cb..e2a7422fd 100644 --- a/core/src/apps/binance/layout.py +++ b/core/src/apps/binance/layout.py @@ -7,9 +7,9 @@ from trezor.messages import ( BinanceTransferMsg, ButtonRequestType, ) +from trezor.strings import format_amount from trezor.ui.scroll import Paginated from trezor.ui.text import Text -from trezor.utils import format_amount from . import helpers diff --git a/core/src/apps/cardano/layout/__init__.py b/core/src/apps/cardano/layout/__init__.py index 172c5234b..53c2a7fb4 100644 --- a/core/src/apps/cardano/layout/__init__.py +++ b/core/src/apps/cardano/layout/__init__.py @@ -1,9 +1,10 @@ from micropython import const from trezor import ui +from trezor.strings import format_amount from trezor.ui.scroll import Paginated from trezor.ui.text import Text -from trezor.utils import chunks, format_amount +from trezor.utils import chunks from apps.common.confirm import confirm, hold_to_confirm diff --git a/core/src/apps/ethereum/layout.py b/core/src/apps/ethereum/layout.py index 51015ac9d..8fd3199cd 100644 --- a/core/src/apps/ethereum/layout.py +++ b/core/src/apps/ethereum/layout.py @@ -2,8 +2,9 @@ from ubinascii import hexlify from trezor import ui from trezor.messages import ButtonRequestType +from trezor.strings import format_amount from trezor.ui.text import Text -from trezor.utils import chunks, format_amount +from trezor.utils import chunks from apps.common.confirm import require_confirm, require_hold_to_confirm from apps.common.layout import split_address diff --git a/core/src/apps/lisk/layout.py b/core/src/apps/lisk/layout.py index 411c9163f..735a1bf40 100644 --- a/core/src/apps/lisk/layout.py +++ b/core/src/apps/lisk/layout.py @@ -1,7 +1,8 @@ from trezor import ui from trezor.messages import ButtonRequestType +from trezor.strings import format_amount from trezor.ui.text import Text -from trezor.utils import chunks, format_amount +from trezor.utils import chunks from .helpers import get_vote_tx_text diff --git a/core/src/apps/management/recovery_device/homescreen.py b/core/src/apps/management/recovery_device/homescreen.py index 117b3c3e9..e813e71da 100644 --- a/core/src/apps/management/recovery_device/homescreen.py +++ b/core/src/apps/management/recovery_device/homescreen.py @@ -2,7 +2,7 @@ import storage import storage.device import storage.recovery import storage.recovery_shares -from trezor import utils, wire, workflow +from trezor import strings, utils, wire, workflow from trezor.crypto import slip39 from trezor.crypto.hashlib import sha256 from trezor.errors import MnemonicError @@ -213,10 +213,7 @@ async def _request_share_next_screen(ctx: wire.GenericContext) -> None: ctx, content, "Enter", _show_remaining_groups_and_shares ) else: - if remaining[0] == 1: - text = "1 more share" - else: - text = "%d more shares" % remaining[0] + text = strings.format_plural("{count} more {plural}", remaining[0], "share") content = layout.RecoveryHomescreen(text, "needed to enter") await layout.homescreen_dialog(ctx, content, "Enter share") diff --git a/core/src/apps/management/recovery_device/layout.py b/core/src/apps/management/recovery_device/layout.py index 6eea99661..7896cd0e4 100644 --- a/core/src/apps/management/recovery_device/layout.py +++ b/core/src/apps/management/recovery_device/layout.py @@ -1,5 +1,5 @@ import storage.recovery -from trezor import ui, wire +from trezor import strings, ui, wire from trezor.crypto.slip39 import MAX_SHARE_COUNT from trezor.messages import ButtonRequestType from trezor.messages.ButtonAck import ButtonAck @@ -99,10 +99,11 @@ async def show_remaining_shares( for remaining, group in groups: if 0 < remaining < MAX_SHARE_COUNT: text = Text("Remaining Shares") - if remaining > 1: - text.bold("%s more shares starting" % remaining) - else: - text.bold("%s more share starting" % remaining) + text.bold( + strings.format_plural( + "{count} more {plural} starting", remaining, "share" + ) + ) for word in group: text.normal(word) pages.append(text) @@ -111,10 +112,11 @@ async def show_remaining_shares( ): text = Text("Remaining Shares") groups_remaining = group_threshold - shares_remaining.count(0) - if groups_remaining > 1: - text.bold("%s more groups starting" % groups_remaining) - elif groups_remaining > 0: - text.bold("%s more group starting" % groups_remaining) + text.bold( + strings.format_plural( + "{count} more {plural} starting", groups_remaining, "group" + ) + ) for word in group: text.normal(word) pages.append(text) diff --git a/core/src/apps/monero/layout/common.py b/core/src/apps/monero/layout/common.py index ab2155430..b36cc00d6 100644 --- a/core/src/apps/monero/layout/common.py +++ b/core/src/apps/monero/layout/common.py @@ -1,4 +1,4 @@ -from trezor import ui, utils +from trezor import strings, ui, utils from trezor.messages import ButtonRequestType from trezor.messages.ButtonAck import ButtonAck from trezor.messages.ButtonRequest import ButtonRequest @@ -59,7 +59,7 @@ def paginate_lines(lines, lines_per_page=5): def format_amount(value): - return "%s XMR" % utils.format_amount(value, 12) + return "%s XMR" % strings.format_amount(value, 12) def split_address(address): diff --git a/core/src/apps/nem/layout.py b/core/src/apps/nem/layout.py index acba46d99..0069a0f01 100644 --- a/core/src/apps/nem/layout.py +++ b/core/src/apps/nem/layout.py @@ -1,7 +1,7 @@ from trezor import ui from trezor.messages import ButtonRequestType +from trezor.strings import format_amount from trezor.ui.text import Text -from trezor.utils import format_amount from .helpers import NEM_MAX_DIVISIBILITY diff --git a/core/src/apps/nem/transfer/layout.py b/core/src/apps/nem/transfer/layout.py index b9942b28e..be2789a32 100644 --- a/core/src/apps/nem/transfer/layout.py +++ b/core/src/apps/nem/transfer/layout.py @@ -8,8 +8,8 @@ from trezor.messages import ( NEMTransactionCommon, NEMTransfer, ) +from trezor.strings import format_amount from trezor.ui.text import Text -from trezor.utils import format_amount from ..helpers import ( NEM_LEVY_PERCENTILE_DIVISOR_ABSOLUTE, diff --git a/core/src/apps/ripple/layout.py b/core/src/apps/ripple/layout.py index 05f557cbb..84b00ca81 100644 --- a/core/src/apps/ripple/layout.py +++ b/core/src/apps/ripple/layout.py @@ -1,7 +1,7 @@ from trezor import ui from trezor.messages import ButtonRequestType +from trezor.strings import format_amount from trezor.ui.text import Text -from trezor.utils import format_amount from . import helpers diff --git a/core/src/apps/stellar/layout.py b/core/src/apps/stellar/layout.py index 9312159ec..4744e7d16 100644 --- a/core/src/apps/stellar/layout.py +++ b/core/src/apps/stellar/layout.py @@ -1,4 +1,4 @@ -from trezor import ui, utils +from trezor import strings, ui, utils from trezor.messages import ButtonRequestType from trezor.ui.text import Text @@ -77,7 +77,7 @@ def format_amount(amount: int, ticker=True) -> str: t = "" if ticker: t = " XLM" - return utils.format_amount(amount, consts.AMOUNT_DECIMALS) + t + return strings.format_amount(amount, consts.AMOUNT_DECIMALS) + t def split(text): diff --git a/core/src/apps/tezos/layout.py b/core/src/apps/tezos/layout.py index 219f17e05..712b7eab5 100644 --- a/core/src/apps/tezos/layout.py +++ b/core/src/apps/tezos/layout.py @@ -1,8 +1,9 @@ from trezor import ui from trezor.messages import ButtonRequestType +from trezor.strings import format_amount from trezor.ui.scroll import Paginated from trezor.ui.text import Text -from trezor.utils import chunks, format_amount +from trezor.utils import chunks from apps.common.confirm import require_confirm, require_hold_to_confirm from apps.tezos.helpers import TEZOS_AMOUNT_DECIMALS diff --git a/core/src/apps/wallet/sign_tx/layout.py b/core/src/apps/wallet/sign_tx/layout.py index a9247e387..47c04f310 100644 --- a/core/src/apps/wallet/sign_tx/layout.py +++ b/core/src/apps/wallet/sign_tx/layout.py @@ -3,7 +3,8 @@ from ubinascii import hexlify from trezor import ui from trezor.messages import ButtonRequestType, OutputScriptType -from trezor.utils import chunks, format_amount +from trezor.strings import format_amount +from trezor.utils import chunks _LOCKTIME_TIMESTAMP_MIN_VALUE = const(500000000) diff --git a/core/src/apps/wallet/sign_tx/omni.py b/core/src/apps/wallet/sign_tx/omni.py index 6bbbbcd21..7d150bedf 100644 --- a/core/src/apps/wallet/sign_tx/omni.py +++ b/core/src/apps/wallet/sign_tx/omni.py @@ -1,6 +1,6 @@ from ustruct import unpack -from trezor.utils import format_amount +from trezor.strings import format_amount currencies = { 1: ("OMNI", True), diff --git a/core/src/trezor/strings.py b/core/src/trezor/strings.py new file mode 100644 index 000000000..d4a3bb135 --- /dev/null +++ b/core/src/trezor/strings.py @@ -0,0 +1,47 @@ +def format_amount(amount: int, decimals: int) -> str: + if amount < 0: + amount = -amount + sign = "-" + else: + sign = "" + d = pow(10, decimals) + s = ( + ("%s%d.%0*d" % (sign, amount // d, decimals, amount % d)) + .rstrip("0") + .rstrip(".") + ) + return s + + +def format_ordinal(number: int) -> str: + return str(number) + {1: "st", 2: "nd", 3: "rd"}.get( + 4 if 10 <= number % 100 < 20 else number % 10, "th" + ) + + +def format_plural(string: str, count: int, plural: str): + """ + Adds plural form to a string based on `count`. + !! Does not work with irregular words !! + + Example: + >>> format_plural("We need {count} more {plural}", 3, "share") + 'We need 3 more shares' + >>> format_plural("We need {count} more {plural}", 1, "share") + 'We need 1 more share' + >>> format_plural("{count} {plural}", 4, "candy") + '4 candies' + """ + if not all(s in string for s in ("{count}", "{plural}")): + # string needs to have {count} and {plural} inside + raise ValueError + + if count == 0 or count > 1: + if plural[-1] == "y": + plural = plural[:-1] + "ies" + elif plural[-1] in "hsxz": + plural = plural + "es" + else: + plural = plural + "s" + + return string.format(count=count, plural=plural) diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py index 009ec4141..587efdbfa 100644 --- a/core/src/trezor/utils.py +++ b/core/src/trezor/utils.py @@ -70,27 +70,6 @@ def chunks(items: Chunkable, size: int) -> Iterator[Chunkable]: yield items[i : i + size] -def format_amount(amount: int, decimals: int) -> str: - if amount < 0: - amount = -amount - sign = "-" - else: - sign = "" - d = pow(10, decimals) - s = ( - ("%s%d.%0*d" % (sign, amount // d, decimals, amount % d)) - .rstrip("0") - .rstrip(".") - ) - return s - - -def format_ordinal(number: int) -> str: - return str(number) + {1: "st", 2: "nd", 3: "rd"}.get( - 4 if 10 <= number % 100 < 20 else number % 10, "th" - ) - - if False: class HashContext(Protocol): diff --git a/core/tests/test_trezor.strings.py b/core/tests/test_trezor.strings.py new file mode 100644 index 000000000..1bd26093c --- /dev/null +++ b/core/tests/test_trezor.strings.py @@ -0,0 +1,34 @@ +from common import * + +from trezor import strings + + +class TestStrings(unittest.TestCase): + + def test_format_amount(self): + VECTORS = [ + (123456, 3, "123.456"), + (4242, 7, "0.0004242"), + (-123456, 3, "-123.456"), + (-4242, 7, "-0.0004242"), + ] + for v in VECTORS: + self.assertEqual(strings.format_amount(v[0], v[1]), v[2]) + + + def test_format_plural(self): + VECTORS = [ + ("We need {count} more {plural}", 3, "share", "We need 3 more shares"), + ("We need {count} more {plural}", 1, "share", "We need 1 more share"), + ("We need {count} more {plural}", 1, "candy", "We need 1 more candy"), + ("We need {count} more {plural}", 7, "candy", "We need 7 more candies"), + ] + for v in VECTORS: + self.assertEqual(strings.format_plural(v[0], v[1], v[2]), v[3]) + + with self.assertRaises(ValueError): + strings.format_plural("Hello", 1, "share") + + +if __name__ == '__main__': + unittest.main() diff --git a/core/tests/test_trezor.utils.py b/core/tests/test_trezor.utils.py index c62635a7b..0adba98f4 100644 --- a/core/tests/test_trezor.utils.py +++ b/core/tests/test_trezor.utils.py @@ -13,16 +13,6 @@ class TestUtils(unittest.TestCase): self.assertEqual(c[i].stop, 100 if (i == 14) else (i + 1) * 7) self.assertEqual(c[i].step, 1) - def test_format_amount(self): - VECTORS = [ - (123456, 3, "123.456"), - (4242, 7, "0.0004242"), - (-123456, 3, "-123.456"), - (-4242, 7, "-0.0004242"), - ] - for v in VECTORS: - self.assertEqual(utils.format_amount(v[0], v[1]), v[2]) - if __name__ == '__main__': unittest.main()