diff --git a/proto/backend.proto b/proto/backend.proto index 445aa233b..fd17b2f3a 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -308,7 +308,8 @@ message TranslateArgValue { message FormatTimeSpanIn { enum Context { - ANSWER_BUTTONS = 0; + NORMAL = 0; + ANSWER_BUTTONS = 1; } float seconds = 1; diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index a9154cb73..290b63e3a 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -345,7 +345,11 @@ class RustBackend: ) ).translate_string - def format_time_span(self, seconds: float, context: FormatTimeSpanContext) -> str: + def format_time_span( + self, + seconds: float, + context: FormatTimeSpanContext = FormatTimeSpanContext.NORMAL, + ) -> str: return self._run_command( pb.BackendInput( format_time_span=pb.FormatTimeSpanIn(seconds=seconds, context=context) diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 38eb62818..d7fcf285c 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -28,8 +28,6 @@ class CardStats: def report(self) -> str: c = self.card - # pylint: disable=unnecessary-lambda - fmt = lambda x, **kwargs: fmtTimeSpan(x, short=True, **kwargs) self.txt = "" self.addLine(_("Added"), self.date(c.id / 1000)) first = self.col.db.scalar("select min(id) from revlog where cid = ?", c.id) @@ -52,7 +50,9 @@ class CardStats: next, ) if c.queue == QUEUE_TYPE_REV: - self.addLine(_("Interval"), fmt(c.ivl * 86400)) + self.addLine( + _("Interval"), self.col.backend.format_time_span(c.ivl * 86400) + ) self.addLine(_("Ease"), "%d%%" % (c.factor / 10.0)) self.addLine(_("Reviews"), "%d" % c.reps) self.addLine(_("Lapses"), "%d" % c.lapses) @@ -83,13 +83,8 @@ class CardStats: def date(self, tm) -> str: return time.strftime("%Y-%m-%d", time.localtime(tm)) - def time(self, tm) -> str: - s = "" - if tm >= 60: - s = fmtTimeSpan((tm / 60) * 60, short=True, point=-1, unit=1) - if tm % 60 != 0 or not s: - s += fmtTimeSpan(tm % 60, point=2 if not s else -1, short=True) - return s + def time(self, tm: float) -> str: + return self.col.backend.format_time_span(tm) # Collection stats diff --git a/pylib/anki/utils.py b/pylib/anki/utils.py index 76cc31610..e2c1ed78f 100644 --- a/pylib/anki/utils.py +++ b/pylib/anki/utils.py @@ -24,7 +24,7 @@ from html.entities import name2codepoint from typing import Iterable, Iterator, List, Optional, Tuple, Union from anki.db import DB -from anki.lang import _, ngettext +from anki.lang import ngettext _tmpdir: Optional[str] @@ -56,28 +56,10 @@ inTimeTable = { } -def shortTimeFmt(type: str) -> str: - return { - # T: year is an abbreviation for year. %s is a number of years - "years": _("%sy"), - # T: m is an abbreviation for month. %s is a number of months - "months": _("%smo"), - # T: d is an abbreviation for day. %s is a number of days - "days": _("%sd"), - # T: h is an abbreviation for hour. %s is a number of hours - "hours": _("%sh"), - # T: m is an abbreviation for minute. %s is a number of minutes - "minutes": _("%sm"), - # T: s is an abbreviation for second. %s is a number of seconds - "seconds": _("%ss"), - }[type] - - def fmtTimeSpan( time: Union[int, float], pad: int = 0, point: int = 0, - short: bool = False, inTime: bool = False, unit: int = 99, ) -> str: @@ -86,13 +68,10 @@ def fmtTimeSpan( time = convertSecondsTo(time, type) if not point: time = int(round(time)) - if short: - fmt = shortTimeFmt(type) + if inTime: + fmt = inTimeTable[type](_pluralCount(time, point)) else: - if inTime: - fmt = inTimeTable[type](_pluralCount(time, point)) - else: - fmt = timeTable[type](_pluralCount(time, point)) + fmt = timeTable[type](_pluralCount(time, point)) timestr = "%%%(a)d.%(b)df" % {"a": pad, "b": point} return locale.format_string(fmt % timestr, time) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 301683d95..8a3806954 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1451,13 +1451,10 @@ border: 1px solid #000; padding: 3px; '>%s""" if not entries: return "" s = "
" % _("Date") - s += ("" * 5) % ( - _("Type"), - _("Rating"), - _("Interval"), - _("Ease"), - _("Time"), - ) + s += "" % _("Type") + s += "" % _("Rating") + s += "" % _("Interval") + s += ("" * 2) % (_("Ease"), _("Time"),) cnt = 0 for (date, ease, ivl, factor, taken, type) in reversed(entries): cnt += 1 @@ -1485,13 +1482,14 @@ border: 1px solid #000; padding: 3px; '>%s""" if ivl == 0: ivl = _("0d") elif ivl > 0: - ivl = fmtTimeSpan(ivl * 86400, short=True) + ivl = fmtTimeSpan(ivl * 86400) else: ivl = cs.time(-ivl) - s += ("" * 5) % ( - tstr, - ease, - ivl, + s += "" % tstr + s += "" % ease + s += "" % ivl + + s += ("" * 2) % ( "%d%%" % (factor / 10) if factor else "", cs.time(taken), ) + "" diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index e4c1420fc..3aee1239e 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -11,7 +11,7 @@ use crate::media::check::MediaChecker; use crate::media::sync::MediaSyncProgress; use crate::media::MediaManager; use crate::sched::cutoff::{local_minutes_west_for_stamp, sched_timing_today}; -use crate::sched::timespan::answer_button_time; +use crate::sched::timespan::{answer_button_time, time_span}; use crate::template::{ render_card, without_legacy_template_directives, FieldMap, FieldRequirements, ParsedTemplate, RenderedNode, @@ -406,6 +406,7 @@ impl Backend { None => return "".to_string(), }; match context { + pb::format_time_span_in::Context::Normal => time_span(input.seconds, &self.i18n), pb::format_time_span_in::Context::AnswerButtons => { answer_button_time(input.seconds, &self.i18n) } diff --git a/rslib/src/i18n/scheduling.ftl b/rslib/src/i18n/scheduling.ftl index 5f735228f..2220368e3 100644 --- a/rslib/src/i18n/scheduling.ftl +++ b/rslib/src/i18n/scheduling.ftl @@ -9,3 +9,31 @@ answer-button-time-hours = {$amount}h answer-button-time-days = {$amount}d answer-button-time-months = {$amount}mo answer-button-time-years = {$amount}y + +## A span of time, such as the delay until a card is shown again, the +## amount of time taken to answer a card, and so on. + +time-span-seconds = { $amount -> + [one] {$amount} second + *[other] {$amount} seconds + } +time-span-minutes = { $amount -> + [one] {$amount} minute + *[other] {$amount} minutes + } +time-span-hours = { $amount -> + [one] {$amount} hour + *[other] {$amount} hours + } +time-span-days = { $amount -> + [one] {$amount} day + *[other] {$amount} days + } +time-span-months = { $amount -> + [one] {$amount} month + *[other] {$amount} months + } +time-span-years = { $amount -> + [one] {$amount} year + *[other] {$amount} years + } diff --git a/rslib/src/sched/timespan.rs b/rslib/src/sched/timespan.rs index d85f67fb0..b70458c99 100644 --- a/rslib/src/sched/timespan.rs +++ b/rslib/src/sched/timespan.rs @@ -3,11 +3,12 @@ use crate::i18n::{tr_args, I18n, StringsGroup}; +/// Short string like '4d' to place above answer buttons. pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String { let span = Timespan::from_secs(seconds).natural_span(); let amount = match span.unit() { TimespanUnit::Months | TimespanUnit::Years => span.as_unit(), - // we don't show fractional value except for months/years + // we don't show fractional values except for months/years _ => span.as_unit().round(), }; let unit = span.unit().as_str(); @@ -16,6 +17,17 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String { .trn(&format!("answer-button-time-{}", unit), args) } +/// Describe the given seconds using the largest appropriate unit +/// eg 70 seconds -> "1.17 minutes" +pub fn time_span(seconds: f32, i18n: &I18n) -> String { + let span = Timespan::from_secs(seconds).natural_span(); + let amount = span.as_unit(); + let unit = span.unit().as_str(); + let args = tr_args!["amount" => amount]; + i18n.get(StringsGroup::Scheduling) + .trn(&format!("time-span-{}", unit), args) +} + const SECOND: f32 = 1.0; const MINUTE: f32 = 60.0 * SECOND; const HOUR: f32 = 60.0 * MINUTE;
%s%s%s%s%s%s%s%s%s%s%s