diff --git a/chrome/content/zotero/dictionaryManager.js b/chrome/content/zotero/dictionaryManager.js
new file mode 100644
index 0000000000..d85630d740
--- /dev/null
+++ b/chrome/content/zotero/dictionaryManager.js
@@ -0,0 +1,193 @@
+/*
+ ***** BEGIN LICENSE BLOCK *****
+
+ Copyright © 2021 Corporation for Digital Scholarship
+ Vienna, Virginia, USA
+ https://digitalscholar.org
+
+ This file is part of Zotero.
+
+ Zotero is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ Zotero is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with Zotero. If not, see .
+
+ ***** END LICENSE BLOCK *****
+*/
+
+"use strict";
+
+// eslint-disable-next-line camelcase, no-unused-vars
+var Zotero_Dictionary_Manager = new function () {
+ const HTML_NS = 'http://www.w3.org/1999/xhtml';
+
+ var installed;
+ var updateMap;
+
+ this.init = async function () {
+ document.title = Zotero.getString('spellCheck.dictionaryManager.title');
+
+ installed = new Set(Zotero.Dictionaries.dictionaries.map(d => d.id));
+ var installedLocales = new Set(Zotero.Dictionaries.dictionaries.map(d => d.locale));
+ var availableDictionaries = await Zotero.Dictionaries.fetchDictionariesList();
+ var availableUpdates = await Zotero.Dictionaries.getAvailableUpdates(availableDictionaries);
+ updateMap = new Map(availableUpdates.map(x => [x.old.id, x.new.id]));
+
+ var { InlineSpellChecker } = ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm", {});
+ var isc = new InlineSpellChecker();
+
+ // Start with installed dictionaries
+ var list = [];
+ for (let d of Zotero.Dictionaries.dictionaries) {
+ let name = Zotero.Dictionaries.getBestDictionaryName(d.locale, isc);
+ list.push(Object.assign({}, d, { name }));
+ }
+ // Add remote dictionaries not in the list
+ for (let d of availableDictionaries) {
+ if (!installed.has(d.id) && !installedLocales.has(d.locale)) {
+ list.push(d);
+ }
+ }
+ var positionMap = new Map(availableDictionaries.map((d, i) => [d.locale, i + 1]));
+ list.sort((a, b) => {
+ // If both locales are in original list, use the original sort order
+ let posA = positionMap.get(a.locale);
+ let posB = positionMap.get(b.locale);
+ if (posA && posB) {
+ return posA - posB;
+ }
+ // Otherwise compare the locale codes
+ return Zotero.localeCompare(a.locale, b.locale);
+ });
+
+ // Build list
+ var listbox = document.getElementById('dictionaries');
+ for (let d of list) {
+ let name = d.name;
+ let li = document.createElement('richlistitem');
+ let div = document.createElementNS(HTML_NS, 'div');
+
+ let checkbox = document.createElementNS(HTML_NS, 'input');
+ checkbox.type = 'checkbox';
+ checkbox.id = d.locale;
+ // Store properties on element
+ // .id will be the current id for installed dictionaries and otherwise the remote id
+ checkbox.dataset.dictId = d.id;
+ checkbox.dataset.dictLocale = d.locale;
+ checkbox.dataset.dictName = d.name;
+ // en-US is always checked and disabled
+ checkbox.checked = d.locale == 'en-US' || installed.has(d.id);
+ if (d.locale == 'en-US') {
+ checkbox.disabled = true;
+ }
+ checkbox.setAttribute('tabindex', -1);
+
+ let label = document.createElementNS(HTML_NS, 'label');
+ label.setAttribute('for', d.locale);
+ // Add " (update available)"
+ if (updateMap.has(d.id)) {
+ name = Zotero.getString('spellCheck.dictionaryManager.updateAvailable', name);
+ }
+ label.textContent = name;
+ // Don't toggle checkbox for single-click on label
+ label.onclick = (event) => {
+ if (event.detail == 1) {
+ event.preventDefault();
+ }
+ };
+
+ div.appendChild(checkbox);
+ div.appendChild(label);
+ li.appendChild(div);
+ listbox.appendChild(li);
+ }
+ listbox.selectedIndex = 0;
+ };
+
+ this.handleAccept = async function () {
+ // Download selected dictionaries if updated or not currently installed
+ var elems = document.querySelectorAll('input[type=checkbox]');
+ var toRemove = [];
+ var toDownload = [];
+ for (let elem of elems) {
+ if (elem.dataset.dictLocale == 'en-US') {
+ continue;
+ }
+
+ let id = elem.dataset.dictId;
+ if (!elem.checked) {
+ if (installed.has(id)) {
+ toRemove.push(id);
+ }
+ continue;
+ }
+
+ if (updateMap.has(id)) {
+ // If id is changing, delete the old one first
+ toRemove.push(id);
+ toDownload.push({ id: updateMap.get(id), name: elem.dataset.dictName });
+ }
+ else if (!installed.has(id)) {
+ toDownload.push({ id, name: elem.dataset.dictName });
+ }
+ }
+ if (toRemove.length) {
+ for (let id of toRemove) {
+ await Zotero.Dictionaries.remove(id);
+ }
+ }
+ if (toDownload.length) {
+ for (let { id, name } of toDownload) {
+ _updateStatus(Zotero.getString('general.downloading.quoted', name));
+ try {
+ await Zotero.Dictionaries.install(id);
+ }
+ catch (e) {
+ Zotero.logError(e);
+ Zotero.alert(
+ null,
+ Zotero.getString('general.error'),
+ Zotero.getString('spellCheck.dictionaryManager.error.unableToInstall', name)
+ + "\n\n" + e.message
+ );
+ return;
+ }
+ finally {
+ _updateStatus();
+ }
+ }
+ }
+ window.close();
+ };
+
+ function _updateStatus(msg) {
+ var elem = document.getElementById('status');
+ elem.textContent = msg
+ // Use non-breaking space to maintain height when empty
+ || '\xA0';
+ }
+};
+
+window.addEventListener('keypress', function (event) {
+ // Toggle checkbox on spacebar
+ if (event.key == ' ') {
+ if (event.target.localName == 'richlistbox') {
+ let elem = event.target.selectedItem.querySelector('input[type=checkbox]');
+ if (!elem.disabled) {
+ elem.checked = !elem.checked;
+ }
+ }
+ }
+
+ if (event.key == 'Enter') {
+ document.querySelector('button[dlgtype="accept"]').click();
+ }
+});
diff --git a/chrome/content/zotero/dictionaryManager.xul b/chrome/content/zotero/dictionaryManager.xul
new file mode 100644
index 0000000000..482b3102ad
--- /dev/null
+++ b/chrome/content/zotero/dictionaryManager.xul
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/chrome/content/zotero/xpcom/dictionaries.js b/chrome/content/zotero/xpcom/dictionaries.js
index 8b96ae6f0d..f2c262d0b2 100644
--- a/chrome/content/zotero/xpcom/dictionaries.js
+++ b/chrome/content/zotero/xpcom/dictionaries.js
@@ -39,6 +39,7 @@ Zotero.Dictionaries = new function () {
}
});
+ // Note: Doesn't include bundled en-US
Zotero.defineProperty(this, 'dictionaries', {
get: () => {
return _dictionaries;
@@ -63,7 +64,7 @@ Zotero.Dictionaries = new function () {
}
try {
let dir = OS.Path.join(dictionariesDir, entry.name);
- await _loadDictionary(dir);
+ await _loadDirectory(dir);
}
catch (e) {
Zotero.logError(e);
@@ -86,41 +87,6 @@ Zotero.Dictionaries = new function () {
return req.response;
};
- /**
- * Install the most popular dictionary for specified locale
- *
- * @param locale
- * @return {Promise}
- */
- this.installByLocale = async function (locale) {
- let dictionaries = await this.fetchDictionariesList();
- let matched = dictionaries.filter(x => x.locale === locale);
- if (!matched.length) {
- matched = dictionaries.filter(x => x.locale === locale.split(/[-_]/)[0]);
- }
- if (!matched.length) {
- return false;
- }
- matched.sort((a, b) => b.users - a.users);
- await this.install(matched[0].id);
- return true;
- };
-
- /**
- * Remove all dictionaries targeting specific locale
- *
- * @param locale
- * @return {Promise}
- */
- this.removeByLocale = async function(locale) {
- for (let dictionary of _dictionaries) {
- if (dictionary.locales.includes(locale)
- || dictionary.locales.some(x => x === locale.split(/[-_]/)[0])) {
- await this.remove(dictionary.id);
- }
- }
- };
-
/**
* Install dictionary by extension id,
* e.g., `en-NZ@dictionaries.addons.mozilla.org`
@@ -129,7 +95,11 @@ Zotero.Dictionaries = new function () {
* @return {Promise}
*/
this.install = async function (id) {
+ if (id == '@unitedstatesenglishdictionary') {
+ throw new Error("en-US dictionary is bundled");
+ }
await this.remove(id);
+ Zotero.debug("Installing dictionaries from " + id);
let url = this.baseURL + id + '.xpi';
let xpiPath = OS.Path.join(Zotero.getTempDirectory().path, id);
let dir = OS.Path.join(Zotero.Profile.dir, 'dictionaries', id);
@@ -162,7 +132,7 @@ Zotero.Dictionaries = new function () {
zipReader.close();
await OS.File.remove(xpiPath);
- await _loadDictionary(dir);
+ await _loadDirectory(dir);
}
catch (e) {
if (await OS.File.exists(xpiPath)) {
@@ -176,46 +146,116 @@ Zotero.Dictionaries = new function () {
};
/**
- * Remove dictionary by extension id
+ * Remove dictionaries by extension id
*
* @param {String} id
* @return {Promise}
*/
this.remove = async function (id) {
- let dictionaryIndex = _dictionaries.findIndex(x => x.id === id);
- if (dictionaryIndex !== -1) {
- let dictionary = _dictionaries[dictionaryIndex];
- try {
- let manifestPath = OS.Path.join(dictionary.dir, 'manifest.json');
- let manifest = await Zotero.File.getContentsAsync(manifestPath);
- manifest = JSON.parse(manifest);
- for (let locale in manifest.dictionaries) {
- let dicPath = manifest.dictionaries[locale];
- let affPath = OS.Path.join(dictionary.dir, dicPath.slice(0, -3) + 'aff');
- _spellChecker.removeDictionary(locale, Zotero.File.pathToFile(affPath));
+ Zotero.debug("Removing dictionaries from " + id);
+ var dictionary = _dictionaries.find(x => x.id === id);
+ if (!dictionary) {
+ return;
+ }
+ try {
+ let manifestPath = OS.Path.join(dictionary.dir, 'manifest.json');
+ let manifest = await Zotero.File.getContentsAsync(manifestPath);
+ manifest = JSON.parse(manifest);
+ for (let locale in manifest.dictionaries) {
+ let dicPath = manifest.dictionaries[locale];
+ let affPath = OS.Path.join(dictionary.dir, dicPath.slice(0, -3) + 'aff');
+ Zotero.debug(`Removing ${locale} dictionary`);
+ _spellChecker.removeDictionary(locale, Zotero.File.pathToFile(affPath));
+ }
+ }
+ catch (e) {
+ Zotero.logError(e);
+ }
+ await OS.File.removeDir(dictionary.dir);
+ // Technically there can be more than one dictionary provided by the same extension id,
+ // so remove all that match
+ _dictionaries = _dictionaries.filter(x => x.id != id);
+ };
+
+ /**
+ * @param {Object[]} [dictionaries] - Dictionary list from fetchDictionariesList(); fetched
+ * automatically if not provided
+ * @return {Object[]} - Array of objects with 'old' and 'new'
+ */
+ this.getAvailableUpdates = async function (dictionaries) {
+ var updates = [];
+ let availableDictionaries = dictionaries || await this.fetchDictionariesList();
+ for (let dictionary of _dictionaries) {
+ let availableDictionary = availableDictionaries.find((x) => {
+ return x.id === dictionary.id || x.locale == dictionary.locale;
+ });
+ if (!availableDictionary) continue;
+ // If same id, check if version is higher
+ if (availableDictionary.id == dictionary.id) {
+ if (Services.vc.compare(dictionary.version, availableDictionary.version) < 0) {
+ updates.push({ old: dictionary, new: availableDictionary });
}
}
+ // If different id for same locale, always offer as an update
+ else {
+ updates.push({ old: dictionary, new: availableDictionary });
+ }
+ }
+ if (updates.length) {
+ Zotero.debug("Available dictionary updates:");
+ Zotero.debug(updates);
+ }
+ else {
+ Zotero.debug("No dictionary updates found");
+ }
+ return updates;
+ };
+
+ /**
+ * Get the best display name for a dictionary
+ *
+ * For known locales, this will be the native name in the target locale. If a native name isn't
+ * available and inlineSpellChecker is provided, an English name will be provided if available.
+ *
+ * @param {String} locale
+ * @param {InlineSpellChecker} [inlineSpellChecker] - An instance of InlineSpellChecker from
+ * InlineSpellChecker.jsm
+ * @return {String} - The best available name, or the locale code if unavailable
+ */
+ this.getBestDictionaryName = function (locale, inlineSpellChecker) {
+ var name = Zotero.Locale.availableLocales[locale];
+ if (!name) {
+ for (let key in Zotero.Locale.availableLocales) {
+ if (key.split('-')[0] === locale) {
+ name = Zotero.Locale.availableLocales[key];
+ }
+ }
+ }
+ if (!name && inlineSpellChecker) {
+ name = inlineSpellChecker.getDictionaryDisplayName(locale)
+ }
+ return name || name;
+ };
+
+ /**
+ * Update dictionaries
+ *
+ * @return {Promise} - Number of updated dictionaries
+ */
+ this.update = async function () {
+ var updates = await Zotero.Dictionaries.getAvailableUpdates();
+ var updated = 0;
+ for (let update of updates) {
+ try {
+ await this.remove(update.old.id);
+ await this.install(update.new.id);
+ updated++;
+ }
catch (e) {
Zotero.logError(e);
}
- await OS.File.removeDir(dictionary.dir);
- _dictionaries.splice(dictionaryIndex, 1);
- }
- };
-
- /**
- * Update all dictionaries
- *
- * @return {Promise}
- */
- this.update = async function () {
- let availableDictionaries = await this.fetchDictionariesList();
- for (let dictionary of _dictionaries) {
- let availableDictionary = availableDictionaries.find(x => x.id === dictionary.id);
- if (availableDictionary && availableDictionary.version > dictionary.version) {
- await this.install(availableDictionary.id);
- }
}
+ return updated;
};
/**
@@ -224,7 +264,7 @@ Zotero.Dictionaries = new function () {
* @param {String} dir
* @return {Promise}
*/
- async function _loadDictionary(dir) {
+ async function _loadDirectory(dir) {
let manifestPath = OS.Path.join(dir, 'manifest.json');
let manifest = await Zotero.File.getContentsAsync(manifestPath);
manifest = JSON.parse(manifest);
@@ -241,8 +281,9 @@ Zotero.Dictionaries = new function () {
locales.push(locale);
let dicPath = manifest.dictionaries[locale];
let affPath = OS.Path.join(dir, dicPath.slice(0, -3) + 'aff');
+ Zotero.debug(`Adding ${locale} dictionary`);
_spellChecker.addDictionary(locale, Zotero.File.pathToFile(affPath));
+ _dictionaries.push({ id, locale, version, dir });
}
- _dictionaries.push({ id, locales, version, dir });
}
};
diff --git a/chrome/content/zotero/xpcom/editorInstance.js b/chrome/content/zotero/xpcom/editorInstance.js
index 8d7560bcd4..7c7483e85e 100644
--- a/chrome/content/zotero/xpcom/editorInstance.js
+++ b/chrome/content/zotero/xpcom/editorInstance.js
@@ -806,8 +806,6 @@ class EditorInstance {
appendItems(this._popup, itemGroups);
- // TODO: Localize
-
// Spell checker
let spellChecker = this._getSpellChecker();
@@ -816,7 +814,7 @@ class EditorInstance {
this._popup.appendChild(separator);
// Check Spelling
var menuitem = this._popup.ownerDocument.createElement('menuitem');
- menuitem.setAttribute('label', 'Check Spelling');
+ menuitem.setAttribute('label', Zotero.getString('spellCheck.checkSpelling'));
menuitem.setAttribute('checked', spellChecker.enabled);
menuitem.addEventListener('command', () => {
// Possible values: 0 - off, 1 - only multi-line, 2 - multi and single line input boxes
@@ -827,7 +825,7 @@ class EditorInstance {
if (spellChecker.enabled) {
// Languages menu
var menu = this._popup.ownerDocument.createElement('menu');
- menu.setAttribute('label', 'Languages');
+ menu.setAttribute('label', Zotero.getString('general.languages'));
this._popup.append(menu);
// Languages menu popup
var menupopup = this._popup.ownerDocument.createElement('menupopup');
@@ -835,19 +833,13 @@ class EditorInstance {
spellChecker.addDictionaryListToMenu(menupopup, null);
+ // The menu is prepopulated with names from InlineSpellChecker::getDictionaryDisplayName(),
+ // which will be in English, so swap in native locale names where we have them
for (var menuitem of menupopup.children) {
// 'spell-check-dictionary-en-US'
let locale = menuitem.id.slice(23);
- let label = Zotero.Locale.availableLocales[locale];
- if (!label) {
- for(let key in Zotero.Locale.availableLocales) {
- if (key.split('-')[0] === locale) {
- label = Zotero.Locale.availableLocales[key];
- }
- }
- }
-
- if (label) {
+ let label = Zotero.Dictionaries.getBestDictionaryName(locale);
+ if (label && label != locale) {
menuitem.setAttribute('label', label);
}
}
@@ -857,9 +849,11 @@ class EditorInstance {
menupopup.appendChild(separator);
// Add Dictionaries
var menuitem = this._popup.ownerDocument.createElement('menuitem');
- menuitem.setAttribute('label', 'Add Dictionaries...');
+ menuitem.setAttribute('label', Zotero.getString('spellCheck.addRemoveDictionaries'));
menuitem.addEventListener('command', () => {
- Zotero.Utilities.Internal.openPreferences('zotero-prefpane-advanced');
+ Services.ww.openWindow(null, "chrome://zotero/content/dictionaryManager.xul",
+ "dictionary-manager", "chrome,centerscreen", {});
+
});
menupopup.append(menuitem);
diff --git a/chrome/content/zotero/xpcom/locale.js b/chrome/content/zotero/xpcom/locale.js
index 8ab19b0062..bb4a649ecb 100644
--- a/chrome/content/zotero/xpcom/locale.js
+++ b/chrome/content/zotero/xpcom/locale.js
@@ -1,6 +1,8 @@
Zotero.Locale = {
/**
- * Keep this up to date with chrome.manifest
+ * Keep this up to date with chrome.manifest and zotero-build/dictionaries/build-dictionaries
+ *
+ * Names from https://addons.mozilla.org/en-US/firefox/language-tools/
*/
availableLocales: Object.freeze({
'ar': 'عربي',
diff --git a/chrome/locale/en-US/zotero/zotero.properties b/chrome/locale/en-US/zotero/zotero.properties
index 075886cb70..71a542a643 100644
--- a/chrome/locale/en-US/zotero/zotero.properties
+++ b/chrome/locale/en-US/zotero/zotero.properties
@@ -26,6 +26,8 @@ general.checkForUpdates = Check for Updates
general.actionCannotBeUndone = This action cannot be undone.
general.install = Install
general.updateAvailable = Update Available
+general.downloading = Downloading %S…
+general.downloading.quoted = Downloading “%S”…
general.noUpdatesFound = No Updates Found
general.isUpToDate = %S is up to date.
general.upgrade = Upgrade
@@ -79,6 +81,7 @@ general.nMegabytes = %S MB
general.item = Item
general.pdf = PDF
general.back = Back
+general.languages = Languages
general.yellow = Yellow
general.red = Red
@@ -1342,3 +1345,9 @@ pdfReader.nextPage = Next Page
pdfReader.previousPage = Previous Page
pdfReader.page = Page
pdfReader.readOnly = Read-only
+
+spellCheck.checkSpelling = Check Spelling
+spellCheck.addRemoveDictionaries = Add/Remove Dictionaries…
+spellCheck.dictionaryManager.title = Add/Remove Dictionaries
+spellCheck.dictionaryManager.updateAvailable = %S (update available)
+spellCheck.dictionaryManager.error.unableToInstall = Unable to install “%S”:
diff --git a/scss/_zotero-react-client.scss b/scss/_zotero-react-client.scss
index 114ae477d0..4e08c775b1 100644
--- a/scss/_zotero-react-client.scss
+++ b/scss/_zotero-react-client.scss
@@ -25,6 +25,7 @@
@import "components/autosuggest";
@import "components/button";
@import "components/createParent";
+@import "components/dictionaryManager";
@import "components/editable";
@import "components/exportOptions";
@import "components/icons";
diff --git a/scss/components/_dictionaryManager.scss b/scss/components/_dictionaryManager.scss
new file mode 100644
index 0000000000..2739fc1e97
--- /dev/null
+++ b/scss/components/_dictionaryManager.scss
@@ -0,0 +1,27 @@
+#DictionaryManager {
+ padding: 15px;
+
+ richlistitem {
+ font-size: 12px;
+ }
+
+ input {
+ // Select rows, not checkboxes
+ -moz-user-focus: ignore;
+ }
+
+ #status {
+ margin: 9px 0 0 5px;
+ font-size: 10px;
+ line-height: 1em;
+ }
+
+ .dialog-button-box {
+ display: flex;
+ justify-content: end;
+ }
+
+ button {
+ font-size: 13px;
+ }
+}
\ No newline at end of file
diff --git a/test/tests/dictionariesTest.js b/test/tests/dictionariesTest.js
new file mode 100644
index 0000000000..1a34b832c3
--- /dev/null
+++ b/test/tests/dictionariesTest.js
@@ -0,0 +1,193 @@
+"use strict";
+
+var sandbox = sinon.createSandbox();
+
+describe("Dictionaries", function () {
+ var win;
+ var enUKXPIOld, frFRv1XPI, unKNXPI, enUKXPINew, frFRv2XPI;
+
+ async function makeFakeDictionary({ id, locale, version }) {
+ var dir = await getTempDirectory();
+ var extDir = OS.Path.join(dir, 'sub');
+ var dictDir = OS.Path.join(extDir, 'dictionaries');
+ await OS.File.makeDir(dictDir, { from: dir });
+ var manifest = {
+ dictionaries: {
+ [locale]: `dictionaries/${locale}.dic`,
+ },
+ version,
+ applications: {
+ gecko: {
+ id
+ }
+ },
+ name,
+ manifest_version: 2
+ };
+ await Zotero.File.putContentsAsync(
+ OS.Path.join(extDir, 'manifest.json'),
+ JSON.stringify(manifest)
+ );
+ await Zotero.File.putContentsAsync(
+ OS.Path.join(dictDir, locale + '.dic'),
+ "1\n0/nm"
+ );
+ var path = OS.Path.join(dir, id + '.xpi');
+ await Zotero.File.zipDirectory(extDir, path);
+ return path;
+ }
+
+ before(async function () {
+ // Make fake installed dictionaries
+ enUKXPIOld = await makeFakeDictionary({
+ id: '@fake-en-UK-dictionary',
+ locale: 'en-UK',
+ version: 5,
+ name: "Fake English UK Dictionary"
+ });
+ frFRv1XPI = await makeFakeDictionary({
+ id: '@fake-fr-FR-dictionary',
+ locale: 'fr-FR',
+ version: 1,
+ name: "Fake French Dictionary"
+ });
+ unKNXPI = await makeFakeDictionary({
+ id: '@fake-unknown-dictionary',
+ locale: 'xx-UN',
+ version: 5,
+ name: "Fake Unknown Dictionary"
+ });
+ // Make fake updated dictionaries
+ enUKXPINew = await makeFakeDictionary({
+ id: '@another-fake-en-UK-dictionary',
+ locale: 'en-UK',
+ version: 1,
+ name: "Another Fake English UK Dictionary"
+ });
+ frFRv2XPI = await makeFakeDictionary({
+ id: '@fake-fr-FR-dictionary',
+ locale: 'fr-FR',
+ version: 2,
+ name: "Fake French Dictionary"
+ });
+ });
+
+ beforeEach(async function () {
+ for (let id of Zotero.Dictionaries.dictionaries.map(x => x.id)) {
+ await Zotero.Dictionaries.remove(id);
+ }
+
+ sandbox.stub(Zotero.File, 'download').callsFake(async (url, downloadPath) => {
+ if (url.includes('en-UK')) {
+ return OS.File.copy(enUKXPIOld, downloadPath);
+ }
+ if (url.includes('fr-FR')) {
+ return OS.File.copy(frFRv1XPI, downloadPath);
+ }
+ if (url.includes('xx-UN')) {
+ return OS.File.copy(unKNXPI, downloadPath);
+ }
+ throw new Error("Unexpected URL " + url);
+ });
+ await Zotero.Dictionaries.install('@fake-en-UK-dictionary');
+ await Zotero.Dictionaries.install('@fake-fr-FR-dictionary');
+ await Zotero.Dictionaries.install('@fake-xx-UN-dictionary');
+ sandbox.restore();
+
+ // Create metadata response for available dictionaries
+ sandbox.stub(Zotero.Dictionaries, 'fetchDictionariesList')
+ .resolves([
+ {
+ id: '@another-fake-en-UK-dictionary',
+ locale: 'en-UK',
+ name: "English (UK)",
+ version: 1
+ },
+ {
+ id: '@fake-fr-FR-dictionary',
+ locale: 'fr-FR',
+ name: "Français",
+ version: 2
+ }
+ ]);
+
+ sandbox.stub(Zotero.File, 'download').callsFake(async (url, downloadPath) => {
+ if (url.includes('en-UK')) {
+ return OS.File.copy(enUKXPINew, downloadPath);
+ }
+ if (url.includes('fr-FR')) {
+ return OS.File.copy(frFRv2XPI, downloadPath);
+ }
+ throw new Error("Unexpected URL " + url);
+ });
+ });
+
+ afterEach(function () {
+ sandbox.restore();
+ });
+
+ describe("Zotero.Dictionaries", function () {
+ describe("#update()", function () {
+ it("should update outdated dictionary and replace an installed dictionary with a new one with a different id", async function () {
+ var numDictionaries = Zotero.Dictionaries.dictionaries.length;
+ function updated() {
+ return !!(
+ !Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-en-UK-dictionary')
+ && Zotero.Dictionaries.dictionaries.find(x => x.id == '@another-fake-en-UK-dictionary')
+ // Version update happens too
+ && !Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 1)
+ && Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 2)
+ );
+ }
+ assert.isFalse(updated());
+ await Zotero.Dictionaries.update();
+ assert.isTrue(updated());
+ assert.lengthOf(Zotero.Dictionaries.dictionaries, numDictionaries);
+ });
+ });
+ });
+
+ describe("Dictionary Manager", function () {
+ beforeEach(async function () {
+ win = Services.ww.openWindow(
+ null,
+ 'chrome://zotero/content/dictionaryManager.xul',
+ 'dictionary-manager',
+ 'chrome,centerscreen',
+ {}
+ );
+ while (!win.document.querySelectorAll('input[type="checkbox"]').length) {
+ await Zotero.Promise.delay(50);
+ }
+ });
+
+ afterEach(function () {
+ win.close();
+ });
+
+ it("should show unknown dictionary as installed", async function () {
+ var elems = win.document.querySelectorAll('input[type="checkbox"]');
+ var names = [...elems].map(elem => elem.dataset.dictName);
+ assert.sameMembers(names, ['English (UK)', 'Français', 'xx (UN)']);
+ });
+
+ it("should update outdated dictionary and replace an installed dictionary with a new one with a different id", async function () {
+ var numDictionaries = Zotero.Dictionaries.dictionaries.length;
+ function updated() {
+ return !!(
+ !Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-en-UK-dictionary')
+ && Zotero.Dictionaries.dictionaries.find(x => x.id == '@another-fake-en-UK-dictionary')
+ // Version update happens too
+ && !Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 1)
+ && Zotero.Dictionaries.dictionaries.find(x => x.id == '@fake-fr-FR-dictionary' && x.version == 2)
+ );
+ }
+ assert.isFalse(updated());
+ win.document.querySelector('button[dlgtype="accept"]').click();
+ while (!updated()) {
+ await Zotero.Promise.delay(50);
+ }
+ assert.lengthOf(Zotero.Dictionaries.dictionaries, numDictionaries);
+ });
+ });
+});