diff --git a/.eslintrc.json b/.eslintrc.json
index 86da3d48da..bdb05baf7a 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -694,6 +694,25 @@
}
}]
}
+ },
+ {
+ "files": [
+ "ext/data/recommended-settings.json"
+ ],
+ "rules": {
+ "jsonc/sort-keys": ["error", {
+ "pathPattern": ".*",
+ "order": [
+ "modification",
+ "description"
+ ]
+ }, {
+ "pathPattern": ".*",
+ "order": {
+ "type": "asc"
+ }
+ }]
+ }
}
]
}
diff --git a/ext/data/recommended-settings.json b/ext/data/recommended-settings.json
new file mode 100644
index 0000000000..c492a2540a
--- /dev/null
+++ b/ext/data/recommended-settings.json
@@ -0,0 +1,406 @@
+{
+ "da": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "de": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "el": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "en": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "es": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "fi": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "fr": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.textReplacements.groups",
+ "value": [
+ [
+ {
+ "pattern": "l'",
+ "ignoreCase": true,
+ "replacement": ""
+ },
+ {
+ "pattern": "j'",
+ "ignoreCase": true,
+ "replacement": ""
+ },
+ {
+ "pattern": "d'",
+ "ignoreCase": true,
+ "replacement": ""
+ }
+ ]
+ ]
+ },
+ "description": "Separating the l', j', d' from the word."
+ }
+ ],
+ "hu": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "id": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "it": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "mn": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "nl": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "pl": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "pt": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "ro": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "ru": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "sh": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "sq": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "sv": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "tr": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ],
+ "vi": [
+ {
+ "modification": {
+ "action": "set",
+ "path": "scanning.scanResolution",
+ "value": "word"
+ },
+ "description": "Scan text one word at a time (as opposed to one character)."
+ },
+ {
+ "modification": {
+ "action": "set",
+ "path": "translation.searchResolution",
+ "value": "word"
+ },
+ "description": "Lookup whole words in the dictionary."
+ }
+ ]
+}
diff --git a/ext/data/schemas/recommended-settings-schema.json b/ext/data/schemas/recommended-settings-schema.json
new file mode 100644
index 0000000000..2c8054244b
--- /dev/null
+++ b/ext/data/schemas/recommended-settings-schema.json
@@ -0,0 +1,180 @@
+{
+ "$id": "recommendedSetttings",
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "description": "Contains data for recommended default options overrides by language.",
+ "type": "object",
+ "$defs": {
+ "path": {
+ "type": "string",
+ "minLength": 2
+ },
+ "value": {
+ "anyOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "number"
+ },
+ {
+ "type": "boolean"
+ },
+ {
+ "type": "array"
+ },
+ {
+ "type": "object"
+ }
+ ]
+ },
+ "description": {
+ "type": "string",
+ "minLength": 2
+ }
+ },
+ "patternProperties": {
+ "^.{2,}$": {
+ "title": "Language",
+ "type": "array",
+ "items": {
+ "title": "Modification",
+ "type": "object",
+ "oneOf": [
+ {
+ "type": "object",
+ "title": "ModificationSet",
+ "properties": {
+ "modification": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "const": "set"
+ },
+ "path": {
+ "$ref": "#/$defs/path"
+ },
+ "value": {
+ "$ref": "#/$defs/value"
+ }
+ }
+ },
+ "description": {
+ "$ref": "#/$defs/description"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "ModificationDelete",
+ "properties": {
+ "modification": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "const": "delete"
+ },
+ "path": {
+ "$ref": "#/$defs/path"
+ },
+ "value": {
+ "$ref": "#/$defs/value"
+ }
+ }
+ },
+ "description": {
+ "$ref": "#/$defs/description"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "ModificationSwap",
+ "properties": {
+ "modification": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "const": "swap"
+ },
+ "path1": {
+ "$ref": "#/$defs/path"
+ },
+ "path2": {
+ "$ref": "#/$defs/path"
+ }
+ }
+ },
+ "description": {
+ "$ref": "#/$defs/description"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "ModificationSplice",
+ "properties": {
+ "modification": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "const": "splice"
+ },
+ "path": {
+ "$ref": "#/$defs/path"
+ },
+ "start": {
+ "type": "number"
+ },
+ "deleteCount": {
+ "type": "number"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/value"
+ }
+ }
+ }
+ },
+ "description": {
+ "$ref": "#/$defs/description"
+ }
+ }
+ },
+ {
+ "type": "object",
+ "title": "ModificationPush",
+ "properties": {
+ "modification": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "const": "push"
+ },
+ "path": {
+ "$ref": "#/$defs/path"
+ },
+ "items": {
+ "type": "array",
+ "items": {
+ "$ref": "#/$defs/value"
+ }
+ }
+ }
+ },
+ "description": {
+ "$ref": "#/$defs/description"
+ }
+ }
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/ext/js/pages/settings/recommended-settings-controller.js b/ext/js/pages/settings/recommended-settings-controller.js
new file mode 100644
index 0000000000..c9b540b614
--- /dev/null
+++ b/ext/js/pages/settings/recommended-settings-controller.js
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2024 Ajatt-Tools and contributors
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+import {fetchJson} from '../../core/fetch-utilities.js';
+import {log} from '../../core/log.js';
+import {querySelectorNotNull} from '../../dom/query-selector.js';
+
+export class RecommendedSettingsController {
+ /**
+ * @param {import('./settings-controller.js').SettingsController} settingsController
+ */
+ constructor(settingsController) {
+ /** @type {import('./settings-controller.js').SettingsController} */
+ this._settingsController = settingsController;
+ /** @type {HTMLElement} */
+ this._recommendedSettingsModal = querySelectorNotNull(document, '#recommended-settings-modal');
+ /** @type {HTMLInputElement} */
+ this._languageSelect = querySelectorNotNull(document, '#language-select');
+ /** @type {HTMLInputElement} */
+ this._applyButton = querySelectorNotNull(document, '#recommended-settings-apply-button');
+ /** @type {Map} */
+ this._recommendedSettings = new Map();
+ }
+
+ /** */
+ async prepare() {
+ this._languageSelect.addEventListener('change', this._onLanguageSelectChanged.bind(this), false);
+ this._applyButton.addEventListener('click', this._onApplyButtonClicked.bind(this), false);
+ }
+
+ /**
+ * @param {Event} _e
+ */
+ async _onLanguageSelectChanged(_e) {
+ const setLanguage = this._languageSelect.value;
+ if (typeof setLanguage !== 'string') { return; }
+
+ const recommendedSettings = await this._getRecommendedSettings(setLanguage);
+ if (typeof recommendedSettings !== 'undefined') {
+ const settingsList = querySelectorNotNull(document, '#recommended-settings-list');
+ settingsList.innerHTML = '';
+ this._recommendedSettings = new Map();
+
+ for (const [index, setting] of recommendedSettings.entries()) {
+ this._recommendedSettings.set(index.toString(), setting);
+
+ const {description} = setting;
+ const template = this._settingsController.instantiateTemplate('recommended-settings-list-item');
+
+ // Render label
+ this._renderLabel(template, setting);
+
+ // Render description
+ const descriptionElement = querySelectorNotNull(template, '.settings-item-description');
+ if (description !== 'undefined') {
+ descriptionElement.textContent = description;
+ }
+
+ // Render checkbox
+ const checkbox = /** @type {HTMLInputElement} */ (querySelectorNotNull(template, 'input[type="checkbox"]'));
+ checkbox.value = index.toString();
+
+ settingsList.append(template);
+ }
+ this._recommendedSettingsModal.hidden = false;
+ }
+ }
+
+ /**
+ *
+ * @param {string} language
+ * @returns {Promise}
+ */
+ async _getRecommendedSettings(language) {
+ if (typeof this._recommendedSettingsByLanguage === 'undefined') {
+ /** @type {import('settings-controller').RecommendedSettingsByLanguage} */
+ this._recommendedSettingsByLanguage = await fetchJson('/data/recommended-settings.json');
+ }
+
+ return this._recommendedSettingsByLanguage[language];
+ }
+
+ /**
+ * @param {MouseEvent} e
+ */
+ _onApplyButtonClicked(e) {
+ e.preventDefault();
+ /** @type {NodeListOf} */
+ const enabledCheckboxes = querySelectorNotNull(document, '#recommended-settings-list').querySelectorAll('input[type="checkbox"]:checked');
+ if (enabledCheckboxes.length > 0) {
+ const modifications = [];
+ for (const checkbox of enabledCheckboxes) {
+ const index = checkbox.value;
+ const setting = this._recommendedSettings.get(index);
+ if (typeof setting === 'undefined') { continue; }
+ modifications.push(setting.modification);
+ }
+ void this._settingsController.modifyProfileSettings(modifications).then(
+ (results) => {
+ results.map((result) => {
+ if (Object.hasOwn(result, 'error')) {
+ log.error(new Error(`Failed to apply recommended setting: ${JSON.stringify(result)}`));
+ }
+ });
+ },
+ );
+ void this._settingsController.refresh();
+ }
+ this._recommendedSettingsModal.hidden = true;
+ }
+
+ /**
+ * @param {Element} template
+ * @param {import('settings-controller').RecommendedSetting} setting
+ */
+ _renderLabel(template, setting) {
+ const label = querySelectorNotNull(template, '.settings-item-label');
+
+ const {modification} = setting;
+ switch (modification.action) {
+ case 'set': {
+ const {path, value} = modification;
+ const pathCodeElement = document.createElement('code');
+ pathCodeElement.textContent = path;
+ const valueCodeElement = document.createElement('code');
+ valueCodeElement.textContent = JSON.stringify(value, null, 2);
+
+ label.appendChild(document.createTextNode('Setting '));
+ label.appendChild(pathCodeElement);
+ label.appendChild(document.createTextNode(' = '));
+ label.appendChild(valueCodeElement);
+ break;
+ }
+ case 'delete': {
+ const {path} = modification;
+ const pathCodeElement = document.createElement('code');
+ pathCodeElement.textContent = path;
+
+ label.appendChild(document.createTextNode('Deleting '));
+ label.appendChild(pathCodeElement);
+ break;
+ }
+ case 'swap': {
+ const {path1, path2} = modification;
+ const path1CodeElement = document.createElement('code');
+ path1CodeElement.textContent = path1;
+ const path2CodeElement = document.createElement('code');
+ path2CodeElement.textContent = path2;
+
+ label.appendChild(document.createTextNode('Swapping '));
+ label.appendChild(path1CodeElement);
+ label.appendChild(document.createTextNode(' and '));
+ label.appendChild(path2CodeElement);
+ break;
+ }
+ case 'splice': {
+ const {path, start, deleteCount, items} = modification;
+ const pathCodeElement = document.createElement('code');
+ pathCodeElement.textContent = path;
+
+ label.appendChild(document.createTextNode('Splicing '));
+ label.appendChild(pathCodeElement);
+ label.appendChild(document.createTextNode(` at ${start} deleting ${deleteCount} items and inserting ${items.length} items`));
+ break;
+ }
+ case 'push': {
+ const {path, items} = modification;
+ const pathCodeElement = document.createElement('code');
+ pathCodeElement.textContent = path;
+
+ label.appendChild(document.createTextNode(`Pushing ${items.length} items to `));
+ label.appendChild(pathCodeElement);
+ break;
+ }
+ default: {
+ log.error(new Error(`Unknown modification: ${modification}`));
+ }
+ }
+ }
+}
diff --git a/ext/js/pages/settings/settings-main.js b/ext/js/pages/settings/settings-main.js
index 38b1fe0b4e..66537c9b04 100644
--- a/ext/js/pages/settings/settings-main.js
+++ b/ext/js/pages/settings/settings-main.js
@@ -40,6 +40,7 @@ import {PersistentStorageController} from './persistent-storage-controller.js';
import {PopupPreviewController} from './popup-preview-controller.js';
import {PopupWindowController} from './popup-window-controller.js';
import {ProfileController} from './profile-controller.js';
+import {RecommendedSettingsController} from './recommended-settings-controller.js';
import {ScanInputsController} from './scan-inputs-controller.js';
import {ScanInputsSimpleController} from './scan-inputs-simple-controller.js';
import {SecondarySearchDictionaryController} from './secondary-search-dictionary-controller.js';
@@ -174,6 +175,8 @@ await Application.main(true, async (application) => {
const sortFrequencyDictionaryController = new SortFrequencyDictionaryController(settingsController);
preparePromises.push(sortFrequencyDictionaryController.prepare());
+ const recommendedSettingsController = new RecommendedSettingsController(settingsController);
+ preparePromises.push(recommendedSettingsController.prepare());
await Promise.all(preparePromises);
diff --git a/ext/js/pages/welcome-main.js b/ext/js/pages/welcome-main.js
index 65a6c4aecb..6dc3f2a551 100644
--- a/ext/js/pages/welcome-main.js
+++ b/ext/js/pages/welcome-main.js
@@ -26,6 +26,7 @@ import {GenericSettingController} from './settings/generic-setting-controller.js
import {LanguagesController} from './settings/languages-controller.js';
import {ModalController} from './settings/modal-controller.js';
import {RecommendedPermissionsController} from './settings/recommended-permissions-controller.js';
+import {RecommendedSettingsController} from './settings/recommended-settings-controller.js';
import {ScanInputsSimpleController} from './settings/scan-inputs-simple-controller.js';
import {SettingsController} from './settings/settings-controller.js';
import {SettingsDisplayController} from './settings/settings-display-controller.js';
@@ -105,6 +106,9 @@ await Application.main(true, async (application) => {
const languagesController = new LanguagesController(settingsController);
preparePromises.push(languagesController.prepare());
+ const recommendedSettingsController = new RecommendedSettingsController(settingsController);
+ preparePromises.push(recommendedSettingsController.prepare());
+
await Promise.all(preparePromises);
document.documentElement.dataset.loaded = 'true';
diff --git a/ext/templates-modals.html b/ext/templates-modals.html
index da36ed83d6..905fd01393 100644
--- a/ext/templates-modals.html
+++ b/ext/templates-modals.html
@@ -346,6 +346,30 @@ Pronunciation Dictionaries
+
+
+
+
+
+
We recommend the following settings for your selected language
+
+
+
+
diff --git a/ext/templates-settings.html b/ext/templates-settings.html
index c12ba24864..9dc1c91be4 100644
--- a/ext/templates-settings.html
+++ b/ext/templates-settings.html
@@ -477,4 +477,21 @@
+
+
+
+
+