From 364f4152a81c40c34fab2e93d47f2ad0eab95288 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 15:43:56 +1000 Subject: [PATCH 01/30] change default night mode card background to dark grey --- qt/ts/scss/reviewer.scss | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qt/ts/scss/reviewer.scss b/qt/ts/scss/reviewer.scss index c5e05619e..f72859c99 100644 --- a/qt/ts/scss/reviewer.scss +++ b/qt/ts/scss/reviewer.scss @@ -1,6 +1,8 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ +@use 'vars'; + hr { background-color: #ccc; } @@ -11,8 +13,8 @@ body { } body.nightMode { - background-color: black; - color: white; + background-color: vars.$night-window-bg; + color: vars.$night-text-fg; } img { From 0fae85d2b43110b7d3e64838fcfee0572a107b8c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 15:46:39 +1000 Subject: [PATCH 02/30] don't pick up system dark theme in -alternate Mac build The older Qt doesn't support it properly --- qt/aqt/theme.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 91c9e782a..02ee1bd60 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -9,7 +9,8 @@ from typing import Dict from anki.utils import isMac from aqt import QApplication, gui_hooks, isWin from aqt.colors import colors -from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt +from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt, \ + qtminor class ThemeManager: @@ -23,6 +24,8 @@ class ThemeManager: return False if not isMac: return False + if qtminor < 14: + return False import darkdetect # pylint: disable=import-error return darkdetect.isDark() is True From 621774559f54a43089039145969a1d51e2d3bd2a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 16:20:14 +1000 Subject: [PATCH 03/30] don't allow UI scale < 100% Both Windows and Linux installs fail to render properly when the scale is below 100%: https://anki.tenderapp.com/discussions/ankidesktop/38909-user-interface-size Retina Macs do render properly, but they tend to display at appropriate size out of the box anyway. --- qt/aqt/profiles.py | 3 ++- qt/designer/preferences.ui | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/qt/aqt/profiles.py b/qt/aqt/profiles.py index 03f4280cb..ba605f401 100644 --- a/qt/aqt/profiles.py +++ b/qt/aqt/profiles.py @@ -485,7 +485,8 @@ please see: ###################################################################### def uiScale(self) -> float: - return self.meta.get("uiScale", 1.0) + scale = self.meta.get("uiScale", 1.0) + return max(scale, 1) def setUiScale(self, scale: float) -> None: self.meta["uiScale"] = scale diff --git a/qt/designer/preferences.ui b/qt/designer/preferences.ui index 3e7c9fca4..2e87ce297 100644 --- a/qt/designer/preferences.ui +++ b/qt/designer/preferences.ui @@ -246,7 +246,7 @@ % - 50 + 100 200 From 32b38bf26d1e1dd4754c0f3855c60cfcde83db9d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 16:25:17 +1000 Subject: [PATCH 04/30] ensure Anki starts on Windows systems that don't have TTS installed --- qt/aqt/sound.py | 4 ---- qt/aqt/tts.py | 7 ++++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index 0b0c5be8c..fcebb3662 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -639,10 +639,6 @@ def setup_audio(taskman: TaskManager, base_folder: str) -> None: mplayer = SimpleMplayerSlaveModePlayer(taskman) av_player.players.append(mplayer) - # currently unused - # mpv = SimpleMpvPlayer(base_folder) - # av_player.players.append(mpv) - # tts support if isMac: from aqt.tts import MacTTSPlayer diff --git a/qt/aqt/tts.py b/qt/aqt/tts.py index a78300e31..66dee8cbc 100644 --- a/qt/aqt/tts.py +++ b/qt/aqt/tts.py @@ -472,9 +472,14 @@ if isWin: return LCIDS.get(dec_str, "unknown") class WindowsTTSPlayer(TTSProcessPlayer): - speaker = win32com.client.Dispatch("SAPI.SpVoice") + try: + speaker = win32com.client.Dispatch("SAPI.SpVoice") + except: + speaker = None def get_available_voices(self) -> List[TTSVoice]: + if self.speaker is None: + return [] return list(map(self._voice_to_object, self.speaker.GetVoices())) def _voice_to_object(self, voice: Any): From fd835d9b64089d9856be6923ef45be9b4feee1c3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 17:00:02 +1000 Subject: [PATCH 05/30] fix display of 1+ day intervals in review history https://anki.tenderapp.com/discussions/ankidesktop/39189-interval-field-of-card-info-display-wrong-unit --- qt/aqt/browser.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index a46388eed..832a22017 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1494,6 +1494,8 @@ border: 1px solid #000; padding: 3px; '>%s""" if ivl == 0: ivl = "" else: + if ivl > 0: + ivl *= 86_400 ivl = cs.time(abs(ivl)) s += "%s" % tstr s += "%s" % ease From fbbbbd6a7d8dbc626a04e3861a03164f4cbbcebd Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 17:29:06 +1000 Subject: [PATCH 06/30] tweak rounding - avoid rounding minutes - round the seconds taken in the card info screen - provide different precise and imprecise modes, since we need to display seconds to multiple decimals in some areas --- proto/backend.proto | 3 ++- pylib/anki/rsbackend.py | 2 +- pylib/anki/stats.py | 6 ++++-- qt/aqt/browser.py | 2 +- rslib/src/backend.rs | 5 ++++- rslib/src/sched/timespan.rs | 41 ++++++++++++++++++++++++------------- 6 files changed, 39 insertions(+), 20 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index 2fcf913a4..cd4c0affb 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -307,8 +307,9 @@ message TranslateArgValue { message FormatTimeSpanIn { enum Context { - NORMAL = 0; + PRECISE = 0; ANSWER_BUTTONS = 1; + INTERVALS = 2; } float seconds = 1; diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 62ee71bff..6395a184f 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -342,7 +342,7 @@ class RustBackend: def format_time_span( self, seconds: float, - context: FormatTimeSpanContext = FormatTimeSpanContext.NORMAL, + context: FormatTimeSpanContext = FormatTimeSpanContext.INTERVALS, ) -> str: return self._run_command( pb.BackendInput( diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 133b7cf72..5be72f04a 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple import anki from anki.consts import * from anki.lang import _, ngettext -from anki.rsbackend import FString +from anki.rsbackend import FormatTimeSpanContext, FString from anki.utils import ids2str # Card stats @@ -85,7 +85,9 @@ class CardStats: return time.strftime("%Y-%m-%d", time.localtime(tm)) def time(self, tm: float) -> str: - return self.col.backend.format_time_span(tm) + return self.col.backend.format_time_span( + tm, context=FormatTimeSpanContext.PRECISE + ) # Collection stats diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 832a22017..d8ecb9fe3 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1503,7 +1503,7 @@ border: 1px solid #000; padding: 3px; '>%s""" s += ("%s" * 2) % ( "%d%%" % (factor / 10) if factor else "", - cs.time(taken), + self.col.backend.format_time_span(taken) ) + "" s += "" if cnt < self.card.reps: diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index 493471b6c..6a7f8d033 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -422,7 +422,10 @@ 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::Precise => time_span(input.seconds, &self.i18n, true), + pb::format_time_span_in::Context::Intervals => { + time_span(input.seconds, &self.i18n, false) + } pb::format_time_span_in::Context::AnswerButtons => { answer_button_time(input.seconds, &self.i18n) } diff --git a/rslib/src/sched/timespan.rs b/rslib/src/sched/timespan.rs index 9c496811e..f78156ad9 100644 --- a/rslib/src/sched/timespan.rs +++ b/rslib/src/sched/timespan.rs @@ -6,13 +6,7 @@ use crate::i18n::{tr_args, FString, I18n}; /// 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() { - // months/years shown with 1 decimal place - TimespanUnit::Months | TimespanUnit::Years => (span.as_unit() * 10.0).round() / 10.0, - // other values shown without decimals - _ => span.as_unit().round(), - }; - let args = tr_args!["amount" => amount]; + let args = tr_args!["amount" => span.as_rounded_unit()]; let key = match span.unit() { TimespanUnit::Seconds => FString::SchedulingAnswerButtonTimeSeconds, TimespanUnit::Minutes => FString::SchedulingAnswerButtonTimeMinutes, @@ -24,11 +18,17 @@ pub fn answer_button_time(seconds: f32, i18n: &I18n) -> String { i18n.trn(key, args) } -/// Describe the given seconds using the largest appropriate unit +/// Describe the given seconds using the largest appropriate unit. +/// If precise is true, show to two decimal places, eg /// eg 70 seconds -> "1.17 minutes" -pub fn time_span(seconds: f32, i18n: &I18n) -> String { +/// If false, seconds and days are shown without decimals. +pub fn time_span(seconds: f32, i18n: &I18n, precise: bool) -> String { let span = Timespan::from_secs(seconds).natural_span(); - let amount = span.as_unit(); + let amount = if precise { + span.as_unit() + } else { + span.as_rounded_unit() + }; let args = tr_args!["amount" => amount]; let key = match span.unit() { TimespanUnit::Seconds => FString::SchedulingTimeSpanSeconds, @@ -133,6 +133,17 @@ impl Timespan { } } + /// Round seconds and days to integers, otherwise + /// truncates to one decimal place. + fn as_rounded_unit(self) -> f32 { + match self.unit { + // seconds/days as integer + TimespanUnit::Seconds | TimespanUnit::Days => self.as_unit().round(), + // other values shown to 1 decimal place + _ => (self.as_unit() * 10.0).round() / 10.0, + } + } + fn unit(self) -> TimespanUnit { self.unit } @@ -173,16 +184,18 @@ mod test { fn answer_buttons() { let i18n = I18n::new(&["zz"], ""); assert_eq!(answer_button_time(30.0, &i18n), "30s"); - assert_eq!(answer_button_time(70.0, &i18n), "1m"); + assert_eq!(answer_button_time(70.0, &i18n), "1.2m"); assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.1mo"); } #[test] fn time_spans() { let i18n = I18n::new(&["zz"], ""); - assert_eq!(time_span(1.0, &i18n), "1 second"); - assert_eq!(time_span(30.0, &i18n), "30 seconds"); - assert_eq!(time_span(90.0, &i18n), "1.5 minutes"); + assert_eq!(time_span(1.0, &i18n, false), "1 second"); + assert_eq!(time_span(30.3, &i18n, false), "30 seconds"); + assert_eq!(time_span(30.3, &i18n, true), "30.3 seconds"); + assert_eq!(time_span(90.0, &i18n, false), "1.5 minutes"); + assert_eq!(time_span(45.0 * 86_400.0, &i18n, false), "1.5 months"); } #[test] From 06dee0ef08add7dc4a761e4b075030c6f31421a2 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 17:29:10 +1000 Subject: [PATCH 07/30] formatting fix --- qt/aqt/theme.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 02ee1bd60..47296dcd0 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -9,8 +9,7 @@ from typing import Dict from anki.utils import isMac from aqt import QApplication, gui_hooks, isWin from aqt.colors import colors -from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt, \ - qtminor +from aqt.qt import QColor, QIcon, QPalette, QPixmap, QStyleFactory, Qt, qtminor class ThemeManager: From f71484a4444941f1a017522daa14e291f8430b1b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 17:38:49 +1000 Subject: [PATCH 08/30] ensure user doesn't try to export into data folder https://anki.tenderapp.com/discussions/ankidesktop/39008-export-path-accidentally-set-to-collectionmedia-possible-damage --- qt/aqt/exporting.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py index 11f53808b..2e11aac84 100644 --- a/qt/aqt/exporting.py +++ b/qt/aqt/exporting.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 + import os import re import time @@ -15,7 +17,7 @@ from aqt.utils import checkInvalidFilename, getSaveFile, showWarning, tooltip class ExportDialog(QDialog): - def __init__(self, mw, did: Optional[int] = None, cids: Optional[List[int]] = None): + def __init__(self, mw: aqt.main.AnkiQt, did: Optional[int] = None, cids: Optional[List[int]] = None): QDialog.__init__(self, mw, Qt.Window) self.mw = mw self.col = mw.col @@ -118,6 +120,9 @@ class ExportDialog(QDialog): return if checkInvalidFilename(os.path.basename(file), dirsep=False): continue + if os.path.commonprefix([self.mw.pm.base, file]) == self.mw.pm.base: + showWarning("Please choose a different export location.") + continue break self.hide() if file: From db69f84c0a6a4f6d377e230343c5c19e4dd782a7 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 17:49:06 +1000 Subject: [PATCH 09/30] interrupt current audio when autoplay off --- qt/aqt/browser.py | 5 ++++- qt/aqt/reviewer.py | 4 ++++ qt/aqt/sound.py | 7 +++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index d8ecb9fe3..277627698 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1772,8 +1772,11 @@ where id in %s""" else: audio = c.answer_av_tags() av_player.play_tags(audio) + else: + av_player.maybe_interrupt() - txt = self.mw.prepare_card_text_for_display(txt) + + txt = self.mw.prepare_card_text_for_display(txt) txt = gui_hooks.card_will_show( txt, c, "preview" + self._previewState.capitalize() ) diff --git a/qt/aqt/reviewer.py b/qt/aqt/reviewer.py index 9200b15f6..316c33803 100644 --- a/qt/aqt/reviewer.py +++ b/qt/aqt/reviewer.py @@ -187,6 +187,8 @@ The front of this card is empty. Please run Tools>Empty Cards.""" # play audio? if self.autoplay(c): av_player.play_tags(c.question_av_tags()) + else: + av_player.maybe_interrupt() # render & update bottom q = self._mungeQA(q) @@ -230,6 +232,8 @@ The front of this card is empty. Please run Tools>Empty Cards.""" # play audio? if self.autoplay(c): av_player.play_tags(c.answer_av_tags()) + else: + av_player.maybe_interrupt() a = self._mungeQA(a) a = gui_hooks.card_will_show(a, c, "reviewAnswer") diff --git a/qt/aqt/sound.py b/qt/aqt/sound.py index fcebb3662..508a9611c 100644 --- a/qt/aqt/sound.py +++ b/qt/aqt/sound.py @@ -95,14 +95,17 @@ class AVPlayer: def play_tags(self, tags: List[AVTag]) -> None: """Clear the existing queue, then start playing provided tags.""" self._enqueued = tags[:] - if self.interrupt_current_audio: - self._stop_if_playing() + self.maybe_interrupt() self._play_next_if_idle() def stop_and_clear_queue(self) -> None: self._enqueued = [] self._stop_if_playing() + def maybe_interrupt(self) -> None: + if self.interrupt_current_audio: + self._stop_if_playing() + def play_file(self, filename: str) -> None: self.play_tags([SoundOrVideoTag(filename=filename)]) From 65ec9d190d7dd44fae5a92be6ca916d9c6e0c9d3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 17:56:46 +1000 Subject: [PATCH 10/30] formatting fixes --- qt/aqt/browser.py | 3 +-- qt/aqt/exporting.py | 7 ++++++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index 277627698..ea2b7657d 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -1503,7 +1503,7 @@ border: 1px solid #000; padding: 3px; '>%s""" s += ("%s" * 2) % ( "%d%%" % (factor / 10) if factor else "", - self.col.backend.format_time_span(taken) + self.col.backend.format_time_span(taken), ) + "" s += "" if cnt < self.card.reps: @@ -1775,7 +1775,6 @@ where id in %s""" else: av_player.maybe_interrupt() - txt = self.mw.prepare_card_text_for_display(txt) txt = gui_hooks.card_will_show( txt, c, "preview" + self._previewState.capitalize() diff --git a/qt/aqt/exporting.py b/qt/aqt/exporting.py index 2e11aac84..8b6657f8d 100644 --- a/qt/aqt/exporting.py +++ b/qt/aqt/exporting.py @@ -17,7 +17,12 @@ from aqt.utils import checkInvalidFilename, getSaveFile, showWarning, tooltip class ExportDialog(QDialog): - def __init__(self, mw: aqt.main.AnkiQt, did: Optional[int] = None, cids: Optional[List[int]] = None): + def __init__( + self, + mw: aqt.main.AnkiQt, + did: Optional[int] = None, + cids: Optional[List[int]] = None, + ): QDialog.__init__(self, mw, Qt.Window) self.mw = mw self.col = mw.col From a634188ec58d771faeff7f2d470bb953a25094b8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 26 Feb 2020 16:53:35 +1000 Subject: [PATCH 11/30] fix dark mode target Qt version --- qt/aqt/theme.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/theme.py b/qt/aqt/theme.py index 47296dcd0..6a25f9d6d 100644 --- a/qt/aqt/theme.py +++ b/qt/aqt/theme.py @@ -23,7 +23,7 @@ class ThemeManager: return False if not isMac: return False - if qtminor < 14: + if qtminor < 13: return False import darkdetect # pylint: disable=import-error From 46fadc2a8221514d7b3d60094c42e5aac32c55bf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 26 Feb 2020 17:47:02 +1000 Subject: [PATCH 12/30] don't abort build if mo file fails; rename files for consistency Issues are now checked for in CI, so there's no need to break the build for everyone each time there's a problem with the gettext catalogs. --- Makefile | 4 ++-- qt/Makefile | 6 ++++-- qt/i18n/build-mo-files | 18 ----------------- qt/i18n/pull-git | 11 ---------- qt/i18n/sync-po-git | 9 --------- qt/{i18n => po}/.gitignore | 2 +- qt/{i18n => po}/plurals.json | 0 qt/{i18n => po}/requirements.txt | 0 qt/po/scripts/build-mo-files | 20 +++++++++++++++++++ qt/{i18n => po/scripts}/copy-qt-files | 0 qt/{tools => po/scripts}/extract-po-string.py | 0 qt/po/scripts/fetch-latest-translations | 9 +++++++++ qt/{i18n => po/scripts}/update-po-template | 2 +- qt/po/scripts/upload-latest-template | 6 ++++++ 14 files changed, 43 insertions(+), 44 deletions(-) delete mode 100755 qt/i18n/build-mo-files delete mode 100755 qt/i18n/pull-git delete mode 100755 qt/i18n/sync-po-git rename qt/{i18n => po}/.gitignore (68%) rename qt/{i18n => po}/plurals.json (100%) rename qt/{i18n => po}/requirements.txt (100%) create mode 100755 qt/po/scripts/build-mo-files rename qt/{i18n => po/scripts}/copy-qt-files (100%) rename qt/{tools => po/scripts}/extract-po-string.py (100%) create mode 100755 qt/po/scripts/fetch-latest-translations rename qt/{i18n => po/scripts}/update-po-template (83%) create mode 100755 qt/po/scripts/upload-latest-template diff --git a/Makefile b/Makefile index f1a22e879..b74c1f877 100644 --- a/Makefile +++ b/Makefile @@ -112,10 +112,10 @@ add-buildhash: pull-i18n: (cd rslib/ftl && scripts/fetch-latest-translations) (cd qt/ftl && scripts/fetch-latest-translations) - (cd qt/i18n && ./pull-git) + (cd qt/po && scripts/fetch-latest-translations) .PHONY: push-i18n push-i18n: pull-i18n (cd rslib/ftl && scripts/upload-latest-templates) (cd qt/ftl && scripts/upload-latest-templates) - (cd qt/i18n && ./sync-po-git) + (cd qt/po && scripts/upload-latest-template) diff --git a/qt/Makefile b/qt/Makefile index 457d0012c..85a8db955 100644 --- a/qt/Makefile +++ b/qt/Makefile @@ -25,8 +25,10 @@ all: check ./tools/build_ui.sh @touch $@ -.build/i18n: $(wildcard i18n/po/desktop/*/anki.po) - (cd i18n && ./pull-git && ./build-mo-files && ./copy-qt-files) +.build/i18n: $(wildcard po/repo/desktop/*/anki.po) + (cd po && ./scripts/fetch-latest-translations && \ + ./scripts/build-mo-files && \ + ./scripts/copy-qt-files) @touch $@ TSDEPS := $(wildcard ts/src/*.ts) $(wildcard ts/scss/*.scss) diff --git a/qt/i18n/build-mo-files b/qt/i18n/build-mo-files deleted file mode 100755 index 8feaf7cfe..000000000 --- a/qt/i18n/build-mo-files +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash -# -# build mo files -# -set -eo pipefail - -targetDir="../aqt_data/locale/gettext" -mkdir -p $targetDir - -echo "Compiling *.po..." -for file in po/desktop/*/anki.po -do - outdir=$(echo "$file" | \ - perl -pe "s%po/desktop/(.*)/anki.po%$targetDir/\1/LC_MESSAGES%") - outfile="$outdir/anki.mo" - mkdir -p $outdir - msgmerge -q "$file" po/desktop/anki.pot | msgfmt - --output-file="$outfile" -done diff --git a/qt/i18n/pull-git b/qt/i18n/pull-git deleted file mode 100755 index aea1c5c43..000000000 --- a/qt/i18n/pull-git +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash - -if [ ! -d po ]; then - git clone https://github.com/ankitects/anki-desktop-i18n po -fi - -echo "Updating translations from git..." -(cd po && git pull) - -# make sure gettext translations haven't broken something -(cd po && python check-po-files.py) diff --git a/qt/i18n/sync-po-git b/qt/i18n/sync-po-git deleted file mode 100755 index 5d5910185..000000000 --- a/qt/i18n/sync-po-git +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash -set -eo pipefail - -# pull any pending changes from git repos -./pull-git - -# upload changes to .pot -./update-po-template -(cd po && git add desktop; git commit -m update; git push) diff --git a/qt/i18n/.gitignore b/qt/po/.gitignore similarity index 68% rename from qt/i18n/.gitignore rename to qt/po/.gitignore index ce405b957..cf4a945f6 100644 --- a/qt/i18n/.gitignore +++ b/qt/po/.gitignore @@ -1,3 +1,3 @@ .build -po +repo ftl diff --git a/qt/i18n/plurals.json b/qt/po/plurals.json similarity index 100% rename from qt/i18n/plurals.json rename to qt/po/plurals.json diff --git a/qt/i18n/requirements.txt b/qt/po/requirements.txt similarity index 100% rename from qt/i18n/requirements.txt rename to qt/po/requirements.txt diff --git a/qt/po/scripts/build-mo-files b/qt/po/scripts/build-mo-files new file mode 100755 index 000000000..d0653351e --- /dev/null +++ b/qt/po/scripts/build-mo-files @@ -0,0 +1,20 @@ +#!/bin/bash +# +# build mo files +# +set -eo pipefail + +targetDir="../aqt_data/locale/gettext" +mkdir -p $targetDir + +echo "Compiling *.repo..." +for file in repo/desktop/*/anki.po +do + outdir=$(echo "$file" | \ + perl -pe "s%repo/desktop/(.*)/anki.po%$targetDir/\1/LC_MESSAGES%") + outfile="$outdir/anki.mo" + mkdir -p $outdir + (msgmerge -q "$file" repo/desktop/anki.pot | msgfmt - --output-file="$outfile") || ( + echo "error building $file" + ) +done diff --git a/qt/i18n/copy-qt-files b/qt/po/scripts/copy-qt-files similarity index 100% rename from qt/i18n/copy-qt-files rename to qt/po/scripts/copy-qt-files diff --git a/qt/tools/extract-po-string.py b/qt/po/scripts/extract-po-string.py similarity index 100% rename from qt/tools/extract-po-string.py rename to qt/po/scripts/extract-po-string.py diff --git a/qt/po/scripts/fetch-latest-translations b/qt/po/scripts/fetch-latest-translations new file mode 100755 index 000000000..286745fe4 --- /dev/null +++ b/qt/po/scripts/fetch-latest-translations @@ -0,0 +1,9 @@ +#!/bin/bash + +echo "Downloading latest translations..." + +if [ ! -d repo ]; then + git clone https://github.com/ankitects/anki-desktop-i18n repo +fi + +(cd repo && git pull) diff --git a/qt/i18n/update-po-template b/qt/po/scripts/update-po-template similarity index 83% rename from qt/i18n/update-po-template rename to qt/po/scripts/update-po-template index a67197e01..de9f425e5 100755 --- a/qt/i18n/update-po-template +++ b/qt/po/scripts/update-po-template @@ -16,5 +16,5 @@ for i in qt/aqt/{*.py,forms/*.py}; do echo $i >> $all done -xgettext -cT: -s --no-wrap --files-from=$all --output=qt/i18n/po/desktop/anki.pot +xgettext -cT: -s --no-wrap --files-from=$all --output=qt/po/repo/desktop/anki.pot rm $all diff --git a/qt/po/scripts/upload-latest-template b/qt/po/scripts/upload-latest-template new file mode 100755 index 000000000..aec43a2d9 --- /dev/null +++ b/qt/po/scripts/upload-latest-template @@ -0,0 +1,6 @@ +#!/bin/bash + +set -eo pipefail + +./update-po-template +(cd repo && git add desktop; git commit -m update; git push) From cd0a09c8652ff94fa37c2b6d6d92dcb8937b275a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 26 Feb 2020 17:52:25 +1000 Subject: [PATCH 13/30] fetch qt/ftl/repo on first build needs to happen prior to develop step to ensure qt translations get compiled into rspy --- Makefile | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index b74c1f877..7854d30a1 100644 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ buildhash: fi .PHONY: develop -develop: pyenv buildhash +develop: pyenv buildhash prepare @set -eo pipefail && \ . pyenv/bin/activate && \ for dir in $(DEVEL); do \ @@ -47,6 +47,13 @@ run: develop echo "Starting Anki..."; \ qt/runanki $(RUNFLAGS) +.PHONY: prepare +prepare: rslib/ftl/repo qt/ftl/repo qt/po/repo + +rslib/ftl/repo: pull-i18n +qt/ftl/repo: pull-i18n +qt/po/repo: pull-i18n + .PHONY: build build: clean-dist build-rspy build-pylib build-qt add-buildhash @echo From e8b2182d79fc060d7ac68b78d02bd0bcf4546b1b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 26 Feb 2020 18:05:32 +1000 Subject: [PATCH 14/30] mention need to update translations and link to docs --- README.contributing | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.contributing b/README.contributing index 80a2bd88c..b04c808e0 100644 --- a/README.contributing +++ b/README.contributing @@ -96,6 +96,16 @@ pylib/tools/genhooks.py and qt/tools/genhooks_gui.py. Adding a new definition in one of those files and running 'make develop' will update pylib/anki/hooks .py or qt/aqt/gui_hooks.py. +Translations +-------------- + +The translations into other languages will be fetched on the first build. +If you'd like to keep them up to date, you need to run 'make pull-i18n' +periodically. + +For information on adding new translatable strings to Anki, please see +https://ankitects.github.io/translating/#/anki/developers + Tests Must Pass ---------------- From 4983eb8e3dff1fadbc7b4141b4a43653e68e2648 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 26 Feb 2020 18:09:06 +1000 Subject: [PATCH 15/30] fix push-i18n --- qt/po/scripts/update-po-template | 2 +- qt/po/scripts/upload-latest-template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/qt/po/scripts/update-po-template b/qt/po/scripts/update-po-template index de9f425e5..223cada7a 100755 --- a/qt/po/scripts/update-po-template +++ b/qt/po/scripts/update-po-template @@ -4,7 +4,7 @@ # set -eo pipefail -topDir=$(dirname $0)/../.. +topDir=$(dirname $0)/../../../ cd $topDir all=all.files diff --git a/qt/po/scripts/upload-latest-template b/qt/po/scripts/upload-latest-template index aec43a2d9..cef056142 100755 --- a/qt/po/scripts/upload-latest-template +++ b/qt/po/scripts/upload-latest-template @@ -2,5 +2,5 @@ set -eo pipefail -./update-po-template +scripts/update-po-template (cd repo && git add desktop; git commit -m update; git push) From 2555990c9c3462287b15164c8c1f477186cea875 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 26 Feb 2020 18:32:47 +1000 Subject: [PATCH 16/30] fix pull-i18n running on every build --- Makefile | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 7854d30a1..7c0126405 100644 --- a/Makefile +++ b/Makefile @@ -50,9 +50,12 @@ run: develop .PHONY: prepare prepare: rslib/ftl/repo qt/ftl/repo qt/po/repo -rslib/ftl/repo: pull-i18n -qt/ftl/repo: pull-i18n -qt/po/repo: pull-i18n +rslib/ftl/repo: + pull-i18n +qt/ftl/repo: + pull-i18n +qt/po/repo: + pull-i18n .PHONY: build build: clean-dist build-rspy build-pylib build-qt add-buildhash From e4bce71f2be951e649600b09f8c2760538e51fcf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 26 Feb 2020 20:07:55 +1000 Subject: [PATCH 17/30] need to invoke make for pull-i18n suggestions on a more elegant want to avoid executing these rules every time without needing to invoke make welcome! --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 7c0126405..60448e2de 100644 --- a/Makefile +++ b/Makefile @@ -51,11 +51,11 @@ run: develop prepare: rslib/ftl/repo qt/ftl/repo qt/po/repo rslib/ftl/repo: - pull-i18n + $(MAKE) pull-i18n qt/ftl/repo: - pull-i18n + $(MAKE) pull-i18n qt/po/repo: - pull-i18n + $(MAKE) pull-i18n .PHONY: build build: clean-dist build-rspy build-pylib build-qt add-buildhash From cfe4af81cfc3e14f55b4ac11181ce714585611bf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 26 Feb 2020 20:36:59 +1000 Subject: [PATCH 18/30] add missing short argument to legacy fmtTimeSpan() --- qt/aqt/legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/aqt/legacy.py b/qt/aqt/legacy.py index 0318a2e44..79fd1f942 100644 --- a/qt/aqt/legacy.py +++ b/qt/aqt/legacy.py @@ -33,7 +33,7 @@ def stripSounds(text) -> str: return aqt.mw.col.backend.strip_av_tags(text) -def fmtTimeSpan(time, pad=0, point=0, inTime=False, unit=99): +def fmtTimeSpan(time, pad=0, point=0, short=False, inTime=False, unit=99): print("fmtTimeSpan() has become col.backend.format_time_span()") return aqt.mw.col.backend.format_time_span(time) From 5ff1f3890f4866f11010158bd99f45bd32dd728d Mon Sep 17 00:00:00 2001 From: Arthur Milchior Date: Mon, 24 Feb 2020 01:46:36 -0800 Subject: [PATCH 19/30] Add a hook to change empty cards to delete --- qt/aqt/gui_hooks.py | 45 ++++++++++++++++++++++++++++++++++++++++ qt/aqt/main.py | 1 + qt/tools/genhooks_gui.py | 24 +++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index de0ec671d..5a001251e 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -862,6 +862,51 @@ class _EditorWillUseFontForFieldFilter: editor_will_use_font_for_field = _EditorWillUseFontForFieldFilter() +class _EmptyCardsWillBeDeletedFilter: + """Allow to change the list of cards to delete. + + For example, an add-on creating a method to delete only empty + new cards would be done as follow: +``` +from anki.consts import CARD_TYPE_NEW +from anki.utils import ids2str +from aqt import mw +from aqt import gui_hooks + +def filter(cids, col): + return col.db.list( + f"select id from cards where (type={CARD_TYPE_NEW} and (id in {ids2str(cids)))") + +def emptyNewCard(): + gui_hooks.append(filter) + mw.onEmptyCards() + gui_hooks.remove(filter) +```""" + + _hooks: List[Callable[[List[int]], List[int]]] = [] + + def append(self, cb: Callable[[List[int]], List[int]]) -> None: + """(cids: List[int])""" + self._hooks.append(cb) + + def remove(self, cb: Callable[[List[int]], List[int]]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, cids: List[int]) -> List[int]: + for filter in self._hooks: + try: + cids = filter(cids) + except: + # if the hook fails, remove it + self._hooks.remove(filter) + raise + return cids + + +empty_cards_will_be_deleted = _EmptyCardsWillBeDeletedFilter() + + class _MediaSyncDidProgressHook: _hooks: List[Callable[["aqt.mediasync.LogEntryWithTime"], None]] = [] diff --git a/qt/aqt/main.py b/qt/aqt/main.py index 9ca7e2acb..182c5de8a 100644 --- a/qt/aqt/main.py +++ b/qt/aqt/main.py @@ -1303,6 +1303,7 @@ will be lost. Continue?""" def onEmptyCards(self): self.progress.start(immediate=True) cids = self.col.emptyCids() + cids = gui_hooks.empty_cards_will_be_deleted(cids) if not cids: self.progress.finish() tooltip(_("No empty cards.")) diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index b92d0a969..54b131f51 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -346,6 +346,30 @@ hooks = [ name="media_sync_did_progress", args=["entry: aqt.mediasync.LogEntryWithTime"], ), Hook(name="media_sync_did_start_or_stop", args=["running: bool"]), + Hook( + name="empty_cards_will_be_deleted", + args=["cids: List[int]"], + return_type="List[int]", + doc="""Allow to change the list of cards to delete. + + For example, an add-on creating a method to delete only empty + new cards would be done as follow: +``` +from anki.consts import CARD_TYPE_NEW +from anki.utils import ids2str +from aqt import mw +from aqt import gui_hooks + +def filter(cids, col): + return col.db.list( + f"select id from cards where (type={CARD_TYPE_NEW} and (id in {ids2str(cids)))") + +def emptyNewCard(): + gui_hooks.append(filter) + mw.onEmptyCards() + gui_hooks.remove(filter) +```""", + ), # Adding cards ################### Hook( From 71c18a9372ba516ec348472147652728e4a2a582 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 08:46:09 +1000 Subject: [PATCH 20/30] separate out infallible and fallible commands in .proto --- proto/backend.proto | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index cd4c0affb..e1eb3b4f0 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -19,10 +19,9 @@ message I18nBackendInit { string locale_folder_path = 5; } -// 1-15 reserved for future use; 2047 for errors +// 1-15 reserved for future use message BackendInput { - reserved 2047; oneof value { TemplateRequirementsIn template_requirements = 16; SchedTimingTodayIn sched_timing_today = 17; @@ -47,25 +46,28 @@ message BackendInput { message BackendOutput { oneof value { - TemplateRequirementsOut template_requirements = 16; + // infallible commands SchedTimingTodayOut sched_timing_today = 17; - DeckTreeOut deck_tree = 18; - FindCardsOut find_cards = 19; - BrowserRowsOut browser_rows = 20; - RenderCardOut render_card = 21; sint32 local_minutes_west = 22; string strip_av_tags = 23; ExtractAVTagsOut extract_av_tags = 24; ExtractLatexOut extract_latex = 25; - string add_media_file = 26; - Empty sync_media = 27; - MediaCheckOut check_media = 28; - Empty trash_media_files = 29; string translate_string = 30; string format_time_span = 31; string studied_today = 32; string congrats_learn_msg = 33; + // fallible commands + TemplateRequirementsOut template_requirements = 16; + DeckTreeOut deck_tree = 18; + FindCardsOut find_cards = 19; + BrowserRowsOut browser_rows = 20; + RenderCardOut render_card = 21; + string add_media_file = 26; + Empty sync_media = 27; + MediaCheckOut check_media = 28; + Empty trash_media_files = 29; + BackendError error = 2047; } } From bec4699e27e10911d10eaabe92cc63f6cbc0c774 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 09:18:49 +1000 Subject: [PATCH 21/30] add comment to sync.ftl --- rslib/ftl/sync.ftl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rslib/ftl/sync.ftl b/rslib/ftl/sync.ftl index 39350a03b..9a8c2db71 100644 --- a/rslib/ftl/sync.ftl +++ b/rslib/ftl/sync.ftl @@ -11,6 +11,9 @@ sync-media-complete = Media sync complete. sync-media-failed = Media sync failed. sync-media-aborting = Media sync aborting... sync-media-aborted = Media sync aborted. + +# shown in the sync log to indicate media syncing will not be done because it +# was previously disabled by the user in the preferences screen sync-media-disabled = Media sync disabled. sync-abort-button = Abort From 644670d0d60fb899b63fa2c688bc5258c1f6c477 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 10:46:24 +1000 Subject: [PATCH 22/30] return a localized error for all error kinds some errors are not yet localized, but now the Python code doesn't need to think about which property to use --- proto/backend.proto | 21 +++++++++------------ pylib/anki/rsbackend.py | 40 +++++++++++++++------------------------- qt/aqt/mediasync.py | 10 +--------- rslib/src/backend.rs | 9 +++++---- rslib/src/err.rs | 17 ++++++++++------- 5 files changed, 40 insertions(+), 57 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index e1eb3b4f0..27c7d5b19 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -73,13 +73,16 @@ message BackendOutput { } message BackendError { + // localized error description suitable for displaying to the user + string localized = 1; + // error specifics oneof value { - StringError invalid_input = 1; - TemplateParseError template_parse = 2; - StringError io_error = 3; - StringError db_error = 4; - NetworkError network_error = 5; - SyncError sync_error = 6; + StringError invalid_input = 2; + StringError template_parse = 3; + StringError io_error = 4; + StringError db_error = 5; + NetworkError network_error = 6; + SyncError sync_error = 7; // user interrupted operation Empty interrupted = 8; } @@ -96,10 +99,6 @@ message StringError { string info = 1; } -message TemplateParseError { - string info = 1; -} - message NetworkError { string info = 1; enum NetworkErrorKind { @@ -109,7 +108,6 @@ message NetworkError { PROXY_AUTH = 3; } NetworkErrorKind kind = 2; - string localized = 3; } message SyncError { @@ -125,7 +123,6 @@ message SyncError { RESYNC_REQUIRED = 7; } SyncErrorKind kind = 2; - string localized = 3; } message MediaSyncProgress { diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index 6395a184f..eccfabfab 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -32,14 +32,17 @@ class StringError(Exception): NetworkErrorKind = pb.NetworkError.NetworkErrorKind +SyncErrorKind = pb.SyncError.SyncErrorKind class NetworkError(StringError): def kind(self) -> NetworkErrorKind: return self.args[1] - def localized(self) -> str: - return self.args[2] + +class SyncError(StringError): + def kind(self) -> SyncErrorKind: + return self.args[1] class IOError(StringError): @@ -54,35 +57,22 @@ class TemplateError(StringError): pass -SyncErrorKind = pb.SyncError.SyncErrorKind - - -class SyncError(StringError): - def kind(self) -> SyncErrorKind: - return self.args[1] - - def localized(self) -> str: - return self.args[2] - - def proto_exception_to_native(err: pb.BackendError) -> Exception: val = err.WhichOneof("value") if val == "interrupted": return Interrupted() elif val == "network_error": - e = err.network_error - return NetworkError(e.info, e.kind, e.localized) - elif val == "io_error": - return IOError(err.io_error.info) - elif val == "db_error": - return DBError(err.db_error.info) - elif val == "template_parse": - return TemplateError(err.template_parse.info) - elif val == "invalid_input": - return StringError(err.invalid_input.info) + return NetworkError(err.localized, err.network_error.kind) elif val == "sync_error": - e2 = err.sync_error - return SyncError(e2.info, e2.kind, e2.localized) + return SyncError(err.localized, err.sync_error.kind) + elif val == "io_error": + return IOError(err.localized) + elif val == "db_error": + return DBError(err.localized) + elif val == "template_parse": + return TemplateError(err.localized) + elif val == "invalid_input": + return StringError(err.localized) else: assert_impossible_literal(val) diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index f0ec19bdd..6cd5a063e 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -14,10 +14,8 @@ from anki.rsbackend import ( FString, Interrupted, MediaSyncProgress, - NetworkError, Progress, ProgressKind, - SyncError, ) from anki.types import assert_impossible from anki.utils import intTime @@ -109,13 +107,7 @@ class MediaSyncer: return self._log_and_notify(tr(FString.SYNC_MEDIA_FAILED)) - if isinstance(exc, SyncError): - showWarning(exc.localized()) - elif isinstance(exc, NetworkError): - msg = exc.localized() - msg += "\n\n" + tr(FString.NETWORK_DETAILS, details=str(exc)) - else: - raise exc + showWarning(str(exc)) def entries(self) -> List[LogEntryWithTime]: return self._log diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index 6a7f8d033..7d0f10444 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -45,23 +45,24 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { let localized = err.localized_description(i18n); let value = match err { AnkiError::InvalidInput { info } => V::InvalidInput(pb::StringError { info }), - AnkiError::TemplateError { info } => V::TemplateParse(pb::TemplateParseError { info }), + AnkiError::TemplateError { info } => V::TemplateParse(pb::StringError { info }), AnkiError::IOError { info } => V::IoError(pb::StringError { info }), AnkiError::DBError { info } => V::DbError(pb::StringError { info }), AnkiError::NetworkError { info, kind } => V::NetworkError(pb::NetworkError { info, kind: kind.into(), - localized, }), AnkiError::SyncError { info, kind } => V::SyncError(pb::SyncError { info, kind: kind.into(), - localized, }), AnkiError::Interrupted => V::Interrupted(Empty {}), }; - pb::BackendError { value: Some(value) } + pb::BackendError { + value: Some(value), + localized, + } } // Convert an Anki error to a protobuf output. diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 9a39babf0..9714f5925 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -1,7 +1,7 @@ // Copyright: Ankitects Pty Ltd and contributors // License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html -use crate::i18n::{FString, I18n}; +use crate::i18n::{tr_strs, FString, I18n}; pub use failure::{Error, Fail}; use reqwest::StatusCode; use std::io; @@ -67,13 +67,16 @@ impl AnkiError { SyncErrorKind::ResyncRequired => i18n.tr(FString::SyncResyncRequired), } .into(), - AnkiError::NetworkError { kind, .. } => match kind { - NetworkErrorKind::Offline => i18n.tr(FString::NetworkOffline), - NetworkErrorKind::Timeout => i18n.tr(FString::NetworkTimeout), - NetworkErrorKind::ProxyAuth => i18n.tr(FString::NetworkProxyAuth), - NetworkErrorKind::Other => i18n.tr(FString::NetworkOther), + AnkiError::NetworkError { kind, info } => { + let summary = match kind { + NetworkErrorKind::Offline => i18n.tr(FString::NetworkOffline), + NetworkErrorKind::Timeout => i18n.tr(FString::NetworkTimeout), + NetworkErrorKind::ProxyAuth => i18n.tr(FString::NetworkProxyAuth), + NetworkErrorKind::Other => i18n.tr(FString::NetworkOther), + }; + let details = i18n.trn(FString::NetworkDetails, tr_strs!["details"=>info]); + format!("{}\n{}", summary, details) } - .into(), _ => "".into(), } } From b610ff781f0a9a52ac2e207b8e0949a36a1ca017 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 10:53:49 +1000 Subject: [PATCH 23/30] info is superfluous --- proto/backend.proto | 18 ++++++------------ rslib/src/backend.rs | 20 ++++++++------------ 2 files changed, 14 insertions(+), 24 deletions(-) diff --git a/proto/backend.proto b/proto/backend.proto index 27c7d5b19..c9435c7d4 100644 --- a/proto/backend.proto +++ b/proto/backend.proto @@ -77,10 +77,10 @@ message BackendError { string localized = 1; // error specifics oneof value { - StringError invalid_input = 2; - StringError template_parse = 3; - StringError io_error = 4; - StringError db_error = 5; + Empty invalid_input = 2; + Empty template_parse = 3; + Empty io_error = 4; + Empty db_error = 5; NetworkError network_error = 6; SyncError sync_error = 7; // user interrupted operation @@ -95,23 +95,17 @@ message Progress { } } -message StringError { - string info = 1; -} - message NetworkError { - string info = 1; enum NetworkErrorKind { OTHER = 0; OFFLINE = 1; TIMEOUT = 2; PROXY_AUTH = 3; } - NetworkErrorKind kind = 2; + NetworkErrorKind kind = 1; } message SyncError { - string info = 1; enum SyncErrorKind { OTHER = 0; CONFLICT = 1; @@ -122,7 +116,7 @@ message SyncError { MEDIA_CHECK_REQUIRED = 6; RESYNC_REQUIRED = 7; } - SyncErrorKind kind = 2; + SyncErrorKind kind = 1; } message MediaSyncProgress { diff --git a/rslib/src/backend.rs b/rslib/src/backend.rs index 7d0f10444..0f76a049f 100644 --- a/rslib/src/backend.rs +++ b/rslib/src/backend.rs @@ -44,18 +44,14 @@ fn anki_error_to_proto_error(err: AnkiError, i18n: &I18n) -> pb::BackendError { use pb::backend_error::Value as V; let localized = err.localized_description(i18n); let value = match err { - AnkiError::InvalidInput { info } => V::InvalidInput(pb::StringError { info }), - AnkiError::TemplateError { info } => V::TemplateParse(pb::StringError { info }), - AnkiError::IOError { info } => V::IoError(pb::StringError { info }), - AnkiError::DBError { info } => V::DbError(pb::StringError { info }), - AnkiError::NetworkError { info, kind } => V::NetworkError(pb::NetworkError { - info, - kind: kind.into(), - }), - AnkiError::SyncError { info, kind } => V::SyncError(pb::SyncError { - info, - kind: kind.into(), - }), + AnkiError::InvalidInput { .. } => V::InvalidInput(pb::Empty {}), + AnkiError::TemplateError { .. } => V::TemplateParse(pb::Empty {}), + AnkiError::IOError { .. } => V::IoError(pb::Empty {}), + AnkiError::DBError { .. } => V::DbError(pb::Empty {}), + AnkiError::NetworkError { kind, .. } => { + V::NetworkError(pb::NetworkError { kind: kind.into() }) + } + AnkiError::SyncError { kind, .. } => V::SyncError(pb::SyncError { kind: kind.into() }), AnkiError::Interrupted => V::Interrupted(Empty {}), }; From 67942b54f5610cdc4cf973bb6082dba57f8e7353 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 11:07:18 +1000 Subject: [PATCH 24/30] use debug description for untranslated errors; fix template error --- rslib/src/err.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 9714f5925..59112aef7 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -77,7 +77,11 @@ impl AnkiError { let details = i18n.trn(FString::NetworkDetails, tr_strs!["details"=>info]); format!("{}\n{}", summary, details) } - _ => "".into(), + AnkiError::TemplateError { info } => { + // already localized + info.into() + } + _ => format!("{:?}", self), } } } From b4c3bf99ab9874c4c50e8477021b1e625939494b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 11:22:48 +1000 Subject: [PATCH 25/30] extra newline in network details --- rslib/src/err.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib/src/err.rs b/rslib/src/err.rs index 59112aef7..1d2bd2faf 100644 --- a/rslib/src/err.rs +++ b/rslib/src/err.rs @@ -75,7 +75,7 @@ impl AnkiError { NetworkErrorKind::Other => i18n.tr(FString::NetworkOther), }; let details = i18n.trn(FString::NetworkDetails, tr_strs!["details"=>info]); - format!("{}\n{}", summary, details) + format!("{}\n\n{}", summary, details) } AnkiError::TemplateError { info } => { // already localized From e439e8cdec534a59e19d198956015090bdc032b4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 12:22:24 +1000 Subject: [PATCH 26/30] add string for media log title, and add button in prefs to access it --- qt/aqt/mediasync.py | 2 ++ qt/aqt/preferences.py | 7 ++++++- qt/designer/preferences.ui | 30 +++++++++++++++++++++++++++--- qt/designer/synclog.ui | 2 +- rslib/ftl/sync.ftl | 8 ++++++-- 5 files changed, 42 insertions(+), 7 deletions(-) diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index 6cd5a063e..889eec80a 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -158,10 +158,12 @@ class MediaSyncDialog(QDialog): self._close_when_done = close_when_done self.form = aqt.forms.synclog.Ui_Dialog() self.form.setupUi(self) + self.setWindowTitle(tr(FString.SYNC_MEDIA_LOG_TITLE)) self.abort_button = QPushButton(tr(FString.SYNC_ABORT_BUTTON)) self.abort_button.clicked.connect(self._on_abort) # type: ignore self.abort_button.setAutoDefault(False) self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole) + self.abort_button.setHidden(not self._syncer.is_syncing()) gui_hooks.media_sync_did_progress.append(self._on_log_entry) gui_hooks.media_sync_did_start_or_stop.append(self._on_start_stop) diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 7c5b924e6..81170f681 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -10,7 +10,7 @@ import aqt from anki.lang import _ from aqt import AnkiQt from aqt.qt import * -from aqt.utils import askUser, openHelp, showInfo, showWarning +from aqt.utils import askUser, openHelp, showInfo, showWarning, tr, FString class Preferences(QDialog): @@ -177,6 +177,8 @@ class Preferences(QDialog): ###################################################################### def setupNetwork(self): + self.form.media_log.setText(tr(FString.SYNC_MEDIA_LOG_BUTTON)) + self.form.media_log.clicked.connect(self.on_media_log) self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"]) self.form.syncMedia.setChecked(self.prof["syncMedia"]) if not self.prof["syncKey"]: @@ -185,6 +187,9 @@ class Preferences(QDialog): self.form.syncUser.setText(self.prof.get("syncUser", "")) self.form.syncDeauth.clicked.connect(self.onSyncDeauth) + def on_media_log(self): + self.mw.media_syncer.show_sync_log() + def _hideAuth(self): self.form.syncDeauth.setVisible(False) self.form.syncUser.setText("") diff --git a/qt/designer/preferences.ui b/qt/designer/preferences.ui index 2e87ce297..03ec7d8eb 100644 --- a/qt/designer/preferences.ui +++ b/qt/designer/preferences.ui @@ -371,6 +371,30 @@ + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + @@ -542,10 +566,10 @@ lrnCutoff timeLimit numBackups - syncOnProgramOpen - tabWidget - fullSync syncMedia + tabWidget + syncOnProgramOpen + fullSync syncDeauth diff --git a/qt/designer/synclog.ui b/qt/designer/synclog.ui index adb8120a9..e0d93b35a 100644 --- a/qt/designer/synclog.ui +++ b/qt/designer/synclog.ui @@ -11,7 +11,7 @@ - Sync + diff --git a/rslib/ftl/sync.ftl b/rslib/ftl/sync.ftl index 9a8c2db71..97dec7e7e 100644 --- a/rslib/ftl/sync.ftl +++ b/rslib/ftl/sync.ftl @@ -12,11 +12,15 @@ sync-media-failed = Media sync failed. sync-media-aborting = Media sync aborting... sync-media-aborted = Media sync aborted. -# shown in the sync log to indicate media syncing will not be done because it -# was previously disabled by the user in the preferences screen +# Shown in the sync log to indicate media syncing will not be done, because it +# was previously disabled by the user in the preferences screen. sync-media-disabled = Media sync disabled. sync-abort-button = Abort +sync-media-log-button = Media Log + +# Title of the screen that shows syncing progress history +sync-media-log-title = Media Sync Log ## Error messages From 0c494317196b88fcc433903f485a35172da82596 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 12:25:19 +1000 Subject: [PATCH 27/30] FString -> TR --- pylib/anki/rsbackend.py | 8 ++++---- pylib/anki/stats.py | 8 ++++---- pylib/tests/test_collection.py | 8 ++++---- qt/aqt/browser.py | 8 ++++---- qt/aqt/deckbrowser.py | 4 ++-- qt/aqt/errors.py | 8 ++++---- qt/aqt/mediacheck.py | 22 ++++++++-------------- qt/aqt/mediasync.py | 24 +++++++++--------------- qt/aqt/preferences.py | 4 ++-- qt/aqt/utils.py | 4 ++-- qt/tests/test_i18n.py | 6 +++--- 11 files changed, 46 insertions(+), 58 deletions(-) diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index eccfabfab..cdd9d9603 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -12,7 +12,7 @@ import ankirspy # pytype: disable=import-error import anki.backend_pb2 as pb import anki.buildinfo from anki import hooks -from anki.fluent_pb2 import FluentString as FString +from anki.fluent_pb2 import FluentString as TR from anki.models import AllTemplateReqs from anki.sound import AVTag, SoundOrVideoTag, TTSTag from anki.types import assert_impossible_literal @@ -324,7 +324,7 @@ class RustBackend: pb.BackendInput(trash_media_files=pb.TrashMediaFilesIn(fnames=fnames)) ) - def translate(self, key: FString, **kwargs: Union[str, int, float]): + def translate(self, key: TR, **kwargs: Union[str, int, float]): return self._run_command( pb.BackendInput(translate_string=translate_string_in(key, **kwargs)) ).translate_string @@ -358,7 +358,7 @@ class RustBackend: def translate_string_in( - key: FString, **kwargs: Union[str, int, float] + key: TR, **kwargs: Union[str, int, float] ) -> pb.TranslateStringIn: args = {} for (k, v) in kwargs.items(): @@ -376,7 +376,7 @@ class I18nBackend: ) self._backend = ankirspy.open_i18n(init_msg.SerializeToString()) - def translate(self, key: FString, **kwargs: Union[str, int, float]): + def translate(self, key: TR, **kwargs: Union[str, int, float]): return self._backend.translate( translate_string_in(key, **kwargs).SerializeToString() ) diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index 5be72f04a..d336b6b61 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -11,7 +11,7 @@ from typing import Any, Dict, List, Optional, Tuple import anki from anki.consts import * from anki.lang import _, ngettext -from anki.rsbackend import FormatTimeSpanContext, FString +from anki.rsbackend import TR, FormatTimeSpanContext from anki.utils import ids2str # Card stats @@ -48,7 +48,7 @@ class CardStats: next = self.date(next) if next: self.addLine( - self.col.backend.translate(FString.STATISTICS_DUE_DATE), next, + self.col.backend.translate(TR.STATISTICS_DUE_DATE), next, ) if c.queue == QUEUE_TYPE_REV: self.addLine( @@ -280,7 +280,7 @@ from revlog where id > ? """ self._line( i, _("Total"), - self.col.backend.translate(FString.STATISTICS_REVIEWS, reviews=tot), + self.col.backend.translate(TR.STATISTICS_REVIEWS, reviews=tot), ) self._line(i, _("Average"), self._avgDay(tot, num, _("reviews"))) tomorrow = self.col.db.scalar( @@ -458,7 +458,7 @@ group by day order by day""" i, _("Average answer time"), self.col.backend.translate( - FString.STATISTICS_AVERAGE_ANSWER_TIME, + TR.STATISTICS_AVERAGE_ANSWER_TIME, **{"cards-per-minute": perMin, "average-seconds": average_secs}, ), ) diff --git a/pylib/tests/test_collection.py b/pylib/tests/test_collection.py index acba9f139..734ca5fa9 100644 --- a/pylib/tests/test_collection.py +++ b/pylib/tests/test_collection.py @@ -5,7 +5,7 @@ import tempfile from anki import Collection as aopen from anki.lang import without_unicode_isolation -from anki.rsbackend import FString +from anki.rsbackend import TR from anki.stdmodels import addBasicModel, models from anki.utils import isWin from tests.shared import assertException, getEmptyCol @@ -157,8 +157,8 @@ def test_translate(): no_uni = without_unicode_isolation assert ( - tr(FString.CARD_TEMPLATE_RENDERING_FRONT_SIDE_PROBLEM) + tr(TR.CARD_TEMPLATE_RENDERING_FRONT_SIDE_PROBLEM) == "Front template has a problem:" ) - assert no_uni(tr(FString.STATISTICS_REVIEWS, reviews=1)) == "1 review" - assert no_uni(tr(FString.STATISTICS_REVIEWS, reviews=2)) == "2 reviews" + assert no_uni(tr(TR.STATISTICS_REVIEWS, reviews=1)) == "1 review" + assert no_uni(tr(TR.STATISTICS_REVIEWS, reviews=2)) == "2 reviews" diff --git a/qt/aqt/browser.py b/qt/aqt/browser.py index ea2b7657d..4a1f7b317 100644 --- a/qt/aqt/browser.py +++ b/qt/aqt/browser.py @@ -24,7 +24,7 @@ from anki.consts import * from anki.lang import _, ngettext from anki.models import NoteType from anki.notes import Note -from anki.rsbackend import FString +from anki.rsbackend import TR from anki.utils import htmlToTextLine, ids2str, intTime, isMac, isWin from aqt import AnkiQt, gui_hooks from aqt.editor import Editor @@ -356,7 +356,7 @@ class DataModel(QAbstractTableModel): elif c.queue == QUEUE_TYPE_LRN: date = c.due elif c.queue == QUEUE_TYPE_NEW or c.type == CARD_TYPE_NEW: - return tr(FString.STATISTICS_DUE_FOR_NEW_CARD, number=c.due) + return tr(TR.STATISTICS_DUE_FOR_NEW_CARD, number=c.due) elif c.queue in (QUEUE_TYPE_REV, QUEUE_TYPE_DAY_LEARN_RELEARN) or ( c.type == CARD_TYPE_REV and c.queue < 0 ): @@ -730,7 +730,7 @@ class Browser(QMainWindow): ("noteCrt", _("Created")), ("noteMod", _("Edited")), ("cardMod", _("Changed")), - ("cardDue", tr(FString.STATISTICS_DUE_DATE)), + ("cardDue", tr(TR.STATISTICS_DUE_DATE)), ("cardIvl", _("Interval")), ("cardEase", _("Ease")), ("cardReps", _("Reviews")), @@ -1281,7 +1281,7 @@ by clicking on one on the left.""" (_("New"), "is:new"), (_("Learning"), "is:learn"), (_("Review"), "is:review"), - (tr(FString.FILTERING_IS_DUE), "is:due"), + (tr(TR.FILTERING_IS_DUE), "is:due"), None, (_("Suspended"), "is:suspended"), (_("Buried"), "is:buried"), diff --git a/qt/aqt/deckbrowser.py b/qt/aqt/deckbrowser.py index 184ccd59e..e1b3cd748 100644 --- a/qt/aqt/deckbrowser.py +++ b/qt/aqt/deckbrowser.py @@ -11,7 +11,7 @@ from typing import Any import aqt from anki.errors import DeckRenameError from anki.lang import _, ngettext -from anki.rsbackend import FString +from anki.rsbackend import TR from anki.utils import ids2str from aqt import AnkiQt, gui_hooks from aqt.qt import * @@ -185,7 +185,7 @@ where id > ?""", %s%s %s""" % ( _("Deck"), - tr(FString.STATISTICS_DUE_COUNT), + tr(TR.STATISTICS_DUE_COUNT), _("New"), ) buf += self._topLevelDragRow() diff --git a/qt/aqt/errors.py b/qt/aqt/errors.py index b40dd18ff..1bbc3b1da 100644 --- a/qt/aqt/errors.py +++ b/qt/aqt/errors.py @@ -11,7 +11,7 @@ from markdown import markdown from anki.lang import _ from aqt import mw from aqt.qt import * -from aqt.utils import FString, showText, showWarning, supportText, tr +from aqt.utils import TR, showText, showWarning, supportText, tr if not os.environ.get("DEBUG"): @@ -106,14 +106,14 @@ your system's temporary folder may be incorrect.""" ) ) if "disk I/O error" in error: - showWarning(markdown(tr(FString.ERRORS_ACCESSING_DB))) + showWarning(markdown(tr(TR.ERRORS_ACCESSING_DB))) return if self.mw.addonManager.dirty: - txt = markdown(tr(FString.ERRORS_ADDONS_ACTIVE_POPUP)) + txt = markdown(tr(TR.ERRORS_ADDONS_ACTIVE_POPUP)) error = supportText() + self._addonText(error) + "\n" + error else: - txt = markdown(tr(FString.ERRORS_STANDARD_POPUP)) + txt = markdown(tr(TR.ERRORS_STANDARD_POPUP)) error = supportText() + "\n" + error # show dialog diff --git a/qt/aqt/mediacheck.py b/qt/aqt/mediacheck.py index 20e36a178..7500ceafd 100644 --- a/qt/aqt/mediacheck.py +++ b/qt/aqt/mediacheck.py @@ -10,13 +10,7 @@ from typing import Iterable, List, Optional, TypeVar import aqt from anki import hooks -from anki.rsbackend import ( - FString, - Interrupted, - MediaCheckOutput, - Progress, - ProgressKind, -) +from anki.rsbackend import TR, Interrupted, MediaCheckOutput, Progress, ProgressKind from aqt.qt import * from aqt.utils import askUser, restoreGeom, saveGeom, showText, tooltip, tr @@ -89,14 +83,14 @@ class MediaChecker: layout.addWidget(box) if output.unused: - b = QPushButton(tr(FString.MEDIA_CHECK_DELETE_UNUSED)) + b = QPushButton(tr(TR.MEDIA_CHECK_DELETE_UNUSED)) b.setAutoDefault(False) box.addButton(b, QDialogButtonBox.RejectRole) b.clicked.connect(lambda c: self._on_trash_files(output.unused)) # type: ignore if output.missing: if any(map(lambda x: x.startswith("latex-"), output.missing)): - b = QPushButton(tr(FString.MEDIA_CHECK_RENDER_LATEX)) + b = QPushButton(tr(TR.MEDIA_CHECK_RENDER_LATEX)) b.setAutoDefault(False) box.addButton(b, QDialogButtonBox.RejectRole) b.clicked.connect(self._on_render_latex) # type: ignore @@ -125,17 +119,17 @@ class MediaChecker: browser.onSearchActivated() showText(err, type="html") else: - tooltip(tr(FString.MEDIA_CHECK_ALL_LATEX_RENDERED)) + tooltip(tr(TR.MEDIA_CHECK_ALL_LATEX_RENDERED)) def _on_render_latex_progress(self, count: int) -> bool: if self.progress_dialog.wantCancel: return False - self.mw.progress.update(tr(FString.MEDIA_CHECK_CHECKED, count=count)) + self.mw.progress.update(tr(TR.MEDIA_CHECK_CHECKED, count=count)) return True def _on_trash_files(self, fnames: List[str]): - if not askUser(tr(FString.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)): + if not askUser(tr(TR.MEDIA_CHECK_DELETE_UNUSED_CONFIRM)): return self.progress_dialog = self.mw.progress.start() @@ -149,10 +143,10 @@ class MediaChecker: remaining -= len(chunk) if time.time() - last_progress >= 0.3: self.mw.progress.update( - tr(FString.MEDIA_CHECK_FILES_REMAINING, count=remaining) + tr(TR.MEDIA_CHECK_FILES_REMAINING, count=remaining) ) finally: self.mw.progress.finish() self.progress_dialog = None - tooltip(tr(FString.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total)) + tooltip(tr(TR.MEDIA_CHECK_DELETE_UNUSED_COMPLETE, count=total)) diff --git a/qt/aqt/mediasync.py b/qt/aqt/mediasync.py index 889eec80a..f0b46647a 100644 --- a/qt/aqt/mediasync.py +++ b/qt/aqt/mediasync.py @@ -10,13 +10,7 @@ from typing import List, Union import aqt from anki import hooks -from anki.rsbackend import ( - FString, - Interrupted, - MediaSyncProgress, - Progress, - ProgressKind, -) +from anki.rsbackend import TR, Interrupted, MediaSyncProgress, Progress, ProgressKind from anki.types import assert_impossible from anki.utils import intTime from aqt import gui_hooks @@ -63,10 +57,10 @@ class MediaSyncer: return if not self.mw.pm.media_syncing_enabled(): - self._log_and_notify(tr(FString.SYNC_MEDIA_DISABLED)) + self._log_and_notify(tr(TR.SYNC_MEDIA_DISABLED)) return - self._log_and_notify(tr(FString.SYNC_MEDIA_STARTING)) + self._log_and_notify(tr(TR.SYNC_MEDIA_STARTING)) self._syncing = True self._want_stop = False gui_hooks.media_sync_did_start_or_stop(True) @@ -99,14 +93,14 @@ class MediaSyncer: if exc is not None: self._handle_sync_error(exc) else: - self._log_and_notify(tr(FString.SYNC_MEDIA_COMPLETE)) + self._log_and_notify(tr(TR.SYNC_MEDIA_COMPLETE)) def _handle_sync_error(self, exc: BaseException): if isinstance(exc, Interrupted): - self._log_and_notify(tr(FString.SYNC_MEDIA_ABORTED)) + self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTED)) return - self._log_and_notify(tr(FString.SYNC_MEDIA_FAILED)) + self._log_and_notify(tr(TR.SYNC_MEDIA_FAILED)) showWarning(str(exc)) def entries(self) -> List[LogEntryWithTime]: @@ -115,7 +109,7 @@ class MediaSyncer: def abort(self) -> None: if not self.is_syncing(): return - self._log_and_notify(tr(FString.SYNC_MEDIA_ABORTING)) + self._log_and_notify(tr(TR.SYNC_MEDIA_ABORTING)) self._want_stop = True def is_syncing(self) -> bool: @@ -158,8 +152,8 @@ class MediaSyncDialog(QDialog): self._close_when_done = close_when_done self.form = aqt.forms.synclog.Ui_Dialog() self.form.setupUi(self) - self.setWindowTitle(tr(FString.SYNC_MEDIA_LOG_TITLE)) - self.abort_button = QPushButton(tr(FString.SYNC_ABORT_BUTTON)) + self.setWindowTitle(tr(TR.SYNC_MEDIA_LOG_TITLE)) + self.abort_button = QPushButton(tr(TR.SYNC_ABORT_BUTTON)) self.abort_button.clicked.connect(self._on_abort) # type: ignore self.abort_button.setAutoDefault(False) self.form.buttonBox.addButton(self.abort_button, QDialogButtonBox.ActionRole) diff --git a/qt/aqt/preferences.py b/qt/aqt/preferences.py index 81170f681..5b997724f 100644 --- a/qt/aqt/preferences.py +++ b/qt/aqt/preferences.py @@ -10,7 +10,7 @@ import aqt from anki.lang import _ from aqt import AnkiQt from aqt.qt import * -from aqt.utils import askUser, openHelp, showInfo, showWarning, tr, FString +from aqt.utils import TR, askUser, openHelp, showInfo, showWarning, tr class Preferences(QDialog): @@ -177,7 +177,7 @@ class Preferences(QDialog): ###################################################################### def setupNetwork(self): - self.form.media_log.setText(tr(FString.SYNC_MEDIA_LOG_BUTTON)) + self.form.media_log.setText(tr(TR.SYNC_MEDIA_LOG_BUTTON)) self.form.media_log.clicked.connect(self.on_media_log) self.form.syncOnProgramOpen.setChecked(self.prof["autoSync"]) self.form.syncMedia.setChecked(self.prof["syncMedia"]) diff --git a/qt/aqt/utils.py b/qt/aqt/utils.py index 26cc4fa5e..a0e123622 100644 --- a/qt/aqt/utils.py +++ b/qt/aqt/utils.py @@ -13,7 +13,7 @@ from typing import Any, Optional, Union import anki import aqt from anki.lang import _ -from anki.rsbackend import FString +from anki.rsbackend import TR from anki.utils import invalidFilename, isMac, isWin, noBundledLibs, versionWithBuild from aqt.qt import * from aqt.theme import theme_manager @@ -32,7 +32,7 @@ def locale_dir() -> str: return os.path.join(aqt_data_folder(), "locale") -def tr(key: FString, **kwargs: Union[str, int, float]) -> str: +def tr(key: TR, **kwargs: Union[str, int, float]) -> str: "Shortcut to access Fluent translations." return anki.lang.current_i18n.translate(key, **kwargs) diff --git a/qt/tests/test_i18n.py b/qt/tests/test_i18n.py index edff4dc87..c11180032 100644 --- a/qt/tests/test_i18n.py +++ b/qt/tests/test_i18n.py @@ -1,13 +1,13 @@ import anki.lang -from anki.rsbackend import FString +from anki.rsbackend import TR def test_no_collection_i18n(): anki.lang.set_lang("zz", "") tr2 = anki.lang.current_i18n.translate no_uni = anki.lang.without_unicode_isolation - assert no_uni(tr2(FString.STATISTICS_REVIEWS, reviews=2)) == "2 reviews" + assert no_uni(tr2(TR.STATISTICS_REVIEWS, reviews=2)) == "2 reviews" anki.lang.set_lang("ja", "") tr2 = anki.lang.current_i18n.translate - assert no_uni(tr2(FString.STATISTICS_REVIEWS, reviews=2)) == "2 枚の復習カード" + assert no_uni(tr2(TR.STATISTICS_REVIEWS, reviews=2)) == "2 枚の復習カード" From 4d917cc65be9ff8d708d9ff96aed71b204d62979 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 12:32:37 +1000 Subject: [PATCH 28/30] add self.col.tr() shortcut --- pylib/anki/collection.py | 2 +- pylib/anki/rsbackend.py | 2 +- pylib/anki/stats.py | 10 +++------- pylib/tests/test_collection.py | 7 +++---- 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/pylib/anki/collection.py b/pylib/anki/collection.py index 2a865f93e..cfa2c3cdb 100644 --- a/pylib/anki/collection.py +++ b/pylib/anki/collection.py @@ -73,7 +73,6 @@ class _Collection: ls: int conf: Dict[str, Any] _undo: List[Any] - backend: RustBackend def __init__( self, @@ -83,6 +82,7 @@ class _Collection: log: bool = False, ) -> None: self.backend = backend + self.tr = backend.translate self._debugLog = log self.db = db self.path = db._path diff --git a/pylib/anki/rsbackend.py b/pylib/anki/rsbackend.py index cdd9d9603..def276f30 100644 --- a/pylib/anki/rsbackend.py +++ b/pylib/anki/rsbackend.py @@ -324,7 +324,7 @@ class RustBackend: pb.BackendInput(trash_media_files=pb.TrashMediaFilesIn(fnames=fnames)) ) - def translate(self, key: TR, **kwargs: Union[str, int, float]): + def translate(self, key: TR, **kwargs: Union[str, int, float]) -> str: return self._run_command( pb.BackendInput(translate_string=translate_string_in(key, **kwargs)) ).translate_string diff --git a/pylib/anki/stats.py b/pylib/anki/stats.py index d336b6b61..2f24babe6 100644 --- a/pylib/anki/stats.py +++ b/pylib/anki/stats.py @@ -47,9 +47,7 @@ class CardStats: next = c.due next = self.date(next) if next: - self.addLine( - self.col.backend.translate(TR.STATISTICS_DUE_DATE), next, - ) + self.addLine(self.col.tr(TR.STATISTICS_DUE_DATE), next) if c.queue == QUEUE_TYPE_REV: self.addLine( _("Interval"), self.col.backend.format_time_span(c.ivl * 86400) @@ -278,9 +276,7 @@ from revlog where id > ? """ def _dueInfo(self, tot, num) -> str: i: List[str] = [] self._line( - i, - _("Total"), - self.col.backend.translate(TR.STATISTICS_REVIEWS, reviews=tot), + i, _("Total"), self.col.tr(TR.STATISTICS_REVIEWS, reviews=tot), ) self._line(i, _("Average"), self._avgDay(tot, num, _("reviews"))) tomorrow = self.col.db.scalar( @@ -457,7 +453,7 @@ group by day order by day""" self._line( i, _("Average answer time"), - self.col.backend.translate( + self.col.tr( TR.STATISTICS_AVERAGE_ANSWER_TIME, **{"cards-per-minute": perMin, "average-seconds": average_secs}, ), diff --git a/pylib/tests/test_collection.py b/pylib/tests/test_collection.py index 734ca5fa9..8c0985426 100644 --- a/pylib/tests/test_collection.py +++ b/pylib/tests/test_collection.py @@ -153,12 +153,11 @@ def test_furigana(): def test_translate(): d = getEmptyCol() - tr = d.backend.translate no_uni = without_unicode_isolation assert ( - tr(TR.CARD_TEMPLATE_RENDERING_FRONT_SIDE_PROBLEM) + d.tr(TR.CARD_TEMPLATE_RENDERING_FRONT_SIDE_PROBLEM) == "Front template has a problem:" ) - assert no_uni(tr(TR.STATISTICS_REVIEWS, reviews=1)) == "1 review" - assert no_uni(tr(TR.STATISTICS_REVIEWS, reviews=2)) == "2 reviews" + assert no_uni(d.tr(TR.STATISTICS_REVIEWS, reviews=1)) == "1 review" + assert no_uni(d.tr(TR.STATISTICS_REVIEWS, reviews=2)) == "2 reviews" From 34fcbf6af8cf5c9629cfa338149ce14402e489cb Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 12:49:52 +1000 Subject: [PATCH 29/30] pipefail requires removing -q https://github.com/ankitects/anki/pull/464#discussion_r384485166 --- .github/scripts/contrib.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/contrib.sh b/.github/scripts/contrib.sh index 055e917ae..b4cbf65c8 100755 --- a/.github/scripts/contrib.sh +++ b/.github/scripts/contrib.sh @@ -9,7 +9,7 @@ git log --pretty=format:' - %ae' CONTRIBUTORS |sort |uniq |sort -f | sed "s/@/$a headAuthor=$(git log -1 --pretty=format:'%ae') authorAt=$(echo "$headAuthor" | sed "s/@/$antispam/") -if git log --pretty=format:'%ae' CONTRIBUTORS | grep -qi "$headAuthor"; then +if git log --pretty=format:'%ae' CONTRIBUTORS | grep -i "$headAuthor"; then echo "Author $authorAt found in CONTRIBUTORS" else echo "Author $authorAt NOT found in list" From 5ea8a591390cae60b57f68a895d49ed4155a2003 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 27 Feb 2020 12:52:00 +1000 Subject: [PATCH 30/30] match Alan's patch --- .github/scripts/contrib.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/scripts/contrib.sh b/.github/scripts/contrib.sh index b4cbf65c8..0bcf6153e 100755 --- a/.github/scripts/contrib.sh +++ b/.github/scripts/contrib.sh @@ -9,7 +9,7 @@ git log --pretty=format:' - %ae' CONTRIBUTORS |sort |uniq |sort -f | sed "s/@/$a headAuthor=$(git log -1 --pretty=format:'%ae') authorAt=$(echo "$headAuthor" | sed "s/@/$antispam/") -if git log --pretty=format:'%ae' CONTRIBUTORS | grep -i "$headAuthor"; then +if git log --pretty=format:'%ae' CONTRIBUTORS | grep -i "$headAuthor" > /dev/null; then echo "Author $authorAt found in CONTRIBUTORS" else echo "Author $authorAt NOT found in list"