From 335047187a7a787dc7816c51dfb0077681abebce Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Mon, 24 Feb 2020 13:42:30 +0100 Subject: [PATCH 01/12] Add hooks for extending the deck options dialog Introduces three new hooks: * deck_conf_will_show: Allows adding or modifying widgets * deck_conf_did_load_config: Allows add-on widgets to read from config * deck_conf_will_save_config: Allows add-on widgets to write to config --- qt/aqt/deckconf.py | 4 ++ qt/aqt/gui_hooks.py | 80 ++++++++++++++++++++++++++++++++++++++++ qt/tools/genhooks_gui.py | 17 +++++++++ 3 files changed, 101 insertions(+) diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index 629ef354f..bacfd816e 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -6,6 +6,7 @@ from operator import itemgetter import aqt from anki.consts import NEW_CARDS_RANDOM from anki.lang import _, ngettext +from aqt import gui_hooks from aqt.qt import * from aqt.utils import ( askUser, @@ -40,6 +41,7 @@ class DeckConf(QDialog): self.setWindowTitle(_("Options for %s") % self.deck["name"]) # qt doesn't size properly with altered fonts otherwise restoreGeom(self, "deckconf", adjustSize=True) + gui_hooks.deck_conf_will_show(self) self.show() self.exec_() saveGeom(self, "deckconf") @@ -218,6 +220,7 @@ class DeckConf(QDialog): f.replayQuestion.setChecked(c.get("replayq", True)) # description f.desc.setPlainText(self.deck["desc"]) + gui_hooks.deck_conf_did_load_config(self, self.conf) def onRestore(self): self.mw.progress.start() @@ -301,6 +304,7 @@ class DeckConf(QDialog): c["replayq"] = f.replayQuestion.isChecked() # description self.deck["desc"] = f.desc.toPlainText() + gui_hooks.deck_conf_will_save_config(self, self.deck, self.conf) self.mw.col.decks.save(self.deck) self.mw.col.decks.save(self.conf) diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index bc6dd98e7..80c7b4630 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -518,6 +518,86 @@ class _DeckBrowserWillShowOptionsMenuHook: deck_browser_will_show_options_menu = _DeckBrowserWillShowOptionsMenuHook() +class _DeckConfDidLoadConfigHook: + """Called once widget state has been set from deck config""" + + _hooks: List[Callable[["aqt.deckconf.DeckConf", Any], None]] = [] + + def append(self, cb: Callable[["aqt.deckconf.DeckConf", Any], None]) -> None: + """(deck_conf: aqt.deckconf.DeckConf, config: Any)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[["aqt.deckconf.DeckConf", Any], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, deck_conf: aqt.deckconf.DeckConf, config: Any) -> None: + for hook in self._hooks: + try: + hook(deck_conf, config) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +deck_conf_did_load_config = _DeckConfDidLoadConfigHook() + + +class _DeckConfWillSaveConfigHook: + """Called before widget state is saved to config""" + + _hooks: List[Callable[["aqt.deckconf.DeckConf", Any, Any], None]] = [] + + def append(self, cb: Callable[["aqt.deckconf.DeckConf", Any, Any], None]) -> None: + """(deck_conf: aqt.deckconf.DeckConf, config: Any, deck: Any)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[["aqt.deckconf.DeckConf", Any, Any], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__( + self, deck_conf: aqt.deckconf.DeckConf, config: Any, deck: Any + ) -> None: + for hook in self._hooks: + try: + hook(deck_conf, config, deck) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +deck_conf_will_save_config = _DeckConfWillSaveConfigHook() + + +class _DeckConfWillShowHook: + """Allows modifying the deck options dialog before it is shown""" + + _hooks: List[Callable[["aqt.deckconf.DeckConf"], None]] = [] + + def append(self, cb: Callable[["aqt.deckconf.DeckConf"], None]) -> None: + """(deck_conf: aqt.deckconf.DeckConf)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[["aqt.deckconf.DeckConf"], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, deck_conf: aqt.deckconf.DeckConf) -> None: + for hook in self._hooks: + try: + hook(deck_conf) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +deck_conf_will_show = _DeckConfWillShowHook() + + class _EditorDidFireTypingTimerHook: _hooks: List[Callable[["anki.notes.Note"], None]] = [] diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 17e55ea25..0d475b53a 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -121,6 +121,23 @@ hooks = [ legacy_hook="prepareQA", doc="Can modify card text before review/preview.", ), + # Deck options + ################### + Hook( + name="deck_conf_will_show", + args=["deck_conf: aqt.deckconf.DeckConf"], + doc="Allows modifying the deck options dialog before it is shown", + ), + Hook( + name="deck_conf_did_load_config", + args=["deck_conf: aqt.deckconf.DeckConf", "config: Any"], + doc="Called once widget state has been set from deck config", + ), + Hook( + name="deck_conf_will_save_config", + args=["deck_conf: aqt.deckconf.DeckConf", "config: Any", "deck: Any"], + doc="Called before widget state is saved to config", + ), # Browser ################### Hook( From 7cc9311b79ec3ae7592902db4c5f9a76943cf353 Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Mon, 24 Feb 2020 15:29:23 +0100 Subject: [PATCH 02/12] Add deck_conf_did_setup_ui_form hook Called earlier than deck_conf_will_show, allowing add-on authors to perform UI modifications before the deck config is loaded. --- qt/aqt/deckconf.py | 1 + qt/aqt/gui_hooks.py | 26 ++++++++++++++++++++++++++ qt/tools/genhooks_gui.py | 5 +++++ 3 files changed, 32 insertions(+) diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index bacfd816e..219632ebc 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -29,6 +29,7 @@ class DeckConf(QDialog): self._origNewOrder = None self.form = aqt.forms.dconf.Ui_Dialog() self.form.setupUi(self) + gui_hooks.deck_conf_did_setup_ui_form(self) self.mw.checkpoint(_("Options")) self.setupCombos() self.setupConfs() diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index 80c7b4630..f019a803f 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -544,6 +544,32 @@ class _DeckConfDidLoadConfigHook: deck_conf_did_load_config = _DeckConfDidLoadConfigHook() +class _DeckConfDidSetupUiFormHook: + """Allows modifying or adding widgets in the deck options UI form""" + + _hooks: List[Callable[["aqt.deckconf.DeckConf"], None]] = [] + + def append(self, cb: Callable[["aqt.deckconf.DeckConf"], None]) -> None: + """(deck_conf: aqt.deckconf.DeckConf)""" + self._hooks.append(cb) + + def remove(self, cb: Callable[["aqt.deckconf.DeckConf"], None]) -> None: + if cb in self._hooks: + self._hooks.remove(cb) + + def __call__(self, deck_conf: aqt.deckconf.DeckConf) -> None: + for hook in self._hooks: + try: + hook(deck_conf) + except: + # if the hook fails, remove it + self._hooks.remove(hook) + raise + + +deck_conf_did_setup_ui_form = _DeckConfDidSetupUiFormHook() + + class _DeckConfWillSaveConfigHook: """Called before widget state is saved to config""" diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index 0d475b53a..c7d959f51 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -123,6 +123,11 @@ hooks = [ ), # Deck options ################### + Hook( + name="deck_conf_did_setup_ui_form", + args=["deck_conf: aqt.deckconf.DeckConf"], + doc="Allows modifying or adding widgets in the deck options UI form", + ), Hook( name="deck_conf_will_show", args=["deck_conf: aqt.deckconf.DeckConf"], From 8454e27efb6d3180b983c84ed354f599c905a82d Mon Sep 17 00:00:00 2001 From: Glutanimate Date: Mon, 24 Feb 2020 15:47:48 +0100 Subject: [PATCH 03/12] Use a consistent function signature across load and save hooks --- qt/aqt/deckconf.py | 2 +- qt/aqt/gui_hooks.py | 20 +++++++++++--------- qt/tools/genhooks_gui.py | 4 ++-- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/qt/aqt/deckconf.py b/qt/aqt/deckconf.py index 219632ebc..6527ff22b 100644 --- a/qt/aqt/deckconf.py +++ b/qt/aqt/deckconf.py @@ -221,7 +221,7 @@ class DeckConf(QDialog): f.replayQuestion.setChecked(c.get("replayq", True)) # description f.desc.setPlainText(self.deck["desc"]) - gui_hooks.deck_conf_did_load_config(self, self.conf) + gui_hooks.deck_conf_did_load_config(self, self.deck, self.conf) def onRestore(self): self.mw.progress.start() diff --git a/qt/aqt/gui_hooks.py b/qt/aqt/gui_hooks.py index f019a803f..de0ec671d 100644 --- a/qt/aqt/gui_hooks.py +++ b/qt/aqt/gui_hooks.py @@ -521,20 +521,22 @@ deck_browser_will_show_options_menu = _DeckBrowserWillShowOptionsMenuHook() class _DeckConfDidLoadConfigHook: """Called once widget state has been set from deck config""" - _hooks: List[Callable[["aqt.deckconf.DeckConf", Any], None]] = [] + _hooks: List[Callable[["aqt.deckconf.DeckConf", Any, Any], None]] = [] - def append(self, cb: Callable[["aqt.deckconf.DeckConf", Any], None]) -> None: - """(deck_conf: aqt.deckconf.DeckConf, config: Any)""" + def append(self, cb: Callable[["aqt.deckconf.DeckConf", Any, Any], None]) -> None: + """(deck_conf: aqt.deckconf.DeckConf, deck: Any, config: Any)""" self._hooks.append(cb) - def remove(self, cb: Callable[["aqt.deckconf.DeckConf", Any], None]) -> None: + def remove(self, cb: Callable[["aqt.deckconf.DeckConf", Any, Any], None]) -> None: if cb in self._hooks: self._hooks.remove(cb) - def __call__(self, deck_conf: aqt.deckconf.DeckConf, config: Any) -> None: + def __call__( + self, deck_conf: aqt.deckconf.DeckConf, deck: Any, config: Any + ) -> None: for hook in self._hooks: try: - hook(deck_conf, config) + hook(deck_conf, deck, config) except: # if the hook fails, remove it self._hooks.remove(hook) @@ -576,7 +578,7 @@ class _DeckConfWillSaveConfigHook: _hooks: List[Callable[["aqt.deckconf.DeckConf", Any, Any], None]] = [] def append(self, cb: Callable[["aqt.deckconf.DeckConf", Any, Any], None]) -> None: - """(deck_conf: aqt.deckconf.DeckConf, config: Any, deck: Any)""" + """(deck_conf: aqt.deckconf.DeckConf, deck: Any, config: Any)""" self._hooks.append(cb) def remove(self, cb: Callable[["aqt.deckconf.DeckConf", Any, Any], None]) -> None: @@ -584,11 +586,11 @@ class _DeckConfWillSaveConfigHook: self._hooks.remove(cb) def __call__( - self, deck_conf: aqt.deckconf.DeckConf, config: Any, deck: Any + self, deck_conf: aqt.deckconf.DeckConf, deck: Any, config: Any ) -> None: for hook in self._hooks: try: - hook(deck_conf, config, deck) + hook(deck_conf, deck, config) except: # if the hook fails, remove it self._hooks.remove(hook) diff --git a/qt/tools/genhooks_gui.py b/qt/tools/genhooks_gui.py index c7d959f51..b92d0a969 100644 --- a/qt/tools/genhooks_gui.py +++ b/qt/tools/genhooks_gui.py @@ -135,12 +135,12 @@ hooks = [ ), Hook( name="deck_conf_did_load_config", - args=["deck_conf: aqt.deckconf.DeckConf", "config: Any"], + args=["deck_conf: aqt.deckconf.DeckConf", "deck: Any", "config: Any"], doc="Called once widget state has been set from deck config", ), Hook( name="deck_conf_will_save_config", - args=["deck_conf: aqt.deckconf.DeckConf", "config: Any", "deck: Any"], + args=["deck_conf: aqt.deckconf.DeckConf", "deck: Any", "config: Any"], doc="Called before widget state is saved to config", ), # Browser From 87c5316123f3a3afe34aca28f2691d91e1c687f6 Mon Sep 17 00:00:00 2001 From: evandrocoan Date: Mon, 24 Feb 2020 16:59:07 -0300 Subject: [PATCH 04/12] Fixed build-mo-files not stopping on build errors --- qt/i18n/build-mo-files | 1 + qt/i18n/sync-po-git | 1 + qt/i18n/update-po-template | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/qt/i18n/build-mo-files b/qt/i18n/build-mo-files index 4666a7e44..b51cfd8a3 100755 --- a/qt/i18n/build-mo-files +++ b/qt/i18n/build-mo-files @@ -2,6 +2,7 @@ # # build mo files # +set -e targetDir="../aqt_data/locale/gettext" mkdir -p $targetDir diff --git a/qt/i18n/sync-po-git b/qt/i18n/sync-po-git index 55c778d9f..b085d3113 100755 --- a/qt/i18n/sync-po-git +++ b/qt/i18n/sync-po-git @@ -1,4 +1,5 @@ #!/bin/bash +set -e # pull any pending changes from git repos ./pull-git diff --git a/qt/i18n/update-po-template b/qt/i18n/update-po-template index 34a223037..3c8323c4e 100755 --- a/qt/i18n/update-po-template +++ b/qt/i18n/update-po-template @@ -2,7 +2,7 @@ # # update template .pot file from source code strings # - +set -e topDir=$(dirname $0)/../.. cd $topDir From c171104a8168988a86d774f2b69929f87a2fb958 Mon Sep 17 00:00:00 2001 From: evandrocoan Date: Mon, 24 Feb 2020 17:53:26 -0300 Subject: [PATCH 05/12] Fixed msgmerge not stopping the shell on error --- qt/i18n/build-mo-files | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/qt/i18n/build-mo-files b/qt/i18n/build-mo-files index b51cfd8a3..9d0136087 100755 --- a/qt/i18n/build-mo-files +++ b/qt/i18n/build-mo-files @@ -2,7 +2,7 @@ # # build mo files # -set -e +set -eo pipefail targetDir="../aqt_data/locale/gettext" mkdir -p $targetDir @@ -10,9 +10,9 @@ mkdir -p $targetDir echo "Compiling *.po..." for file in po/desktop/*/anki.po do - outdir=$(echo $file | \ + outdir=$(echo "$file" | \ perl -pe "s%po/desktop/(.*)/anki.po%$targetDir/\1/LC_MESSAGES%") outfile="$outdir/anki.mo" mkdir -p $outdir - msgmerge --for-msgfmt $file po/desktop/anki.pot | msgfmt - --output-file=$outfile + msgmerge --for-msgfmt "$file" po/desktop/anki.pot | msgfmt - --output-file=$outfile done From c781de8c24d47d794094aea172db4210fa92f6bb Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 08:26:20 +1000 Subject: [PATCH 06/12] sort FStrings - easier to read - ensures things don't break when ankirspy and anki wheels built on separate machines due to mismatched directory order --- rslib/build.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rslib/build.rs b/rslib/build.rs index 8e4f645e6..bf365568f 100644 --- a/rslib/build.rs +++ b/rslib/build.rs @@ -15,6 +15,8 @@ fn get_identifiers(ftl_text: &str) -> Vec { } } + idents.sort(); + idents } From 47ccd6638d55a8b92eb6b985dcfc085ac188e6ac Mon Sep 17 00:00:00 2001 From: evandrocoan Date: Mon, 24 Feb 2020 20:10:10 -0300 Subject: [PATCH 07/12] Added -o pipefail to all set -e ensuring the build fails when some operation with pipe exits error status. # Conflicts: # Makefile --- .github/scripts/contrib.sh | 2 +- Makefile | 10 +++++----- README.contributing | 2 +- qt/i18n/build-mo-files | 2 +- qt/i18n/copy-qt-files | 2 +- qt/i18n/sync-po-git | 2 +- qt/i18n/update-po-template | 2 +- qt/tools/build_ui.sh | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/scripts/contrib.sh b/.github/scripts/contrib.sh index 438aa7ebf..055e917ae 100755 --- a/.github/scripts/contrib.sh +++ b/.github/scripts/contrib.sh @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -eo pipefail antispam=", at the domain " diff --git a/Makefile b/Makefile index a2d8a7d44..2a90b3e11 100644 --- a/Makefile +++ b/Makefile @@ -34,7 +34,7 @@ buildhash: .PHONY: develop develop: pyenv buildhash - @set -e && \ + @set -eo pipefail && \ . pyenv/bin/activate && \ for dir in $(DEVEL); do \ $(SUBMAKE) -C $$dir develop BUILDFLAGS="$(BUILDFLAGS)"; \ @@ -42,7 +42,7 @@ develop: pyenv buildhash .PHONY: run run: develop - @set -e && \ + @set -eo pipefail && \ . pyenv/bin/activate && \ echo "Starting Anki..."; \ qt/runanki $(RUNFLAGS) @@ -69,7 +69,7 @@ build-qt: .PHONY: clean clean: clean-dist - @set -e && \ + @set -eo pipefail && \ for dir in $(DEVEL); do \ $(SUBMAKE) -C $$dir clean; \ done @@ -80,7 +80,7 @@ clean-dist: .PHONY: check check: pyenv buildhash - @set -e && \ + @set -eo pipefail && \ for dir in $(CHECKABLE_RS); do \ $(SUBMAKE) -C $$dir check; \ done; \ @@ -95,7 +95,7 @@ check: pyenv buildhash .PHONY: fix fix: - @set -e && \ + @set -eo pipefail && \ . pyenv/bin/activate && \ for dir in $(CHECKABLE_RS) $(CHECKABLE_PY); do \ $(SUBMAKE) -C $$dir fix; \ diff --git a/README.contributing b/README.contributing index 645d59218..80a2bd88c 100644 --- a/README.contributing +++ b/README.contributing @@ -104,7 +104,7 @@ You can do this automatically by adding the following into .git/hooks/pre-commit or .git/hooks/pre-push and making it executable. #!/bin/bash -set -e +set -eo pipefail make check You may need to adjust the PATH variable so that things like a local install diff --git a/qt/i18n/build-mo-files b/qt/i18n/build-mo-files index 9d0136087..3d3ecc3ee 100755 --- a/qt/i18n/build-mo-files +++ b/qt/i18n/build-mo-files @@ -14,5 +14,5 @@ do perl -pe "s%po/desktop/(.*)/anki.po%$targetDir/\1/LC_MESSAGES%") outfile="$outdir/anki.mo" mkdir -p $outdir - msgmerge --for-msgfmt "$file" po/desktop/anki.pot | msgfmt - --output-file=$outfile + msgmerge --for-msgfmt "$file" po/desktop/anki.pot | msgfmt - --output-file="$outfile" done diff --git a/qt/i18n/copy-qt-files b/qt/i18n/copy-qt-files index fe6210b18..6955bc4ea 100755 --- a/qt/i18n/copy-qt-files +++ b/qt/i18n/copy-qt-files @@ -1,6 +1,6 @@ #!/bin/bash -set -e +set -eo pipefail out=../aqt_data/locale/qt mkdir -p $out diff --git a/qt/i18n/sync-po-git b/qt/i18n/sync-po-git index b085d3113..5d5910185 100755 --- a/qt/i18n/sync-po-git +++ b/qt/i18n/sync-po-git @@ -1,5 +1,5 @@ #!/bin/bash -set -e +set -eo pipefail # pull any pending changes from git repos ./pull-git diff --git a/qt/i18n/update-po-template b/qt/i18n/update-po-template index 3c8323c4e..a67197e01 100755 --- a/qt/i18n/update-po-template +++ b/qt/i18n/update-po-template @@ -2,7 +2,7 @@ # # update template .pot file from source code strings # -set -e +set -eo pipefail topDir=$(dirname $0)/../.. cd $topDir diff --git a/qt/tools/build_ui.sh b/qt/tools/build_ui.sh index 33deeb4df..80e28d37a 100755 --- a/qt/tools/build_ui.sh +++ b/qt/tools/build_ui.sh @@ -4,7 +4,7 @@ # should be on the path. # -set -e +set -eo pipefail if [ ! -d "designer" ] then From b412747a16f68d203c44799f3e60c2c4c2f7acdb Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 12:57:35 +1000 Subject: [PATCH 08/12] add workaround for panic on 32 bit systems https://anki.tenderapp.com/discussions/beta-testing/1817-failed-to-generate-operands-out-of-fluentnumber-could-not-convert-string-to-integer --- rslib/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rslib/Cargo.toml b/rslib/Cargo.toml index dbdf92e51..0b6d23b79 100644 --- a/rslib/Cargo.toml +++ b/rslib/Cargo.toml @@ -31,8 +31,8 @@ coarsetime = "=0.1.11" utime = "0.2.1" serde-aux = "0.6.1" unic-langid = { version = "0.8.0", features = ["macros"] } -fluent = "0.10.2" -intl-memoizer = "0.3.0" +fluent = { git = "https://github.com/ankitects/fluent-rs.git", branch="32bit-panic" } +intl-memoizer = { git = "https://github.com/ankitects/fluent-rs.git", branch="32bit-panic" } num-format = "0.4.0" [target.'cfg(target_vendor="apple")'.dependencies] From c58b4158a7d35d50b9a5a4d034827d9c5ef7139d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 13:11:47 +1000 Subject: [PATCH 09/12] use Fluent's number formatting; don't show trailing zeros We can add NUMBER() in the future for more control, but this will do for the time being. --- rslib/src/i18n/mod.rs | 30 +++++++++++++----------------- rslib/src/sched/timespan.rs | 4 ++-- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/rslib/src/i18n/mod.rs b/rslib/src/i18n/mod.rs index b0f650447..4f6c4626c 100644 --- a/rslib/src/i18n/mod.rs +++ b/rslib/src/i18n/mod.rs @@ -323,7 +323,11 @@ fn set_bundle_formatter_for_langs(bundle: &mut FluentBundle, langs: &[Lang let num_formatter = NumberFormatter::new(langs); let formatter = move |val: &FluentValue, _intls: &Mutex| -> Option { match val { - FluentValue::Number(n) => Some(num_formatter.format(n.value)), + FluentValue::Number(n) => { + let mut num = n.clone(); + num.options.maximum_fraction_digits = Some(2); + Some(num_formatter.format(num.as_string().to_string())) + } _ => None, } }; @@ -371,17 +375,12 @@ impl NumberFormatter { } } - fn format(&self, num: f64) -> String { - // is it an integer? - if (num - num.round()).abs() < std::f64::EPSILON { - num.to_string() + /// Given a pre-formatted number, change the decimal separator as appropriate. + fn format(&self, num: String) -> String { + if self.decimal_separator != "." { + num.replace(".", self.decimal_separator) } else { - // currently we hard-code to 2 decimal places - let mut formatted = format!("{:.2}", num); - if self.decimal_separator != "." { - formatted = formatted.replace(".", self.decimal_separator) - } - formatted + num } } } @@ -395,11 +394,8 @@ mod test { #[test] fn numbers() { - let fmter = NumberFormatter::new(&[]); - assert_eq!(&fmter.format(1.0), "1"); - assert_eq!(&fmter.format(1.007), "1.01"); let fmter = NumberFormatter::new(&[langid!("pl-PL")]); - assert_eq!(&fmter.format(1.007), "1,01"); + assert_eq!(&fmter.format("1.007".to_string()), "1,007"); } #[test] @@ -414,7 +410,7 @@ mod test { assert_eq!( i18n.tr_("two-args-key", Some(tr_args!["one"=>1.1, "two"=>"2"])), - "two args: 1.10 and 2" + "two args: 1.1 and 2" ); assert_eq!( @@ -423,7 +419,7 @@ mod test { ); assert_eq!( i18n.tr_("plural", Some(tr_args!["hats"=>1.1])), - "You have 1.10 hats." + "You have 1.1 hats." ); assert_eq!( i18n.tr_("plural", Some(tr_args!["hats"=>3])), diff --git a/rslib/src/sched/timespan.rs b/rslib/src/sched/timespan.rs index 9d426ee8a..b5725a7b1 100644 --- a/rslib/src/sched/timespan.rs +++ b/rslib/src/sched/timespan.rs @@ -173,7 +173,7 @@ mod test { 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(1.1 * MONTH, &i18n), "1.10mo"); + assert_eq!(answer_button_time(1.1 * MONTH, &i18n), "1.1mo"); } #[test] @@ -181,7 +181,7 @@ mod test { 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.50 minutes"); + assert_eq!(time_span(90.0, &i18n), "1.5 minutes"); } #[test] From b1a192b384b84d33967b72d212ecbf462d1cfee3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 13:21:26 +1000 Subject: [PATCH 10/12] cap answer buttons to 1 decimal place we can switch to NUMBER() instead in the future, but will need to update all the translations at the same time --- pylib/tests/test_schedv1.py | 2 +- pylib/tests/test_schedv2.py | 2 +- rslib/src/sched/timespan.rs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pylib/tests/test_schedv1.py b/pylib/tests/test_schedv1.py index edff07beb..d59264997 100644 --- a/pylib/tests/test_schedv1.py +++ b/pylib/tests/test_schedv1.py @@ -516,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 without_unicode_isolation(d.sched.nextIvlStr(c, 4)) == "10.83mo" + assert without_unicode_isolation(d.sched.nextIvlStr(c, 4)) == "10.8mo" def test_misc(): diff --git a/pylib/tests/test_schedv2.py b/pylib/tests/test_schedv2.py index 2b8f996ac..0df6c2005 100644 --- a/pylib/tests/test_schedv2.py +++ b/pylib/tests/test_schedv2.py @@ -613,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 without_unicode_isolation(d.sched.nextIvlStr(c, 4)) == "10.83mo" + assert without_unicode_isolation(d.sched.nextIvlStr(c, 4)) == "10.8mo" def test_bury(): diff --git a/rslib/src/sched/timespan.rs b/rslib/src/sched/timespan.rs index b5725a7b1..9c496811e 100644 --- a/rslib/src/sched/timespan.rs +++ b/rslib/src/sched/timespan.rs @@ -7,8 +7,9 @@ use crate::i18n::{tr_args, FString, I18n}; 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 values except for months/years + // 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]; From 22e8b7cd84672c217055198a404582ab6ceb63f6 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 13:52:40 +1000 Subject: [PATCH 11/12] use -q instead of --for-msgfmt so older gettext versions don't break I was mainly using it to keep the output tidy anyway --- qt/i18n/build-mo-files | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qt/i18n/build-mo-files b/qt/i18n/build-mo-files index 3d3ecc3ee..8feaf7cfe 100755 --- a/qt/i18n/build-mo-files +++ b/qt/i18n/build-mo-files @@ -14,5 +14,5 @@ do perl -pe "s%po/desktop/(.*)/anki.po%$targetDir/\1/LC_MESSAGES%") outfile="$outdir/anki.mo" mkdir -p $outdir - msgmerge --for-msgfmt "$file" po/desktop/anki.pot | msgfmt - --output-file="$outfile" + msgmerge -q "$file" po/desktop/anki.pot | msgfmt - --output-file="$outfile" done From a333e2024bcbceb318c048f7800f535f0345249f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 25 Feb 2020 15:32:11 +1000 Subject: [PATCH 12/12] add shortcut to update translations to makefile --- Makefile | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Makefile b/Makefile index 2a90b3e11..f1a22e879 100644 --- a/Makefile +++ b/Makefile @@ -106,3 +106,16 @@ add-buildhash: @ver=$$(cat meta/version); \ hash=$$(cat meta/buildhash); \ rename "s/-$${ver}-/-$${ver}+$${hash}-/" dist/*-$$ver-* + + +.PHONY: pull-i18n +pull-i18n: + (cd rslib/ftl && scripts/fetch-latest-translations) + (cd qt/ftl && scripts/fetch-latest-translations) + (cd qt/i18n && ./pull-git) + +.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)