diff --git a/proto/backend.proto b/proto/backend.proto index 581e6688d..a7fcb6c4e 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -21,6 +21,7 @@ enum StringsGroup { NETWORK = 5; STATISTICS = 6; FILTERING = 7; + SCHEDULING = 8; } // 1-15 reserved for future use; 2047 for errors diff --git a/pylib/anki/lang.py b/pylib/anki/lang.py index 94fcdc3ab..db0547be4 100644 --- a/pylib/anki/lang.py +++ b/pylib/anki/lang.py @@ -169,3 +169,9 @@ def set_lang(lang: str, locale_dir: str) -> None: "anki", gettext_dir, languages=[lang], fallback=True ) locale_folder = locale_dir + + +# strip off unicode isolation markers from a translated string +# for testing purposes +def without_unicode_isolation(s: str) -> str: + return s.replace("\u2068", "").replace("\u2069", "") diff --git a/pylib/anki/sched.py b/pylib/anki/sched.py index 3bc82972b..5884cfaca 100644 --- a/pylib/anki/sched.py +++ b/pylib/anki/sched.py @@ -12,7 +12,7 @@ from anki import hooks from anki.cards import Card from anki.consts import * from anki.lang import _ -from anki.utils import fmtTimeSpan, ids2str, intTime +from anki.utils import answer_button_time, ids2str, intTime # queue types: 0=new/cram, 1=lrn, 2=rev, 3=day lrn, -1=suspended, -2=buried # revlog types: 0=lrn, 1=rev, 2=relrn, 3=cram @@ -1351,11 +1351,11 @@ To study outside of the normal schedule, click the Custom Study button below.""" def nextIvlStr(self, card, ease, short=False): "Return the next interval for CARD as a string." - ivl = self.nextIvl(card, ease) - if not ivl: + ivl_secs = self.nextIvl(card, ease) + if not ivl_secs: return _("(end)") - s = fmtTimeSpan(ivl, short=short) - if ivl < self.col.conf["collapseTime"]: + s = answer_button_time(self.col, ivl_secs) + if ivl_secs < self.col.conf["collapseTime"]: s = "<" + s return s diff --git a/pylib/anki/schedv2.py b/pylib/anki/schedv2.py index 06d947cc0..565d32573 100644 --- a/pylib/anki/schedv2.py +++ b/pylib/anki/schedv2.py @@ -19,7 +19,7 @@ from anki.cards import Card from anki.consts import * from anki.lang import _ from anki.rsbackend import SchedTimingToday -from anki.utils import fmtTimeSpan, ids2str, intTime +from anki.utils import answer_button_time, ids2str, intTime # card types: 0=new, 1=lrn, 2=rev, 3=relrn # queue types: 0=new, 1=(re)lrn, 2=rev, 3=day (re)lrn, @@ -1545,11 +1545,11 @@ To study outside of the normal schedule, click the Custom Study button below.""" def nextIvlStr(self, card: Card, ease: int, short: bool = False) -> str: "Return the next interval for CARD as a string." - ivl = self.nextIvl(card, ease) - if not ivl: + ivl_secs = self.nextIvl(card, ease) + if not ivl_secs: return _("(end)") - s = fmtTimeSpan(ivl, short=short) - if ivl < self.col.conf["collapseTime"]: + s = answer_button_time(self.col, ivl_secs) + if ivl_secs < self.col.conf["collapseTime"]: s = "<" + s return s diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index 2fdcac15c..aea864c98 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -1,6 +1,8 @@ # Copyright: Ankitects Pty Ltd and contributors # License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html +from __future__ import annotations + # some add-ons expect json to be in the utils module import json # pylint: disable=unused-import import locale @@ -19,8 +21,10 @@ import traceback from contextlib import contextmanager from hashlib import sha1 from html.entities import name2codepoint -from typing import Any, Iterable, Iterator, List, Optional, Tuple, Union +from typing import Iterable, Iterator, List, Optional, Tuple, Union +import anki +from anki.backend_pb2 import StringsGroup from anki.db import DB from anki.lang import _, ngettext @@ -35,6 +39,22 @@ def intTime(scale: int = 1) -> int: return int(time.time() * scale) +# eg 70 seconds -> (1.16, "minutes") +def seconds_to_appropriate_unit(seconds: int) -> Tuple[float, str]: + unit, _ = optimalPeriod(seconds, 0, 99) + amount = convertSecondsTo(seconds, unit) + return (amount, unit) + + +def answer_button_time(col: anki.storage._Collection, seconds: int) -> str: + (amount, unit) = seconds_to_appropriate_unit(seconds) + if unit not in ("months", "years"): + amount = int(amount) + return col.backend.translate( + StringsGroup.SCHEDULING, f"answer-button-time-{unit}", amount=amount + ) + + timeTable = { "years": lambda n: ngettext("%s year", "%s years", n), "months": lambda n: ngettext("%s month", "%s months", n), @@ -114,7 +134,7 @@ def optimalPeriod(time: Union[int, float], point: int, unit: int) -> Tuple[str, return (type, max(point, 0)) -def convertSecondsTo(seconds: Union[int, float], type: str) -> Any: +def convertSecondsTo(seconds: Union[int, float], type: str) -> float: if type == "seconds": return seconds elif type == "minutes": diff --git a/pylib/tests/test_collection.py b/pylib/tests/test_collection.py index 623e16181..2fb6032ab 100644 --- a/pylib/tests/test_collection.py +++ b/pylib/tests/test_collection.py @@ -4,6 +4,7 @@ import os import tempfile from anki import Collection as aopen +from anki.lang import without_unicode_isolation from anki.rsbackend import StringsGroup from anki.stdmodels import addBasicModel, models from anki.utils import isWin @@ -153,10 +154,7 @@ def test_furigana(): def test_translate(): d = getEmptyCol() tr = d.backend.translate - - # strip off unicode separators - def no_uni(s: str) -> str: - return s.replace("\u2068", "").replace("\u2069", "") + no_uni = without_unicode_isolation assert tr(StringsGroup.TEST, "valid-key") == "a valid key" assert "invalid-key" in tr(StringsGroup.TEST, "invalid-key") diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index 5613badf2..edff07beb 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -5,6 +5,7 @@ import time from anki import hooks from anki.consts import * +from anki.lang import without_unicode_isolation from anki.utils import intTime from tests.shared import getEmptyCol as getEmptyColOrig @@ -397,9 +398,10 @@ def test_button_spacing(): c.flush() d.reset() ni = d.sched.nextIvlStr - assert ni(c, 2) == "2 days" - assert ni(c, 3) == "3 days" - assert ni(c, 4) == "4 days" + wo = without_unicode_isolation + assert wo(ni(c, 2)) == "2d" + assert wo(ni(c, 3)) == "3d" + assert wo(ni(c, 4)) == "4d" def test_overdue_lapse(): @@ -514,7 +516,7 @@ def test_nextIvl(): assert ni(c, 3) == 21600000 # (* 100 2.5 1.3 86400)28080000.0 assert ni(c, 4) == 28080000 - assert d.sched.nextIvlStr(c, 4) == "10.8 months" + assert without_unicode_isolation(d.sched.nextIvlStr(c, 4)) == "10.83mo" def test_misc(): diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index cbee96c2a..2b8f996ac 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -5,6 +5,7 @@ import time from anki import hooks from anki.consts import * +from anki.lang import without_unicode_isolation from anki.utils import intTime from tests.shared import getEmptyCol as getEmptyColOrig @@ -486,14 +487,15 @@ def test_button_spacing(): c.flush() d.reset() ni = d.sched.nextIvlStr - assert ni(c, 2) == "2 days" - assert ni(c, 3) == "3 days" - assert ni(c, 4) == "4 days" + wo = without_unicode_isolation + assert wo(ni(c, 2)) == "2d" + assert wo(ni(c, 3)) == "3d" + assert wo(ni(c, 4)) == "4d" # if hard factor is <= 1, then hard may not increase conf = d.decks.confForDid(1) conf["rev"]["hardFactor"] = 1 - assert ni(c, 2) == "1 day" + assert wo(ni(c, 2)) == "1d" def test_overdue_lapse(): @@ -611,7 +613,7 @@ def test_nextIvl(): assert ni(c, 3) == 21600000 # (* 100 2.5 1.3 86400)28080000.0 assert ni(c, 4) == 28080000 - assert d.sched.nextIvlStr(c, 4) == "10.8 months" + assert without_unicode_isolation(d.sched.nextIvlStr(c, 4)) == "10.83mo" def test_bury(): diff --git a/rslib/src/i18n/mod.rs b/rslib/src/i18n/mod.rs index 34746acd8..d31d183fe 100644 --- a/rslib/src/i18n/mod.rs +++ b/rslib/src/i18n/mod.rs @@ -61,6 +61,7 @@ fn ftl_fallback_for_group(group: StringsGroup) -> String { StringsGroup::Network => include_str!("network.ftl"), StringsGroup::Statistics => include_str!("statistics.ftl"), StringsGroup::Filtering => include_str!("filtering.ftl"), + StringsGroup::Scheduling => include_str!("scheduling.ftl"), } .to_string() } @@ -77,6 +78,7 @@ fn localized_ftl_for_group(group: StringsGroup, lang_ftl_folder: &Path) -> Optio StringsGroup::Network => "network.ftl", StringsGroup::Statistics => "statistics.ftl", StringsGroup::Filtering => "filtering.ftl", + StringsGroup::Scheduling => "scheduling.ftl", }); fs::read_to_string(&path) .map_err(|e| { diff --git a/rslib/src/i18n/scheduling.ftl b/rslib/src/i18n/scheduling.ftl new file mode 100644 index 000000000..5f735228f --- /dev/null +++ b/rslib/src/i18n/scheduling.ftl @@ -0,0 +1,11 @@ +## The next time a card will be shown, in a short form that will fit +## on the answer buttons. For example, English shows "4d" to +## represent the card will be due in 4 days, "3m" for 3 minutes, and +## "5mo" for 5 months. + +answer-button-time-seconds = {$amount}s +answer-button-time-minutes = {$amount}m +answer-button-time-hours = {$amount}h +answer-button-time-days = {$amount}d +answer-button-time-months = {$amount}mo +answer-button-time-years = {$amount}y