diff --git a/package.json b/package.json index 618da02..3c53a2e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "textractor-translator", "productName": "TextractorTranslator", - "version": "0.3.2", + "version": "0.4.0", "description": "Textractor Translator", "main": ".webpack/main", "scripts": { diff --git a/src/configuration/Configuration.ts b/src/configuration/Configuration.ts index 187b468..1d4c2e0 100644 --- a/src/configuration/Configuration.ts +++ b/src/configuration/Configuration.ts @@ -15,11 +15,20 @@ export interface Translator { translate(text: string, sourceLanguage: string, targetLanguage: string): Promise; } +export interface LibreTranslatorConfig { + host?: string; + apiKey?: string; + format?: 'text' | 'html'; +} + export interface DefinedTranslators { - GOOGLE_TRANSLATE: Translator, - X_IDENTITY: Translator, - X_INFINITE: Translator, - X_NONE: Translator + GoogleTranslate: () => Translator, + LibreTranslate: (config?: LibreTranslatorConfig) => Translator; + debug: { + Identity: () => Translator; + Infinite: () => Translator; + None: () => Translator; + } } export interface DisplayedTransformedText { @@ -39,6 +48,8 @@ export interface Configuration { source: string; target: string; }; + transformOriginal?(sentence: Sentence): MultiTransformedText; + transformTranslated?(translatedText: string, originalSentence: Sentence): OptionalTransformedText; } \ No newline at end of file diff --git a/src/configuration/getConfiguration.ts b/src/configuration/getConfiguration.ts index 79f3e8e..1ef1ca4 100644 --- a/src/configuration/getConfiguration.ts +++ b/src/configuration/getConfiguration.ts @@ -3,18 +3,22 @@ import safeEval from 'safe-eval'; import transformOriginal from '../transformation/transformOriginal'; import transformTranslated from '../transformation/transformTranslated'; import nodeConsole from '../utils/nodeConsole'; -import {DefinedTranslatorsImpl} from '../translation/DefinedTranslators'; import httpRequest from '../utils/httpRequest'; import * as queryString from 'query-string'; +import {DefinedTranslatorsImpl} from '../translation/DefinedTranslators'; +import * as net from 'net'; interface CommonConfigContext { config: Configuration; common: object; memory: object; - DefinedTranslators: DefinedTranslators; + Translators: DefinedTranslators; httpRequest: typeof httpRequest; queryString: typeof queryString console: Console; + URL: typeof URL, + URLSearchParams: typeof URLSearchParams, + net: typeof net } interface CustomConfigContext extends CommonConfigContext { @@ -35,9 +39,11 @@ const safeEvalVoid = (code: string, context?: Record): void => { const memory = {}; const getCommonConfiguration = (configSourceCode: string | undefined): CommonConfigContext => { + const definedTranslators = new DefinedTranslatorsImpl(); + const context: CommonConfigContext = { config: { - translator: DefinedTranslatorsImpl.INSTANCE.GOOGLE_TRANSLATE, + translator: definedTranslators.GoogleTranslate(), languages: { source: 'auto', target: 'en' @@ -47,10 +53,13 @@ const getCommonConfiguration = (configSourceCode: string | undefined): CommonCon }, common: {}, memory, - DefinedTranslators: DefinedTranslatorsImpl.INSTANCE, + Translators: definedTranslators, httpRequest, queryString, - console + console, + URL, + URLSearchParams, + net }; if (configSourceCode != null) { @@ -72,14 +81,9 @@ const getConfiguration = (commonConfigSourceCode: string | undefined, configSour } const context: CustomConfigContext = { - common: commonConfigContext.common, + ...commonConfigContext, commonConfig: commonConfigContext.config, config: {...commonConfigContext.config}, - memory, - DefinedTranslators: DefinedTranslatorsImpl.INSTANCE, - httpRequest, - queryString, - console }; safeEvalVoid(configSourceCode, context); diff --git a/src/configuration/getProfileConfig.ts b/src/configuration/getProfileConfig.ts index bd10540..bf7d7e0 100644 --- a/src/configuration/getProfileConfig.ts +++ b/src/configuration/getProfileConfig.ts @@ -1,13 +1,30 @@ import indexArrayBy from '../utils/indexArrayBy'; import {COMMON_PROFILE_ID} from '../windows/settings/profiles/constants'; import getConfiguration from './getConfiguration'; -import type Store from 'electron-store'; import {Configuration} from './Configuration'; import initializeSavedProfiles from './initializeSavedProfiles'; import {StoreKeys} from '../constants/store-keys'; import SavedProfiles from '../windows/settings/profiles/SavedProfiles'; +import ref from '../utils/ref'; +import electronStore from '../electron-store/electronStore'; +import store from '../electron-store/store'; + +const configurationCache = ref(); + +store.onDidChange(StoreKeys.SAVED_PROFILES, () => { + configurationCache.current = undefined; +}); + +electronStore.onDidChange(StoreKeys.SAVED_PROFILES, () => { + configurationCache.current = undefined; +}); + +const getProfileConfig = (profileId?: string): Configuration => { + const cache = configurationCache.current; + if (cache) { + return cache; + } -const getProfileConfig = (store: Store, profileId?: string): Configuration => { const savedProfiles = store.get(StoreKeys.SAVED_PROFILES) as (SavedProfiles | undefined) ?? initializeSavedProfiles(store); const savedProfilesMap = indexArrayBy(savedProfiles.profiles, 'id'); @@ -15,7 +32,9 @@ const getProfileConfig = (store: Store, profileId?: string): Configuration => { const activeProfileId = profileId ?? savedProfiles.activeProfileId; const activeProfile = activeProfileId ? savedProfilesMap.get(activeProfileId) : undefined; - return getConfiguration(commonProfile?.configSource, activeProfile?.configSource); + const result = getConfiguration(commonProfile?.configSource, activeProfile?.configSource); + configurationCache.current = result; + return result; }; export default getProfileConfig; \ No newline at end of file diff --git a/src/electron-store/initElectronStore.ts b/src/electron-store/initElectronStore.ts index 2a49ee7..d6b1e1f 100644 --- a/src/electron-store/initElectronStore.ts +++ b/src/electron-store/initElectronStore.ts @@ -2,10 +2,9 @@ import {BrowserWindow, ipcMain} from 'electron'; import Store from 'electron-store'; import nodeConsole from '../utils/nodeConsole'; import {electronStoreKeysSimple} from './electronStoreShared'; +import store from './store'; export const initElectronStore = (): Store => { - const store = new Store(); - nodeConsole.log('Creating handlers for electronStore') for (const key of electronStoreKeysSimple) { diff --git a/src/electron-store/store.ts b/src/electron-store/store.ts new file mode 100644 index 0000000..c58a564 --- /dev/null +++ b/src/electron-store/store.ts @@ -0,0 +1,5 @@ +import Store from 'electron-store'; + +const store = new Store(); + +export default store; \ No newline at end of file diff --git a/src/translation/DefinedTranslators.ts b/src/translation/DefinedTranslators.ts index dc77d1e..c01ab27 100644 --- a/src/translation/DefinedTranslators.ts +++ b/src/translation/DefinedTranslators.ts @@ -1,31 +1,34 @@ -import {DefinedTranslators, Translator} from '../configuration/Configuration'; +import {DefinedTranslators, LibreTranslatorConfig} from '../configuration/Configuration'; import GoogleTranslator from './translators/GoogleTranslator'; +import IdentityTranslator from './translators/utility/IdentityTranslator'; +import InfiniteTranslator from './translators/utility/InfiniteTranslator'; +import NoneTranslator from './translators/utility/NoneTranslator'; +import LibreTranslator from './translators/LibreTranslator'; -class IdentityTranslator implements Translator { - translate(text: string, sourceLanguage: string, targetLanguage: string): Promise { - return Promise.resolve(text); - } -} - -class NoneTranslator implements Translator { - translate(text: string, sourceLanguage: string, targetLanguage: string): Promise { - return undefined as any; - } -} - -class InfiniteTranslator implements Translator { - translate(text: string, sourceLanguage: string, targetLanguage: string): Promise { - return new Promise(() => {}); +export class DefinedTranslatorsImpl implements DefinedTranslators { + constructor() { + Object.keys(this).forEach((keyName) => { + const key = keyName as keyof DefinedTranslatorsImpl; + Object.defineProperty(this, key, { + value: this[key], + writable: false, + configurable: false, + enumerable: true + }); + }); } -} -export class DefinedTranslatorsImpl implements DefinedTranslators { - GOOGLE_TRANSLATE = new GoogleTranslator(); - X_IDENTITY = new IdentityTranslator(); - X_NONE = new NoneTranslator(); - X_INFINITE = new InfiniteTranslator(); + GoogleTranslate = () => { + return new GoogleTranslator(); + }; - public static INSTANCE: DefinedTranslators = new DefinedTranslatorsImpl(); + LibreTranslate = (config?: LibreTranslatorConfig) => { + return new LibreTranslator(config || {}); + }; - private constructor() {} + debug = Object.freeze({ + Identity: () => new IdentityTranslator(), + Infinite: () => new InfiniteTranslator(), + None: () => new NoneTranslator() + }); } \ No newline at end of file diff --git a/src/translation/translators/LibreTranslator.ts b/src/translation/translators/LibreTranslator.ts new file mode 100644 index 0000000..662e271 --- /dev/null +++ b/src/translation/translators/LibreTranslator.ts @@ -0,0 +1,35 @@ +import httpRequest from '../../utils/httpRequest'; +import {LibreTranslatorConfig, Translator} from '../../configuration/Configuration'; + +class LibreTranslator implements Translator { + constructor(private readonly config: LibreTranslatorConfig) { + } + + async translate(text: string, sourceLanguage: string, targetLanguage: string): Promise { + const host = this.config.host || 'https://libretranslate.com'; + const url = new URL('/translate', host).toString(); + + const responseData: any = await httpRequest(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + q: text, + source: sourceLanguage, + target: targetLanguage, + format: this.config.format, + api_key: this.config.apiKey + }) + }).then((response) => response.json()); + + if (!responseData || responseData.translatedText == null) { + console.error('LibreTranslator unable to translate: ', responseData); + throw responseData; + } + + return responseData.translatedText as string; + } +} + +export default LibreTranslator; \ No newline at end of file diff --git a/src/translation/translators/utility/IdentityTranslator.ts b/src/translation/translators/utility/IdentityTranslator.ts new file mode 100644 index 0000000..15fc1a4 --- /dev/null +++ b/src/translation/translators/utility/IdentityTranslator.ts @@ -0,0 +1,9 @@ +import {Translator} from '../../../configuration/Configuration'; + +class IdentityTranslator implements Translator { + translate(text: string, sourceLanguage: string, targetLanguage: string): Promise { + return Promise.resolve(text); + } +} + +export default IdentityTranslator; \ No newline at end of file diff --git a/src/translation/translators/utility/InfiniteTranslator.ts b/src/translation/translators/utility/InfiniteTranslator.ts new file mode 100644 index 0000000..92c498a --- /dev/null +++ b/src/translation/translators/utility/InfiniteTranslator.ts @@ -0,0 +1,9 @@ +import {Translator} from '../../../configuration/Configuration'; + +class InfiniteTranslator implements Translator { + translate(text: string, sourceLanguage: string, targetLanguage: string): Promise { + return new Promise(() => {}); + } +} + +export default InfiniteTranslator; \ No newline at end of file diff --git a/src/translation/translators/utility/NoneTranslator.ts b/src/translation/translators/utility/NoneTranslator.ts new file mode 100644 index 0000000..d00899e --- /dev/null +++ b/src/translation/translators/utility/NoneTranslator.ts @@ -0,0 +1,9 @@ +import {Translator} from '../../../configuration/Configuration'; + +class NoneTranslator implements Translator { + translate(text: string, sourceLanguage: string, targetLanguage: string): Promise { + return undefined as any; + } +} + +export default NoneTranslator; \ No newline at end of file diff --git a/src/windows/main/logic/workTextractorPipe.ts b/src/windows/main/logic/workTextractorPipe.ts index 40b0075..6f11b1c 100644 --- a/src/windows/main/logic/workTextractorPipe.ts +++ b/src/windows/main/logic/workTextractorPipe.ts @@ -1,6 +1,5 @@ import {listenToPipe} from '../../../textractorServer'; import {Configuration, OptionalTransformedText, Sentence} from '../../../configuration/Configuration'; -import Store from 'electron-store'; import getProfileConfig from '../../../configuration/getProfileConfig'; import nodeConsole from '../../../utils/nodeConsole'; @@ -73,8 +72,6 @@ const showSentence = ( textContainerWrapper.scrollTo(0, textContainerWrapper.scrollHeight); }; -const store = new Store(); - const workTextractorPipe = () => { const textContainerWrapper = document.getElementById('text-wrapper')!; const textContainer = document.getElementById('text')!; @@ -82,7 +79,7 @@ const workTextractorPipe = () => { listenToPipe((sentence) => { try { - const config = getProfileConfig(store); + const config = getProfileConfig(); const multiTransformedText = config.transformOriginal?.(sentence); const transformedTexts = Array.isArray(multiTransformedText) ? multiTransformedText : [multiTransformedText]; diff --git a/src/windows/settings/components/ProfileSourceEditor.tsx b/src/windows/settings/components/ProfileSourceEditor.tsx index 74dea6b..b1462c0 100644 --- a/src/windows/settings/components/ProfileSourceEditor.tsx +++ b/src/windows/settings/components/ProfileSourceEditor.tsx @@ -4,35 +4,49 @@ import React, {FC, useEffect} from 'react'; import configurationDeclarations from "!!raw-loader!../../../configuration/Configuration"; // @ts-ignore // eslint-disable-next-line -import electronRequestDeclarations from '!!raw-loader!electron-request/dist/index' +import electronRequestDeclarations from '!!raw-loader!electron-request/dist/index.d.ts' // @ts-ignore // eslint-disable-next-line -// import queryStringDeclarations from '!!raw-loader!query-string/index' +import queryStringDeclarations from '!!raw-loader!query-string/index.d.ts' +// @ts-ignore +// eslint-disable-next-line +import nodeNetDeclarations from '!!raw-loader!@types/node/net.d.ts' import * as monaco from 'monaco-editor'; import SavedProfile from '../profiles/SavedProfile'; import {COMMON_PROFILE_ID} from '../profiles/constants'; const initializeMonacoTypes = (isCommon: boolean) => { - const declarations = [ - electronRequestDeclarations.toString() - .replace(/declare const main:.*;/g, ''), - configurationDeclarations, - ]; - - const configDeclarationsUsable = declarations - .join('\n\n') + const configDeclarationsUsable = configurationDeclarations .replace(/export default \w+;?/, '') - .replace(/export ([a-z]+) /g, '$1 ') - .replace(/export \{.*};/g, ''); + .replace(/export \{.*};/g, '') + .replace(/export ([a-z]+) /g, '$1 '); monaco.languages.typescript.javascriptDefaults.setExtraLibs([ + { + content: nodeNetDeclarations + .replace('declare module \'net\' {', 'declare namespace net {') + .replace('declare module \'node:net\' {\n export * from \'net\';\n}', ''), + filePath: 'node/net.ts' + }, + { + content: 'declare namespace queryString {\n' + (queryStringDeclarations.toString() + .replace(/export \{.*};/g, '')) + + '\n}', + filePath: 'queryString.ts' + }, + { + content: 'declare namespace ElectronRequest {\n' + (electronRequestDeclarations.toString() + .replace(/declare const main:.*;/g, '') + .replace(/export \{.*};/g, '')) + + '\n}\n\nconst httpRequest: (requestURL: string, options?: ElectronRequest.Options) => Promise;', + filePath: 'electron-request.ts' + }, { content: configDeclarationsUsable + ` declare const config: Configuration; const common: object; const memory: object; -const DefinedTranslators: DefinedTranslators; -const httpRequest: (requestURL: string, options?: Options) => Promise; +const Translators: DefinedTranslators; const queryString: any; `.trimEnd() + (isCommon ? '' : ` diff --git a/src/windows/settings/components/weewrwe.d.ts b/src/windows/settings/components/weewrwe.d.ts new file mode 100644 index 0000000..2ad072f --- /dev/null +++ b/src/windows/settings/components/weewrwe.d.ts @@ -0,0 +1,157 @@ +declare namespace ElectronRequest { + import { BinaryToTextEncoding } from 'crypto'; + import { Stream, Writable } from 'stream'; + + interface Session { + // Docs: https://electronjs.org/docs/api/session + + /** + * A `Session` object, the default session object of the app. + */ + defaultSession: Session; + } + + interface Options { + /** + * Request method + * @default 'GET' + */ + method?: string; + /** + * Request body + * @default null + */ + body?: string | null | Buffer | Stream; + /** + * Request headers + */ + headers?: Record; + /** + * Request query + */ + query?: Record; + /** + * Allow redirect + * @default true + */ + followRedirect?: boolean; + /** + * Maximum redirect count. 0 to not follow redirect + * @default 20 + */ + maxRedirectCount?: number; + /** + * Request/Response timeout in ms. 0 to disable + * @default 0 + */ + timeout?: number; + /** + * Maximum response body size in bytes. 0 to disable + * @default 0 + */ + size?: number; + /** + * Whether to use nodejs native request + * @default false + */ + useNative?: boolean; + + // Docs: https://www.electronjs.org/docs/api/client-request#new-clientrequestoptions + + /** + * Only in Electron. When use authenticated HTTP proxy, username to use to authenticate + */ + username?: string; + /** + * Only in Electron. When use authenticated HTTP proxy, password to use to authenticate + */ + password?: string; + /** + * Only in Electron. Whether to send cookies with this request from the provided session + * @default true + */ + useSessionCookies?: boolean; + /** + * Only in Electron. The Session instance with which the request is associated + * @default electron.session.defaultSession + */ + session?: Session; + } + + interface ProgressInfo { + /** Total file bytes */ + total: number; + /** Delta file bytes */ + delta: number; + /** Transferred file bytes */ + transferred: number; + /** Transferred percentage */ + percent: number; + /** Bytes transferred per second */ + bytesPerSecond: number; + } + + type ProgressCallback = (progressInfo: ProgressInfo) => void; + + interface ValidateOptions { + /** Expected hash */ + expected: string; + /** + * Algorithm: first parameter of crypto.createHash + * @default 'md5' + */ + algorithm?: string; + /** + * Encoding: first parameter of Hash.digest + * @default 'base64' + */ + encoding?: BinaryToTextEncoding; + } + + interface Response { + /** Whether the response was successful (status in the range 200-299) */ + ok: boolean; + /** Response headers */ + headers: Record; + /** Return origin stream */ + stream: Stream; + /** Decode response as ArrayBuffer */ + arrayBuffer(): Promise; + /** Decode response as Blob */ + blob(): Promise; + /** Decode response as text */ + text(): Promise; + /** Decode response as json */ + json(): Promise; + /** Decode response as buffer */ + buffer(): Promise; + /** + * Download file to destination + * @param {Writable} destination Writable destination stream + * @param {ProgressCallback=} onProgress Download progress callback + * @param {ValidateOptions=} validateOptions Validate options + */ + download: ( + destination: Writable, + onProgress?: ProgressCallback, + validateOptions?: ValidateOptions, + ) => Promise; + } + + interface Blob { + size: number; + type: string; + isClosed: boolean; + content: Buffer; + slice(start?: number, end?: number, type?: string): Blob; + close(): void; + toString(): string; + } + + // declare const main: (requestURL: string, options?: Options) => Promise; + + // export { ProgressInfo, Response, main as default }; + +} + +const httpRequest: (requestURL: string, options?: ElectronRequest.Options) => Promise; \ No newline at end of file diff --git a/webpack.main.config.ts b/webpack.main.config.ts index ed37547..b33b893 100644 --- a/webpack.main.config.ts +++ b/webpack.main.config.ts @@ -1,6 +1,7 @@ import type { Configuration } from 'webpack'; import { rules } from './webpack.rules'; +import path from 'path'; export const mainConfig: Configuration = { /** @@ -14,5 +15,8 @@ export const mainConfig: Configuration = { }, resolve: { extensions: ['.js', '.ts', '.jsx', '.tsx', '.css', '.scss', '.sass', '.json'], + alias: { + '@types/node': path.resolve(__dirname, 'node_modules/@types/node') + } }, }; diff --git a/webpack.renderer.config.ts b/webpack.renderer.config.ts index dc097d0..110ac6a 100644 --- a/webpack.renderer.config.ts +++ b/webpack.renderer.config.ts @@ -2,6 +2,7 @@ import type { Configuration } from 'webpack'; import { rules } from './webpack.rules'; import { plugins } from './webpack.plugins'; +import path from 'path'; // eslint-disable-next-line @typescript-eslint/no-var-requires const webpack = require('webpack'); @@ -23,5 +24,8 @@ export const rendererConfig: Configuration = { ], resolve: { extensions: ['.js', '.ts', '.jsx', '.tsx', '.css'], + alias: { + '@types/node': path.resolve(__dirname, 'node_modules/@types/node') + } }, };