aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorLibravatar André Oliveira <37463445+SpecialAro@users.noreply.github.com>2022-08-17 22:54:41 +0100
committerLibravatar GitHub <noreply@github.com>2022-08-17 22:54:41 +0100
commitfb0cc81d1db0d88c90bb112a0caec66095fcc0f0 (patch)
treeaaa5d0f92f55ccf3984af2cbf2ebbcb1da5fd7c6 /src
parent6.0.1-nightly.16 [skip ci] (diff)
downloadferdium-app-fb0cc81d1db0d88c90bb112a0caec66095fcc0f0.tar.gz
ferdium-app-fb0cc81d1db0d88c90bb112a0caec66095fcc0f0.tar.zst
ferdium-app-fb0cc81d1db0d88c90bb112a0caec66095fcc0f0.zip
Feature: Add Ferdium Translator (#548)
Add feature to translate text natively using https://github.com/shikar/NODE_GOOGLE_TRANSLATE package and a LibreTranslate self-hosted option (already running on our server on https://translator.ferdium.org).
Diffstat (limited to 'src')
-rw-r--r--src/@types/stores.types.ts3
-rw-r--r--src/components/settings/settings/EditSettingsForm.jsx12
-rw-r--r--src/config.ts139
-rw-r--r--src/containers/settings/EditSettingsScreen.tsx54
-rw-r--r--src/helpers/translation-helpers.ts47
-rw-r--r--src/i18n/locales/en-US.json3
-rw-r--r--src/index.ts13
-rw-r--r--src/models/IContextMenuParams.ts7
-rw-r--r--src/stores/ServicesStore.ts21
-rw-r--r--src/webview/contextMenu.ts6
-rw-r--r--src/webview/contextMenuBuilder.ts255
-rw-r--r--src/webview/recipe.js6
12 files changed, 540 insertions, 26 deletions
diff --git a/src/@types/stores.types.ts b/src/@types/stores.types.ts
index eec18c11e..13870a43a 100644
--- a/src/@types/stores.types.ts
+++ b/src/@types/stores.types.ts
@@ -90,6 +90,7 @@ interface AppStore extends TypedStore {
90 darkMode: boolean; 90 darkMode: boolean;
91 dictionaries: []; 91 dictionaries: [];
92 enableSpellchecking: boolean; 92 enableSpellchecking: boolean;
93 enableTranslator: boolean;
93 fetchDataInterval: 4; 94 fetchDataInterval: 4;
94 get(key: string): any; 95 get(key: string): any;
95 getAppCacheSizeRequest: () => void; 96 getAppCacheSizeRequest: () => void;
@@ -106,6 +107,8 @@ interface AppStore extends TypedStore {
106 reloadAfterResume: boolean; 107 reloadAfterResume: boolean;
107 reloadAfterResumeTime: number; 108 reloadAfterResumeTime: number;
108 searchEngine: string; 109 searchEngine: string;
110 translatorEngine: string;
111 translatorLanguage: string;
109 spellcheckerLanguage: string; 112 spellcheckerLanguage: string;
110 splitMode: boolean; 113 splitMode: boolean;
111 splitColumns: number; 114 splitColumns: number;
diff --git a/src/components/settings/settings/EditSettingsForm.jsx b/src/components/settings/settings/EditSettingsForm.jsx
index 0e5be38ed..e6cba922b 100644
--- a/src/components/settings/settings/EditSettingsForm.jsx
+++ b/src/components/settings/settings/EditSettingsForm.jsx
@@ -870,12 +870,24 @@ class EditSettingsForm extends Component {
870 {intl.formatMessage(messages.spellCheckerLanguageInfo)} 870 {intl.formatMessage(messages.spellCheckerLanguageInfo)}
871 </p> 871 </p>
872 )} 872 )}
873
873 <p className="settings__help"> 874 <p className="settings__help">
874 {intl.formatMessage(messages.appRestartRequired)} 875 {intl.formatMessage(messages.appRestartRequired)}
875 </p> 876 </p>
876 877
877 <Hr /> 878 <Hr />
878 879
880 <Toggle field={form.$('enableTranslator')} />
881
882 {form.$('enableTranslator').value && (
883 <Select field={form.$('translatorEngine')} />
884 )}
885 {form.$('enableTranslator').value && (
886 <Select field={form.$('translatorLanguage')} />
887 )}
888
889 <Hr />
890
879 <a 891 <a
880 href={FERDIUM_TRANSLATION} 892 href={FERDIUM_TRANSLATION}
881 target="_blank" 893 target="_blank"
diff --git a/src/config.ts b/src/config.ts
index 150b7101d..c8cb09d84 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -24,6 +24,8 @@ export const LIVE_WS_API = 'wss://api.franzinfra.com';
24export const LOCAL_API_WEBSITE = 'http://localhost:3333'; 24export const LOCAL_API_WEBSITE = 'http://localhost:3333';
25export const DEV_API_FRANZ_WEBSITE = 'https://meetfranz.com'; 25export const DEV_API_FRANZ_WEBSITE = 'https://meetfranz.com';
26export const LIVE_API_FERDIUM_WEBSITE = 'https://ferdium.org'; 26export const LIVE_API_FERDIUM_WEBSITE = 'https://ferdium.org';
27export const LIVE_API_FERDIUM_LIBRETRANSLATE =
28 'https://translator.ferdium.org/translate';
27 29
28export const STATS_API = 'https://stats.franzinfra.com'; 30export const STATS_API = 'https://stats.franzinfra.com';
29 31
@@ -84,6 +86,140 @@ export const SEARCH_ENGINE_NAMES = {
84 [SEARCH_ENGINE_DDG]: 'DuckDuckGo', 86 [SEARCH_ENGINE_DDG]: 'DuckDuckGo',
85}; 87};
86 88
89export const TRANSLATOR_ENGINE_GOOGLE = 'Google';
90export const TRANSLATOR_ENGINE_LIBRETRANSLATE = 'LibreTranslate';
91export const TRANSLATOR_ENGINE_NAMES = {
92 [TRANSLATOR_ENGINE_LIBRETRANSLATE]:
93 'Ferdium Translator (Powered by LibreTranslate)',
94 [TRANSLATOR_ENGINE_GOOGLE]: 'Google',
95};
96
97export const LIBRETRANSLATE_TRANSLATOR_LANGUAGES = {
98 ar: 'Arabic',
99 zh: 'Chinese',
100 en: 'English',
101 fr: 'French',
102 de: 'German',
103 hi: 'Hindi',
104 id: 'Indonesian',
105 ga: 'Irish',
106 it: 'Italian',
107 ja: 'Japanese',
108 ko: 'Korean',
109 pl: 'Polish',
110 pt: 'Portuguese',
111 ru: 'Russian',
112 es: 'Spanish',
113 tr: 'Turkish',
114 vi: 'Vietnamese',
115};
116
117export const GOOGLE_TRANSLATOR_LANGUAGES = {
118 af: 'Afrikaans',
119 sq: 'Albanian',
120 ar: 'Arabic',
121 hy: 'Armenian',
122 az: 'Azerbaijani',
123 eu: 'Basque',
124 be: 'Belarusian',
125 bn: 'Bengali',
126 bs: 'Bosnian',
127 bg: 'Bulgarian',
128 ca: 'Catalan',
129 ceb: 'Cebuano',
130 ny: 'Chichewa',
131 'zh-cn': 'Chinese Simplified',
132 'zh-tw': 'Chinese Traditional',
133 co: 'Corsican',
134 hr: 'Croatian',
135 cs: 'Czech',
136 da: 'Danish',
137 nl: 'Dutch',
138 en: 'English',
139 eo: 'Esperanto',
140 et: 'Estonian',
141 tl: 'Filipino',
142 fi: 'Finnish',
143 fr: 'French',
144 fy: 'Frisian',
145 gl: 'Galician',
146 ka: 'Georgian',
147 de: 'German',
148 el: 'Greek',
149 gu: 'Gujarati',
150 ht: 'Haitian Creole',
151 ha: 'Hausa',
152 haw: 'Hawaiian',
153 iw: 'Hebrew',
154 hi: 'Hindi',
155 hmn: 'Hmong',
156 hu: 'Hungarian',
157 is: 'Icelandic',
158 ig: 'Igbo',
159 id: 'Indonesian',
160 ga: 'Irish',
161 it: 'Italian',
162 ja: 'Japanese',
163 jw: 'Javanese',
164 kn: 'Kannada',
165 kk: 'Kazakh',
166 km: 'Khmer',
167 ko: 'Korean',
168 ku: 'Kurdish (Kurmanji)',
169 ky: 'Kyrgyz',
170 lo: 'Lao',
171 la: 'Latin',
172 lv: 'Latvian',
173 lt: 'Lithuanian',
174 lb: 'Luxembourgish',
175 mk: 'Macedonian',
176 mg: 'Malagasy',
177 ms: 'Malay',
178 ml: 'Malayalam',
179 mt: 'Maltese',
180 mi: 'Maori',
181 mr: 'Marathi',
182 mn: 'Mongolian',
183 my: 'Myanmar (Burmese)',
184 ne: 'Nepali',
185 no: 'Norwegian',
186 ps: 'Pashto',
187 fa: 'Persian',
188 pl: 'Polish',
189 pt: 'Portuguese',
190 ma: 'Punjabi',
191 ro: 'Romanian',
192 ru: 'Russian',
193 sm: 'Samoan',
194 gd: 'Scots Gaelic',
195 sr: 'Serbian',
196 st: 'Sesotho',
197 sn: 'Shona',
198 sd: 'Sindhi',
199 si: 'Sinhala',
200 sk: 'Slovak',
201 sl: 'Slovenian',
202 so: 'Somali',
203 es: 'Spanish',
204 su: 'Sudanese',
205 sw: 'Swahili',
206 sv: 'Swedish',
207 tg: 'Tajik',
208 ta: 'Tamil',
209 te: 'Telugu',
210 th: 'Thai',
211 tr: 'Turkish',
212 uk: 'Ukrainian',
213 ur: 'Urdu',
214 uz: 'Uzbek',
215 vi: 'Vietnamese',
216 cy: 'Welsh',
217 xh: 'Xhosa',
218 yi: 'Yiddish',
219 yo: 'Yoruba',
220 zu: 'Zulu',
221};
222
87export const SEARCH_ENGINE_URLS = { 223export const SEARCH_ENGINE_URLS = {
88 [SEARCH_ENGINE_STARTPAGE]: ({ searchTerm }) => 224 [SEARCH_ENGINE_STARTPAGE]: ({ searchTerm }) =>
89 `https://www.startpage.com/sp/search?query=${searchTerm}`, 225 `https://www.startpage.com/sp/search?query=${searchTerm}`,
@@ -222,6 +358,7 @@ export const DEFAULT_APP_SETTINGS = {
222 showMessageBadgeWhenMuted: true, 358 showMessageBadgeWhenMuted: true,
223 showDragArea: false, 359 showDragArea: false,
224 enableSpellchecking: true, 360 enableSpellchecking: true,
361 enableTranslator: false,
225 spellcheckerLanguage: 'en-us', 362 spellcheckerLanguage: 'en-us',
226 darkMode: false, 363 darkMode: false,
227 navigationBarManualActive: false, 364 navigationBarManualActive: false,
@@ -261,6 +398,8 @@ export const DEFAULT_APP_SETTINGS = {
261 iconSize: iconSizeBias, 398 iconSize: iconSizeBias,
262 navigationBarBehaviour: 'custom', 399 navigationBarBehaviour: 'custom',
263 searchEngine: SEARCH_ENGINE_STARTPAGE, 400 searchEngine: SEARCH_ENGINE_STARTPAGE,
401 translatorLanguage: 'en',
402 translatorEngine: TRANSLATOR_ENGINE_LIBRETRANSLATE,
264 useHorizontalStyle: false, 403 useHorizontalStyle: false,
265 hideCollapseButton: false, 404 hideCollapseButton: false,
266 isMenuCollapsed: false, 405 isMenuCollapsed: false,
diff --git a/src/containers/settings/EditSettingsScreen.tsx b/src/containers/settings/EditSettingsScreen.tsx
index fbbed629a..a4d7ba0eb 100644
--- a/src/containers/settings/EditSettingsScreen.tsx
+++ b/src/containers/settings/EditSettingsScreen.tsx
@@ -15,6 +15,10 @@ import {
15 ICON_SIZES, 15 ICON_SIZES,
16 NAVIGATION_BAR_BEHAVIOURS, 16 NAVIGATION_BAR_BEHAVIOURS,
17 SEARCH_ENGINE_NAMES, 17 SEARCH_ENGINE_NAMES,
18 TRANSLATOR_ENGINE_NAMES,
19 GOOGLE_TRANSLATOR_LANGUAGES,
20 TRANSLATOR_ENGINE_GOOGLE,
21 LIBRETRANSLATE_TRANSLATOR_LANGUAGES,
18 TODO_APPS, 22 TODO_APPS,
19 DEFAULT_SETTING_KEEP_ALL_WORKSPACES_LOADED, 23 DEFAULT_SETTING_KEEP_ALL_WORKSPACES_LOADED,
20 DEFAULT_IS_FEATURE_ENABLED_BY_USER, 24 DEFAULT_IS_FEATURE_ENABLED_BY_USER,
@@ -103,6 +107,14 @@ const messages = defineMessages({
103 id: 'settings.app.form.searchEngine', 107 id: 'settings.app.form.searchEngine',
104 defaultMessage: 'Search engine', 108 defaultMessage: 'Search engine',
105 }, 109 },
110 translatorEngine: {
111 id: 'settings.app.form.translatorEngine',
112 defaultMessage: 'Translator Engine',
113 },
114 translatorLanguage: {
115 id: 'settings.app.form.translatorLanguage',
116 defaultMessage: 'Default Translator language',
117 },
106 hibernateOnStartup: { 118 hibernateOnStartup: {
107 id: 'settings.app.form.hibernateOnStartup', 119 id: 'settings.app.form.hibernateOnStartup',
108 defaultMessage: 'Keep services in hibernation on startup', 120 defaultMessage: 'Keep services in hibernation on startup',
@@ -267,6 +279,10 @@ const messages = defineMessages({
267 id: 'settings.app.form.enableSpellchecking', 279 id: 'settings.app.form.enableSpellchecking',
268 defaultMessage: 'Enable spell checking', 280 defaultMessage: 'Enable spell checking',
269 }, 281 },
282 enableTranslator: {
283 id: 'settings.app.form.enableTranslator',
284 defaultMessage: 'Enable Translator',
285 },
270 enableGPUAcceleration: { 286 enableGPUAcceleration: {
271 id: 'settings.app.form.enableGPUAcceleration', 287 id: 'settings.app.form.enableGPUAcceleration',
272 defaultMessage: 'Enable GPU Acceleration', 288 defaultMessage: 'Enable GPU Acceleration',
@@ -342,6 +358,8 @@ class EditSettingsScreen extends Component<EditSettingsScreenProps> {
342 notifyTaskBarOnMessage: Boolean(settingsData.notifyTaskBarOnMessage), 358 notifyTaskBarOnMessage: Boolean(settingsData.notifyTaskBarOnMessage),
343 navigationBarBehaviour: settingsData.navigationBarBehaviour, 359 navigationBarBehaviour: settingsData.navigationBarBehaviour,
344 searchEngine: settingsData.searchEngine, 360 searchEngine: settingsData.searchEngine,
361 translatorEngine: settingsData.translatorEngine,
362 translatorLanguage: settingsData.translatorLanguage,
345 hibernateOnStartup: Boolean(settingsData.hibernateOnStartup), 363 hibernateOnStartup: Boolean(settingsData.hibernateOnStartup),
346 hibernationStrategy: Number(settingsData.hibernationStrategy), 364 hibernationStrategy: Number(settingsData.hibernationStrategy),
347 wakeUpStrategy: Number(settingsData.wakeUpStrategy), 365 wakeUpStrategy: Number(settingsData.wakeUpStrategy),
@@ -394,6 +412,7 @@ class EditSettingsScreen extends Component<EditSettingsScreenProps> {
394 ), 412 ),
395 showDragArea: Boolean(settingsData.showDragArea), 413 showDragArea: Boolean(settingsData.showDragArea),
396 enableSpellchecking: Boolean(settingsData.enableSpellchecking), 414 enableSpellchecking: Boolean(settingsData.enableSpellchecking),
415 enableTranslator: Boolean(settingsData.enableTranslator),
397 spellcheckerLanguage: settingsData.spellcheckerLanguage, 416 spellcheckerLanguage: settingsData.spellcheckerLanguage,
398 userAgentPref: settingsData.userAgentPref, 417 userAgentPref: settingsData.userAgentPref,
399 beta: Boolean(settingsData.beta), // we need this info in the main process as well 418 beta: Boolean(settingsData.beta), // we need this info in the main process as well
@@ -451,6 +470,16 @@ class EditSettingsScreen extends Component<EditSettingsScreenProps> {
451 sort: false, 470 sort: false,
452 }); 471 });
453 472
473 const translatorEngines = getSelectOptions({
474 locales: TRANSLATOR_ENGINE_NAMES,
475 sort: false,
476 });
477
478 const translatorLanguages = getSelectOptions({
479 locales: LIBRETRANSLATE_TRANSLATOR_LANGUAGES,
480 sort: false,
481 });
482
454 const hibernationStrategies = getSelectOptions({ 483 const hibernationStrategies = getSelectOptions({
455 locales: HIBERNATION_STRATEGIES, 484 locales: HIBERNATION_STRATEGIES,
456 sort: false, 485 sort: false,
@@ -574,6 +603,18 @@ class EditSettingsScreen extends Component<EditSettingsScreenProps> {
574 default: DEFAULT_APP_SETTINGS.searchEngine, 603 default: DEFAULT_APP_SETTINGS.searchEngine,
575 options: searchEngines, 604 options: searchEngines,
576 }, 605 },
606 translatorEngine: {
607 label: intl.formatMessage(messages.translatorEngine),
608 value: settings.all.app.translatorEngine,
609 default: DEFAULT_APP_SETTINGS.translatorEngine,
610 options: translatorEngines,
611 },
612 translatorLanguage: {
613 label: intl.formatMessage(messages.translatorLanguage),
614 value: settings.all.app.translatorLanguage,
615 default: DEFAULT_APP_SETTINGS.translatorLanguage,
616 options: translatorLanguages,
617 },
577 hibernateOnStartup: { 618 hibernateOnStartup: {
578 label: intl.formatMessage(messages.hibernateOnStartup), 619 label: intl.formatMessage(messages.hibernateOnStartup),
579 value: settings.all.app.hibernateOnStartup, 620 value: settings.all.app.hibernateOnStartup,
@@ -677,6 +718,11 @@ class EditSettingsScreen extends Component<EditSettingsScreenProps> {
677 value: settings.all.app.enableSpellchecking, 718 value: settings.all.app.enableSpellchecking,
678 default: DEFAULT_APP_SETTINGS.enableSpellchecking, 719 default: DEFAULT_APP_SETTINGS.enableSpellchecking,
679 }, 720 },
721 enableTranslator: {
722 label: intl.formatMessage(messages.enableTranslator),
723 value: settings.all.app.enableTranslator,
724 default: DEFAULT_APP_SETTINGS.enableTranslator,
725 },
680 spellcheckerLanguage: { 726 spellcheckerLanguage: {
681 label: intl.formatMessage(globalMessages.spellcheckerLanguage), 727 label: intl.formatMessage(globalMessages.spellcheckerLanguage),
682 value: settings.all.app.spellcheckerLanguage, 728 value: settings.all.app.spellcheckerLanguage,
@@ -828,6 +874,14 @@ class EditSettingsScreen extends Component<EditSettingsScreenProps> {
828 }, 874 },
829 }; 875 };
830 876
877 if (settings.app.translatorEngine === TRANSLATOR_ENGINE_GOOGLE) {
878 const translatorGoogleLanguages = getSelectOptions({
879 locales: GOOGLE_TRANSLATOR_LANGUAGES,
880 sort: false,
881 });
882 config.fields.translatorLanguage.options = translatorGoogleLanguages;
883 }
884
831 if (workspaces.isFeatureActive) { 885 if (workspaces.isFeatureActive) {
832 config.fields.keepAllWorkspacesLoaded = { 886 config.fields.keepAllWorkspacesLoaded = {
833 label: intl.formatMessage(messages.keepAllWorkspacesLoaded), 887 label: intl.formatMessage(messages.keepAllWorkspacesLoaded),
diff --git a/src/helpers/translation-helpers.ts b/src/helpers/translation-helpers.ts
new file mode 100644
index 000000000..215b2a49c
--- /dev/null
+++ b/src/helpers/translation-helpers.ts
@@ -0,0 +1,47 @@
1import fetch from 'node-fetch';
2import translateGoogle from 'translate-google';
3import { LIVE_API_FERDIUM_LIBRETRANSLATE } from '../config';
4
5export async function translateTo(
6 text: string,
7 translateToLanguage: string,
8 translatorEngine: string,
9): Promise<{ text: string; error: boolean }> {
10 const errorText =
11 // TODO: Need to support i18n
12 'FERDIUM ERROR: An error occured. Please select less text to translate or try again later.';
13
14 if (translatorEngine === 'Google') {
15 try {
16 const res = await translateGoogle(text, {
17 to: translateToLanguage,
18 });
19
20 return { text: res, error: false };
21 } catch {
22 return { text: errorText, error: true };
23 }
24 } else if (translatorEngine === 'LibreTranslate') {
25 try {
26 const res = await fetch(LIVE_API_FERDIUM_LIBRETRANSLATE, {
27 method: 'POST',
28 body: JSON.stringify({
29 q: text,
30 source: 'auto',
31 target: translateToLanguage,
32 }),
33 headers: {
34 'Content-Type': 'application/json',
35 },
36 });
37
38 const response = await res.json();
39
40 return { text: response.translatedText, error: false };
41 } catch {
42 return { text: errorText, error: true };
43 }
44 }
45
46 return { text: errorText, error: true };
47}
diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json
index a98bc2f77..4dc8bad82 100644
--- a/src/i18n/locales/en-US.json
+++ b/src/i18n/locales/en-US.json
@@ -219,6 +219,7 @@
219 "settings.app.form.enableSpellchecking": "Enable spell checking", 219 "settings.app.form.enableSpellchecking": "Enable spell checking",
220 "settings.app.form.enableSystemTray": "Always show Ferdium in System Tray", 220 "settings.app.form.enableSystemTray": "Always show Ferdium in System Tray",
221 "settings.app.form.enableTodos": "Enable Ferdium Todos", 221 "settings.app.form.enableTodos": "Enable Ferdium Todos",
222 "settings.app.form.enableTranslator": "Enable Translator",
222 "settings.app.form.grayscaleServicesDim": "Grayscale dim level", 223 "settings.app.form.grayscaleServicesDim": "Grayscale dim level",
223 "settings.app.form.hibernateOnStartup": "Keep services in hibernation on startup", 224 "settings.app.form.hibernateOnStartup": "Keep services in hibernation on startup",
224 "settings.app.form.hibernationStrategy": "Hibernation strategy", 225 "settings.app.form.hibernationStrategy": "Hibernation strategy",
@@ -256,6 +257,8 @@
256 "settings.app.form.splitColumns": "Number of columns", 257 "settings.app.form.splitColumns": "Number of columns",
257 "settings.app.form.splitMode": "Enable Split View Mode", 258 "settings.app.form.splitMode": "Enable Split View Mode",
258 "settings.app.form.startMinimized": "Start minimized", 259 "settings.app.form.startMinimized": "Start minimized",
260 "settings.app.form.translatorEngine": "Translator Engine",
261 "settings.app.form.translatorLanguage": "Default Translator language",
259 "settings.app.form.universalDarkMode": "Enable universal Dark Mode", 262 "settings.app.form.universalDarkMode": "Enable universal Dark Mode",
260 "settings.app.form.useGrayscaleServices": "Use grayscale services", 263 "settings.app.form.useGrayscaleServices": "Use grayscale services",
261 "settings.app.form.useHorizontalStyle": "Use horizontal style", 264 "settings.app.form.useHorizontalStyle": "Use horizontal style",
diff --git a/src/index.ts b/src/index.ts
index 05e222a25..7c80ca955 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -45,6 +45,7 @@ import './electron/exception';
45import { asarPath } from './helpers/asar-helpers'; 45import { asarPath } from './helpers/asar-helpers';
46import { openExternalUrl } from './helpers/url-helpers'; 46import { openExternalUrl } from './helpers/url-helpers';
47import userAgent from './helpers/userAgent-helpers'; 47import userAgent from './helpers/userAgent-helpers';
48import { translateTo } from './helpers/translation-helpers';
48 49
49const debug = require('./preload-safe-debug')('Ferdium:App'); 50const debug = require('./preload-safe-debug')('Ferdium:App');
50 51
@@ -500,6 +501,18 @@ app.on('login', (event, _webContents, _request, authInfo, callback) => {
500 } 501 }
501}); 502});
502 503
504ipcMain.handle(
505 'translate',
506 async (_e, { text, translateToLanguage, translatorEngine }) => {
507 const response = await translateTo(
508 text,
509 translateToLanguage,
510 translatorEngine,
511 );
512 return response;
513 },
514);
515
503// TODO: evaluate if we need to store the authCallback for every service 516// TODO: evaluate if we need to store the authCallback for every service
504ipcMain.on('feature-basic-auth-credentials', (_e, { user, password }) => { 517ipcMain.on('feature-basic-auth-credentials', (_e, { user, password }) => {
505 debug('Received basic auth credentials', user, '********'); 518 debug('Received basic auth credentials', user, '********');
diff --git a/src/models/IContextMenuParams.ts b/src/models/IContextMenuParams.ts
new file mode 100644
index 000000000..c661b4595
--- /dev/null
+++ b/src/models/IContextMenuParams.ts
@@ -0,0 +1,7 @@
1export default interface IContextMenuParams extends Electron.ContextMenuParams {
2 enableTranslator: boolean;
3 clipboardNotifications: boolean;
4 searchEngine: string;
5 translatorEngine: string;
6 translatorLanguage: string;
7}
diff --git a/src/stores/ServicesStore.ts b/src/stores/ServicesStore.ts
index 83ec7d18e..00cf33b17 100644
--- a/src/stores/ServicesStore.ts
+++ b/src/stores/ServicesStore.ts
@@ -158,6 +158,13 @@ export default class ServicesStore extends TypedStore {
158 ); 158 );
159 159
160 reaction( 160 reaction(
161 () => this.stores.settings.app.enableTranslator,
162 () => {
163 this._shareSettingsWithServiceProcess();
164 },
165 );
166
167 reaction(
161 () => this.stores.settings.app.spellcheckerLanguage, 168 () => this.stores.settings.app.spellcheckerLanguage,
162 () => { 169 () => {
163 this._shareSettingsWithServiceProcess(); 170 this._shareSettingsWithServiceProcess();
@@ -207,6 +214,20 @@ export default class ServicesStore extends TypedStore {
207 ); 214 );
208 215
209 reaction( 216 reaction(
217 () => this.stores.settings.app.translatorEngine,
218 () => {
219 this._shareSettingsWithServiceProcess();
220 },
221 );
222
223 reaction(
224 () => this.stores.settings.app.translatorLanguage,
225 () => {
226 this._shareSettingsWithServiceProcess();
227 },
228 );
229
230 reaction(
210 () => this.stores.settings.app.clipboardNotifications, 231 () => this.stores.settings.app.clipboardNotifications,
211 () => { 232 () => {
212 this._shareSettingsWithServiceProcess(); 233 this._shareSettingsWithServiceProcess();
diff --git a/src/webview/contextMenu.ts b/src/webview/contextMenu.ts
index 72f927ef4..61771854f 100644
--- a/src/webview/contextMenu.ts
+++ b/src/webview/contextMenu.ts
@@ -9,6 +9,9 @@ export default async function setupContextMenu(
9 getSpellcheckerLanguage: () => void, 9 getSpellcheckerLanguage: () => void,
10 getSearchEngine: () => void, 10 getSearchEngine: () => void,
11 getClipboardNotifications: () => void, 11 getClipboardNotifications: () => void,
12 getEnableTranslator: () => void,
13 getTranslatorEngine: () => void,
14 getTranslatorLanguage: () => void,
12) { 15) {
13 const contextMenuBuilder = new ContextMenuBuilder(webContents); 16 const contextMenuBuilder = new ContextMenuBuilder(webContents);
14 17
@@ -19,6 +22,9 @@ export default async function setupContextMenu(
19 ...props, 22 ...props,
20 searchEngine: getSearchEngine(), 23 searchEngine: getSearchEngine(),
21 clipboardNotifications: getClipboardNotifications(), 24 clipboardNotifications: getClipboardNotifications(),
25 enableTranslator: getEnableTranslator(),
26 translatorEngine: getTranslatorEngine(),
27 translatorLanguage: getTranslatorLanguage(),
22 }, 28 },
23 // @ts-expect-error Expected 1 arguments, but got 4. 29 // @ts-expect-error Expected 1 arguments, but got 4.
24 isSpellcheckEnabled(), 30 isSpellcheckEnabled(),
diff --git a/src/webview/contextMenuBuilder.ts b/src/webview/contextMenuBuilder.ts
index 7bd86556e..2e64977c1 100644
--- a/src/webview/contextMenuBuilder.ts
+++ b/src/webview/contextMenuBuilder.ts
@@ -11,8 +11,16 @@ import { clipboard, ipcRenderer, nativeImage, WebContents } from 'electron';
11import { Menu, MenuItem } from '@electron/remote'; 11import { Menu, MenuItem } from '@electron/remote';
12import { cmdOrCtrlShortcutKey, isMac } from '../environment'; 12import { cmdOrCtrlShortcutKey, isMac } from '../environment';
13 13
14import { SEARCH_ENGINE_NAMES, SEARCH_ENGINE_URLS } from '../config'; 14import {
15 SEARCH_ENGINE_NAMES,
16 SEARCH_ENGINE_URLS,
17 GOOGLE_TRANSLATOR_LANGUAGES,
18 TRANSLATOR_ENGINE_GOOGLE,
19 TRANSLATOR_ENGINE_LIBRETRANSLATE,
20 LIBRETRANSLATE_TRANSLATOR_LANGUAGES,
21} from '../config';
15import { openExternalUrl } from '../helpers/url-helpers'; 22import { openExternalUrl } from '../helpers/url-helpers';
23import IContextMenuParams from '../models/IContextMenuParams';
16 24
17function matchesWord(string: string) { 25function matchesWord(string: string) {
18 const regex = 26 const regex =
@@ -21,6 +29,83 @@ function matchesWord(string: string) {
21 return string.match(regex); 29 return string.match(regex);
22} 30}
23 31
32function childOf(node, ancestor) {
33 let child = node;
34 while (child !== null) {
35 if (child === ancestor) return true;
36 child = child.parentNode;
37 }
38 return false;
39}
40
41function translatePopup(res, isError: boolean = false) {
42 const elementExists = document.querySelector('#container-ferdium-translator');
43 if (elementExists) {
44 elementExists.remove();
45 }
46
47 const style = document.createElement('style');
48 style.innerHTML = `
49 .container-ferdium-translator {
50 position: fixed;
51 opacity: 0.9;
52 z-index: 999999;
53 ${
54 isError
55 ? `background: rgb(255 37 37);`
56 : `background: rgb(131 131 131);`
57 }
58 border-radius: 8px;
59 top: 5%;
60 left: 50%;
61 transform: translate(-50%, -5%);
62 display: flex;
63 flex-direction: row;
64 -webkit-box-shadow: 0px 10px 13px -7px #000000, 5px 5px 13px 9px rgba(0,0,0,0);
65 overflow: auto;
66 max-height: 95%;
67 max-width: 90%;
68 width: max-content;
69 height: max-content;
70 -webkit-animation: scale-up-center 0.4s cubic-bezier(0.390, 0.575, 0.565, 1.000) both;
71 }
72 .container-ferdium-translator > p {
73 color: white;
74 margin: 10px;
75 text-align: justify;
76 }
77
78 @-webkit-keyframes scale-up-center {
79 0% {
80 -webkit-transform: translate(-50%, -50%) scale(0.5);
81 }
82 100% {
83 -webkit-transform: translate(-50%, -50%) scale(1);
84 }
85 }
86 `;
87 document.head.append(style);
88
89 const para = document.createElement('p');
90
91 const node = document.createTextNode(res);
92 para.append(node);
93
94 const div = document.createElement('div');
95 div.setAttribute('id', 'container-ferdium-translator');
96 div.setAttribute('class', 'container-ferdium-translator');
97
98 div.append(para);
99
100 document.body.insertBefore(div, document.body.firstChild);
101
102 document.addEventListener('click', e => {
103 if (div !== e.target && !childOf(e.target, div)) {
104 div?.remove();
105 }
106 });
107}
108
24interface ContextMenuStringTable { 109interface ContextMenuStringTable {
25 lookUpDefinition: ({ word }: { word: string }) => string; 110 lookUpDefinition: ({ word }: { word: string }) => string;
26 cut: () => string; 111 cut: () => string;
@@ -28,6 +113,17 @@ interface ContextMenuStringTable {
28 paste: () => string; 113 paste: () => string;
29 pasteAndMatchStyle: () => string; 114 pasteAndMatchStyle: () => string;
30 searchWith: ({ searchEngine }: { searchEngine: string }) => string; 115 searchWith: ({ searchEngine }: { searchEngine: string }) => string;
116 translate: () => string;
117 quickTranslate: ({
118 translatorLanguage,
119 }: {
120 translatorLanguage: string;
121 }) => string;
122 translateLanguage: ({
123 translatorLanguage,
124 }: {
125 translatorLanguage: string;
126 }) => string;
31 openLinkUrl: () => string; 127 openLinkUrl: () => string;
32 openLinkInFerdiumUrl: () => string; 128 openLinkInFerdiumUrl: () => string;
33 openInBrowser: () => string; 129 openInBrowser: () => string;
@@ -52,6 +148,10 @@ const contextMenuStringTable: ContextMenuStringTable = {
52 paste: () => 'Paste', 148 paste: () => 'Paste',
53 pasteAndMatchStyle: () => 'Paste and match style', 149 pasteAndMatchStyle: () => 'Paste and match style',
54 searchWith: ({ searchEngine }) => `Search with ${searchEngine}`, 150 searchWith: ({ searchEngine }) => `Search with ${searchEngine}`,
151 translate: () => `Translate to ...`,
152 quickTranslate: ({ translatorLanguage }) =>
153 `Translate to ${translatorLanguage}`,
154 translateLanguage: ({ translatorLanguage }) => `${translatorLanguage}`,
55 openLinkUrl: () => 'Open Link', 155 openLinkUrl: () => 'Open Link',
56 openLinkInFerdiumUrl: () => 'Open Link in Ferdium', 156 openLinkInFerdiumUrl: () => 'Open Link in Ferdium',
57 openInBrowser: () => 'Open in Browser', 157 openInBrowser: () => 'Open in Browser',
@@ -127,7 +227,7 @@ export class ContextMenuBuilder {
127 * 227 *
128 * @param contextInfo The object returned from the 'context-menu' Electron event. 228 * @param contextInfo The object returned from the 'context-menu' Electron event.
129 */ 229 */
130 async showPopupMenu(contextInfo: Electron.ContextMenuParams): Promise<void> { 230 async showPopupMenu(contextInfo: IContextMenuParams): Promise<void> {
131 const menu = await this.buildMenuForElement(contextInfo); 231 const menu = await this.buildMenuForElement(contextInfo);
132 if (!menu) return; 232 if (!menu) return;
133 menu.popup(); 233 menu.popup();
@@ -139,7 +239,7 @@ export class ContextMenuBuilder {
139 * the list but use most of the default behavior. 239 * the list but use most of the default behavior.
140 */ 240 */
141 async buildMenuForElement( 241 async buildMenuForElement(
142 info: Electron.ContextMenuParams, 242 info: IContextMenuParams,
143 ): Promise<Electron.CrossProcessExports.Menu> { 243 ): Promise<Electron.CrossProcessExports.Menu> {
144 if (info.linkURL && info.linkURL.length > 0) { 244 if (info.linkURL && info.linkURL.length > 0) {
145 return this.buildMenuForLink(info); 245 return this.buildMenuForLink(info);
@@ -165,12 +265,17 @@ export class ContextMenuBuilder {
165 * @return {Menu} The `Menu` 265 * @return {Menu} The `Menu`
166 */ 266 */
167 buildMenuForTextInput( 267 buildMenuForTextInput(
168 menuInfo: Electron.ContextMenuParams, 268 menuInfo: IContextMenuParams,
169 ): Electron.CrossProcessExports.Menu { 269 ): Electron.CrossProcessExports.Menu {
170 const menu = new Menu(); 270 const menu = new Menu();
171 271
272 const { enableTranslator } = menuInfo;
273
172 this.addSpellingItems(menu, menuInfo); 274 this.addSpellingItems(menu, menuInfo);
173 this.addSearchItems(menu, menuInfo); 275 this.addSearchItems(menu, menuInfo);
276 if (enableTranslator) {
277 this.addTranslateItems(menu, menuInfo);
278 }
174 279
175 this.addCut(menu, menuInfo); 280 this.addCut(menu, menuInfo);
176 this.addCopy(menu, menuInfo); 281 this.addCopy(menu, menuInfo);
@@ -194,7 +299,7 @@ export class ContextMenuBuilder {
194 * @return {Menu} The `Menu` 299 * @return {Menu} The `Menu`
195 */ 300 */
196 buildMenuForLink( 301 buildMenuForLink(
197 menuInfo: Electron.ContextMenuParams, 302 menuInfo: IContextMenuParams,
198 ): Electron.CrossProcessExports.Menu { 303 ): Electron.CrossProcessExports.Menu {
199 const menu = new Menu(); 304 const menu = new Menu();
200 const isEmailAddress = menuInfo.linkURL.startsWith('mailto:'); 305 const isEmailAddress = menuInfo.linkURL.startsWith('mailto:');
@@ -208,7 +313,6 @@ export class ContextMenuBuilder {
208 const url = isEmailAddress ? menuInfo.linkText : menuInfo.linkURL; 313 const url = isEmailAddress ? menuInfo.linkText : menuInfo.linkURL;
209 clipboard.writeText(url); 314 clipboard.writeText(url);
210 this._sendNotificationOnClipboardEvent( 315 this._sendNotificationOnClipboardEvent(
211 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
212 menuInfo.clipboardNotifications, 316 menuInfo.clipboardNotifications,
213 () => `Link URL copied: ${url}`, 317 () => `Link URL copied: ${url}`,
214 ); 318 );
@@ -257,11 +361,16 @@ export class ContextMenuBuilder {
257 * Builds a menu applicable to a text field. 361 * Builds a menu applicable to a text field.
258 */ 362 */
259 buildMenuForText( 363 buildMenuForText(
260 menuInfo: Electron.ContextMenuParams, 364 menuInfo: IContextMenuParams,
261 ): Electron.CrossProcessExports.Menu { 365 ): Electron.CrossProcessExports.Menu {
262 const menu = new Menu(); 366 const menu = new Menu();
263 367
368 const { enableTranslator } = menuInfo;
369
264 this.addSearchItems(menu, menuInfo); 370 this.addSearchItems(menu, menuInfo);
371 if (enableTranslator) {
372 this.addTranslateItems(menu, menuInfo);
373 }
265 this.addCopy(menu, menuInfo); 374 this.addCopy(menu, menuInfo);
266 this.addInspectElement(menu, menuInfo); 375 this.addInspectElement(menu, menuInfo);
267 // @ts-expect-error Expected 1 arguments, but got 2. 376 // @ts-expect-error Expected 1 arguments, but got 2.
@@ -283,7 +392,7 @@ export class ContextMenuBuilder {
283 * @return {Menu} The `Menu` 392 * @return {Menu} The `Menu`
284 */ 393 */
285 buildMenuForImage( 394 buildMenuForImage(
286 menuInfo: Electron.ContextMenuParams, 395 menuInfo: IContextMenuParams,
287 ): Electron.CrossProcessExports.Menu { 396 ): Electron.CrossProcessExports.Menu {
288 const menu = new Menu(); 397 const menu = new Menu();
289 398
@@ -303,7 +412,7 @@ export class ContextMenuBuilder {
303 */ 412 */
304 addSpellingItems( 413 addSpellingItems(
305 menu: Electron.CrossProcessExports.Menu, 414 menu: Electron.CrossProcessExports.Menu,
306 menuInfo: Electron.ContextMenuParams, 415 menuInfo: IContextMenuParams,
307 ) { 416 ) {
308 const webContents = this.getWebContents(); 417 const webContents = this.getWebContents();
309 // Add each spelling suggestion 418 // Add each spelling suggestion
@@ -338,7 +447,7 @@ export class ContextMenuBuilder {
338 */ 447 */
339 addSearchItems( 448 addSearchItems(
340 menu: Electron.CrossProcessExports.Menu, 449 menu: Electron.CrossProcessExports.Menu,
341 menuInfo: Electron.ContextMenuParams, 450 menuInfo: IContextMenuParams,
342 ) { 451 ) {
343 if (!menuInfo.selectionText || menuInfo.selectionText.length === 0) { 452 if (!menuInfo.selectionText || menuInfo.selectionText.length === 0) {
344 return menu; 453 return menu;
@@ -364,11 +473,9 @@ export class ContextMenuBuilder {
364 473
365 const search = new MenuItem({ 474 const search = new MenuItem({
366 label: this.stringTable.searchWith({ 475 label: this.stringTable.searchWith({
367 // @ts-expect-error Property 'searchEngine' does not exist on type 'ContextMenuParams'.
368 searchEngine: SEARCH_ENGINE_NAMES[menuInfo.searchEngine], 476 searchEngine: SEARCH_ENGINE_NAMES[menuInfo.searchEngine],
369 }), 477 }),
370 click: () => { 478 click: () => {
371 // @ts-expect-error Property 'searchEngine' does not exist on type 'ContextMenuParams'.
372 const url = SEARCH_ENGINE_URLS[menuInfo.searchEngine]({ 479 const url = SEARCH_ENGINE_URLS[menuInfo.searchEngine]({
373 searchTerm: encodeURIComponent(menuInfo.selectionText), 480 searchTerm: encodeURIComponent(menuInfo.selectionText),
374 }); 481 });
@@ -382,7 +489,106 @@ export class ContextMenuBuilder {
382 return menu; 489 return menu;
383 } 490 }
384 491
385 isSrcUrlValid(menuInfo: Electron.ContextMenuParams) { 492 /**
493 * Adds translate-related menu items.
494 */
495 addTranslateItems(
496 menu: Electron.CrossProcessExports.Menu,
497 menuInfo: IContextMenuParams,
498 ) {
499 const { translatorEngine } = menuInfo;
500
501 if (!menuInfo.selectionText || menuInfo.selectionText.length === 0) {
502 return menu;
503 }
504
505 const match = matchesWord(menuInfo.selectionText);
506 if (!match || match.length === 0) {
507 return menu;
508 }
509
510 const generateTranslationItems = async (
511 translateToLanguage: string,
512 translatorEngine: string,
513 ) => {
514 // TODO: Need to support i18n
515 translatePopup('Loading...', false);
516
517 const translatedText = await ipcRenderer.invoke('translate', {
518 text: menuInfo.selectionText,
519 translateToLanguage,
520 translatorEngine,
521 });
522
523 translatePopup(translatedText.text, translatedText.error);
524 };
525
526 let arrayLanguagesAfterEngine;
527 if (translatorEngine === TRANSLATOR_ENGINE_LIBRETRANSLATE) {
528 arrayLanguagesAfterEngine = LIBRETRANSLATE_TRANSLATOR_LANGUAGES;
529 } else if (translatorEngine === TRANSLATOR_ENGINE_GOOGLE) {
530 arrayLanguagesAfterEngine = GOOGLE_TRANSLATOR_LANGUAGES;
531 }
532
533 const arrayLanguages = Object.keys(arrayLanguagesAfterEngine).map(
534 code => arrayLanguagesAfterEngine[code],
535 );
536
537 const menuLanguages = new Menu();
538
539 const getLanguageCode = (obj, val): string =>
540 Object.keys(obj).find(key => obj[key] === val)!;
541
542 for (const language in arrayLanguages) {
543 if (arrayLanguages[language]) {
544 const languageItem = new MenuItem({
545 label: this.stringTable.translateLanguage({
546 translatorLanguage: arrayLanguages[language],
547 }),
548 click: () => {
549 const translateToLanguageCode = getLanguageCode(
550 arrayLanguagesAfterEngine,
551 arrayLanguages[language],
552 );
553 generateTranslationItems(translateToLanguageCode, translatorEngine);
554 },
555 });
556 menuLanguages.append(languageItem);
557 }
558 }
559
560 const translateToLanguage =
561 arrayLanguagesAfterEngine[menuInfo.translatorLanguage];
562
563 const translateToLanguageCode = getLanguageCode(
564 arrayLanguagesAfterEngine,
565 translateToLanguage,
566 );
567 const quickTranslateItem = new MenuItem({
568 label: this.stringTable.quickTranslate({
569 translatorLanguage: translateToLanguage,
570 }),
571 click: () => {
572 generateTranslationItems(translateToLanguageCode, translatorEngine);
573 },
574 });
575
576 const translateItem = new MenuItem({
577 label: this.stringTable.translate(),
578 submenu: menuLanguages,
579 click: () => {
580 generateTranslationItems(translateToLanguageCode, translatorEngine);
581 },
582 });
583
584 menu.append(translateItem);
585 menu.append(quickTranslateItem);
586 this.addSeparator(menu);
587
588 return menu;
589 }
590
591 isSrcUrlValid(menuInfo: IContextMenuParams) {
386 return menuInfo.srcURL && menuInfo.srcURL.length > 0; 592 return menuInfo.srcURL && menuInfo.srcURL.length > 0;
387 } 593 }
388 594
@@ -391,7 +597,7 @@ export class ContextMenuBuilder {
391 */ 597 */
392 addImageItems( 598 addImageItems(
393 menu: Electron.CrossProcessExports.Menu, 599 menu: Electron.CrossProcessExports.Menu,
394 menuInfo: Electron.ContextMenuParams, 600 menuInfo: IContextMenuParams,
395 ) { 601 ) {
396 const copyImage = new MenuItem({ 602 const copyImage = new MenuItem({
397 label: this.stringTable.copyImage(), 603 label: this.stringTable.copyImage(),
@@ -403,7 +609,6 @@ export class ContextMenuBuilder {
403 ); 609 );
404 610
405 this._sendNotificationOnClipboardEvent( 611 this._sendNotificationOnClipboardEvent(
406 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
407 menuInfo.clipboardNotifications, 612 menuInfo.clipboardNotifications,
408 () => `Image copied from URL: ${menuInfo.srcURL}`, 613 () => `Image copied from URL: ${menuInfo.srcURL}`,
409 ); 614 );
@@ -418,7 +623,6 @@ export class ContextMenuBuilder {
418 click: () => { 623 click: () => {
419 const result = clipboard.writeText(menuInfo.srcURL); 624 const result = clipboard.writeText(menuInfo.srcURL);
420 this._sendNotificationOnClipboardEvent( 625 this._sendNotificationOnClipboardEvent(
421 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
422 menuInfo.clipboardNotifications, 626 menuInfo.clipboardNotifications,
423 () => `Image URL copied: ${menuInfo.srcURL}`, 627 () => `Image URL copied: ${menuInfo.srcURL}`,
424 ); 628 );
@@ -446,7 +650,6 @@ export class ContextMenuBuilder {
446 }); 650 });
447 }); 651 });
448 this._sendNotificationOnClipboardEvent( 652 this._sendNotificationOnClipboardEvent(
449 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
450 menuInfo.clipboardNotifications, 653 menuInfo.clipboardNotifications,
451 () => `Image downloaded: ${urlWithoutBlob}`, 654 () => `Image downloaded: ${urlWithoutBlob}`,
452 ); 655 );
@@ -464,7 +667,7 @@ export class ContextMenuBuilder {
464 */ 667 */
465 addCut( 668 addCut(
466 menu: Electron.CrossProcessExports.Menu, 669 menu: Electron.CrossProcessExports.Menu,
467 menuInfo: Electron.ContextMenuParams, 670 menuInfo: IContextMenuParams,
468 ) { 671 ) {
469 const webContents = this.getWebContents(); 672 const webContents = this.getWebContents();
470 menu.append( 673 menu.append(
@@ -484,7 +687,7 @@ export class ContextMenuBuilder {
484 */ 687 */
485 addCopy( 688 addCopy(
486 menu: Electron.CrossProcessExports.Menu, 689 menu: Electron.CrossProcessExports.Menu,
487 menuInfo: Electron.ContextMenuParams, 690 menuInfo: IContextMenuParams,
488 ) { 691 ) {
489 const webContents = this.getWebContents(); 692 const webContents = this.getWebContents();
490 menu.append( 693 menu.append(
@@ -504,7 +707,7 @@ export class ContextMenuBuilder {
504 */ 707 */
505 addPaste( 708 addPaste(
506 menu: Electron.CrossProcessExports.Menu, 709 menu: Electron.CrossProcessExports.Menu,
507 menuInfo: Electron.ContextMenuParams, 710 menuInfo: IContextMenuParams,
508 ) { 711 ) {
509 const webContents = this.getWebContents(); 712 const webContents = this.getWebContents();
510 menu.append( 713 menu.append(
@@ -521,7 +724,7 @@ export class ContextMenuBuilder {
521 724
522 addPastePlain( 725 addPastePlain(
523 menu: Electron.CrossProcessExports.Menu, 726 menu: Electron.CrossProcessExports.Menu,
524 menuInfo: Electron.ContextMenuParams, 727 menuInfo: IContextMenuParams,
525 ) { 728 ) {
526 if ( 729 if (
527 menuInfo.editFlags.canPaste && 730 menuInfo.editFlags.canPaste &&
@@ -552,7 +755,7 @@ export class ContextMenuBuilder {
552 */ 755 */
553 addInspectElement( 756 addInspectElement(
554 menu: Electron.CrossProcessExports.Menu, 757 menu: Electron.CrossProcessExports.Menu,
555 menuInfo: Electron.ContextMenuParams, 758 menuInfo: IContextMenuParams,
556 needsSeparator = true, 759 needsSeparator = true,
557 ) { 760 ) {
558 const webContents = this.getWebContents(); 761 const webContents = this.getWebContents();
@@ -609,6 +812,7 @@ export class ContextMenuBuilder {
609 */ 812 */
610 goBack(menu: Electron.CrossProcessExports.Menu) { 813 goBack(menu: Electron.CrossProcessExports.Menu) {
611 const webContents = this.getWebContents(); 814 const webContents = this.getWebContents();
815
612 menu.append( 816 menu.append(
613 new MenuItem({ 817 new MenuItem({
614 label: this.stringTable.goBack(), 818 label: this.stringTable.goBack(),
@@ -643,7 +847,7 @@ export class ContextMenuBuilder {
643 */ 847 */
644 copyPageUrl( 848 copyPageUrl(
645 menu: Electron.CrossProcessExports.Menu, 849 menu: Electron.CrossProcessExports.Menu,
646 menuInfo: Electron.ContextMenuParams, 850 menuInfo: IContextMenuParams,
647 ) { 851 ) {
648 menu.append( 852 menu.append(
649 new MenuItem({ 853 new MenuItem({
@@ -652,7 +856,6 @@ export class ContextMenuBuilder {
652 click: () => { 856 click: () => {
653 clipboard.writeText(window.location.href); 857 clipboard.writeText(window.location.href);
654 this._sendNotificationOnClipboardEvent( 858 this._sendNotificationOnClipboardEvent(
655 // @ts-expect-error Property 'clipboardNotifications' does not exist on type 'ContextMenuParams'.
656 menuInfo?.clipboardNotifications, 859 menuInfo?.clipboardNotifications,
657 () => `Page URL copied: ${window.location.href}`, 860 () => `Page URL copied: ${window.location.href}`,
658 ); 861 );
@@ -668,7 +871,7 @@ export class ContextMenuBuilder {
668 */ 871 */
669 goToHomePage( 872 goToHomePage(
670 menu: Electron.CrossProcessExports.Menu, 873 menu: Electron.CrossProcessExports.Menu,
671 menuInfo: Electron.ContextMenuParams, 874 menuInfo: IContextMenuParams,
672 ) { 875 ) {
673 const baseURL = new window.URL(menuInfo.pageURL); 876 const baseURL = new window.URL(menuInfo.pageURL);
674 menu.append( 877 menu.append(
@@ -691,7 +894,7 @@ export class ContextMenuBuilder {
691 */ 894 */
692 openInBrowser( 895 openInBrowser(
693 menu: Electron.CrossProcessExports.Menu, 896 menu: Electron.CrossProcessExports.Menu,
694 menuInfo: Electron.ContextMenuParams, 897 menuInfo: IContextMenuParams,
695 ) { 898 ) {
696 menu.append( 899 menu.append(
697 new MenuItem({ 900 new MenuItem({
diff --git a/src/webview/recipe.js b/src/webview/recipe.js
index 8c93da202..acf4f9f31 100644
--- a/src/webview/recipe.js
+++ b/src/webview/recipe.js
@@ -188,6 +188,9 @@ class RecipeController {
188 () => this.spellcheckerLanguage, 188 () => this.spellcheckerLanguage,
189 () => this.settings.app.searchEngine, 189 () => this.settings.app.searchEngine,
190 () => this.settings.app.clipboardNotifications, 190 () => this.settings.app.clipboardNotifications,
191 () => this.settings.app.enableTranslator,
192 () => this.settings.app.translatorEngine,
193 () => this.settings.app.translatorLanguage,
191 ); 194 );
192 195
193 autorun(() => this.update()); 196 autorun(() => this.update());
@@ -293,6 +296,9 @@ class RecipeController {
293 ); 296 );
294 debug('darkReaderSettigs', this.settings.service.darkReaderSettings); 297 debug('darkReaderSettigs', this.settings.service.darkReaderSettings);
295 debug('searchEngine', this.settings.app.searchEngine); 298 debug('searchEngine', this.settings.app.searchEngine);
299 debug('enableTranslator', this.settings.app.enableTranslator);
300 debug('translatorEngine', this.settings.app.translatorEngine);
301 debug('translatorLanguage', this.settings.app.translatorLanguage);
296 302
297 if (this.userscript && this.userscript.internal_setSettings) { 303 if (this.userscript && this.userscript.internal_setSettings) {
298 this.userscript.internal_setSettings(this.settings); 304 this.userscript.internal_setSettings(this.settings);