diff --git a/package.json b/package.json index 4f4e210..d748d23 100644 --- a/package.json +++ b/package.json @@ -29,11 +29,7 @@ "Other" ], "activationEvents": [ - "onStartupFinished", - "onCommand:deepl.translate", - "onCommand:deepl.translateTo", - "onCommand:deepl.translateFromTo", - "onCommand:deepl.translateBelow" + "onStartupFinished" ], "icon": "resources/logo@2.png", "galleryBanner": { @@ -49,6 +45,7 @@ "deepl.apiKey": { "type": "string", "default": "", + "deprecationMessage": "For security reasons the api key will no longer be stored in the configuration. Configured api keys will be automatically migrated to the vscode secret store and removed from the config. The config option will be removed in future releases!", "markdownDescription": "The key is used to authenticate with the DeepL API. [See offical documentation](https://www.deepl.com/docs-api/accessing-the-api/authentication/)" }, "deepl.formality": { @@ -271,6 +268,7 @@ "eslint": "^8.56.0", "glob": "^10.3.10", "husky": "^8.0.3", + "lint-staged": "^15.2.0", "ts-loader": "^9.2.2", "ts-node": "^10.1.0", "typescript": "^5.3.3", @@ -279,10 +277,7 @@ }, "dependencies": { "@vue/reactivity": "^3.4.10", - "axios": "^1.6.0", - "deepl-node": "^1.11.0", - "lint-staged": "^15.2.0", - "xstate": "4.23.0" + "deepl-node": "^1.11.0" }, "lint-staged": { "*.{ts,tsx}": [ diff --git a/src/commands.ts b/src/commands.ts index 1b7a39a..b42075c 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,14 +1,14 @@ -import * as deepl from './deepl'; import * as debug from './debug'; +import * as deepl from './deepl'; import * as vscode from 'vscode'; import { state } from './state'; import { showMessageWithTimeout } from './vscode'; import { showApiKeyInput, showSourceLanguageInput, showTargetLanguageInput } from "./inputs"; -import { TranslateCommandParam, TranslateParam } from './types'; import { getDefaultSourceLanguage, getDefaultTargetLanguage } from './helper'; +import { SourceLanguageCode, TargetLanguageCode } from 'deepl-node'; -function translateSelections(selections: vscode.Selection[], translateParam: TranslateParam): Thenable { - const { targetLang, sourceLang, below } = translateParam; +function translateSelections(selections: vscode.Selection[], request: { targetLang: TargetLanguageCode, sourceLang: SourceLanguageCode | null, translateBelow: boolean }): Thenable { + const { targetLang, sourceLang, translateBelow } = request; return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: 'Translating' }, async (progress) => { const increment = 100 / 2 / selections.length; @@ -25,12 +25,15 @@ function translateSelections(selections: vscode.Selection[], translateParam: Tra ? `Start translating '${text}' to '${targetLang}'` : `Start translating '${text}' from '${sourceLang}' to '${targetLang}'` ); - const translations = await deepl.translate(text, targetLang, sourceLang).catch(() => []); - const result = translations.length > 0 ? translations[0] : null; + const result = await deepl.translate( + text, + sourceLang ?? null, + targetLang, + ); progress.report({ increment }); debug.write( result - ? `Successfully translated '${text}' to '${result.text}'! (Source: '${result.detected_source_language}', Target: '${targetLang}')` + ? `Successfully translated '${text}' to '${result.text}'! (Source: '${result.detectedSourceLang}', Target: '${targetLang}')` : `'${text}' could be translated to '${targetLang}! (Reason: DeepL-API returned no translation)'` ); return result; @@ -45,7 +48,7 @@ function translateSelections(selections: vscode.Selection[], translateParam: Tra if (selection && translation) { let replacement = translation.text; - if (below) { + if (translateBelow) { const originalText = vscode.window.activeTextEditor?.document.getText(selection); replacement = `${originalText}\n${translation.text}`; } @@ -61,8 +64,8 @@ function translateSelections(selections: vscode.Selection[], translateParam: Tra }); } -function createTranslateCommand(param: TranslateCommandParam) { - const { askForTargetLang, askForSourceLang, below } = param; +function createTranslateCommand(request: { askForTargetLang: boolean, askForSourceLang: boolean, translateBelow: boolean }) { + const { askForTargetLang, askForSourceLang, translateBelow } = request; return async function () { if (!state.apiKey) { await configureSettings(); @@ -89,7 +92,7 @@ function createTranslateCommand(param: TranslateCommandParam) { } if (state.targetLanguage === state.sourceLanguage) { - state.sourceLanguage = null; + state.sourceLanguage = undefined; } const selections = vscode.window.activeTextEditor?.selections?.filter(selection => !selection.isEmpty); @@ -97,19 +100,21 @@ function createTranslateCommand(param: TranslateCommandParam) { return; } - const translateParam: TranslateParam = { - targetLang: state.targetLanguage, - sourceLang: sourceLang ?? undefined, - below - }; - translateSelections(selections, translateParam); + await translateSelections( + selections, + { + targetLang: state.targetLanguage, + sourceLang: sourceLang ?? null, + translateBelow, + } + ); }; } -export const translate = createTranslateCommand({ askForTargetLang: false, askForSourceLang: false, below: false }); -export const translateTo = createTranslateCommand({ askForTargetLang: true, askForSourceLang: false, below: false }); -export const translateFromTo = createTranslateCommand({ askForTargetLang: true, askForSourceLang: true, below: false }); -export const translateBelow = createTranslateCommand({ askForTargetLang: false, askForSourceLang: false, below: true }); +export const translate = createTranslateCommand({ askForTargetLang: false, askForSourceLang: false, translateBelow: false }); +export const translateTo = createTranslateCommand({ askForTargetLang: true, askForSourceLang: false, translateBelow: false }); +export const translateFromTo = createTranslateCommand({ askForTargetLang: true, askForSourceLang: true, translateBelow: false }); +export const translateBelow = createTranslateCommand({ askForTargetLang: false, askForSourceLang: false, translateBelow: true }); export const configureSettings = async () => { state.apiKey = await showApiKeyInput(); if (!state.apiKey) { diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..562d8cc --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,23 @@ +export const DEEPL_CONFIGURATION_SECTION = 'deepl'; + +export const CONFIG_API_KEY = `${DEEPL_CONFIGURATION_SECTION}.apiKey`; +export const CONFIG_FORMALITY = `${DEEPL_CONFIGURATION_SECTION}.formality`; +export const CONFIG_IGNORE_TAGS = `${DEEPL_CONFIGURATION_SECTION}.ignoreTags`; +export const CONFIG_TAG_HANDLING = `${DEEPL_CONFIGURATION_SECTION}.tagHandling`; +export const CONFIG_SPLITTING_TAGS = `${DEEPL_CONFIGURATION_SECTION}.splittingTags`; +export const CONFIG_SPLIT_SENTENCES = `${DEEPL_CONFIGURATION_SECTION}.splitSentences`; +export const CONFIG_NON_SPLITTING_TAGS = `${DEEPL_CONFIGURATION_SECTION}.nonSplittingTags`; +export const CONFIG_PRESERVE_FORMATTING = `${DEEPL_CONFIGURATION_SECTION}.preserveFormatting`; +export const CONFIG_GLOSSARY_ID = `${DEEPL_CONFIGURATION_SECTION}.glossaryId`; +export const CONFIG_DEFAULT_TARGET_LANGUAGE = `${DEEPL_CONFIGURATION_SECTION}.defaultTargetLanguage`; +export const CONFIG_DEFAULT_SOURCE_LANGUAGE = `${DEEPL_CONFIGURATION_SECTION}.defaultSourceLanguage`; +export const WORKSPACE_TARGET_LANGUAGE = `${DEEPL_CONFIGURATION_SECTION}.targetLanguage`; +export const WORKSPACE_SOURCE_LANGUAGE = `${DEEPL_CONFIGURATION_SECTION}.sourceLanguage`; + +export const EXTENSION_IDENTIFIER = 'soerenuhrbach.vscode-deepl'; + +export const COMMAND_CONFIGURE = 'deepl.configure'; +export const COMMAND_TRANSLATE = 'deepl.translate'; +export const COMMAND_TRANSLATE_TO = 'deepl.translateTo'; +export const COMMAND_TRANSLATE_FROM_TO = 'deepl.translateFromTo'; +export const COMMAND_TRANSLATE_BELOW = 'deepl.translateBelow'; \ No newline at end of file diff --git a/src/deepl.ts b/src/deepl.ts index 7eb7c11..5927be7 100644 --- a/src/deepl.ts +++ b/src/deepl.ts @@ -1,134 +1,64 @@ -import axios from 'axios'; +import * as vscode from 'vscode'; import { state } from './state'; -import { Language, LanguageType, Translation } from './types'; -import { isFreeAccountAuthKey } from 'deepl-node'; - -/* eslint-disable */ -export enum DeepLErrorCodes { - BAD_REQUEST = 400, - AUTHORIZATION_FAILED = 403, - NOT_FOUND = 404, - REQUEST_SIZE_EXCEEDED = 413, - URL_TOO_LONG = 414, - TOO_MANY_REQUEST_4XX = 429, - CHARACTER_LIMIT_REACHED = 456, - INTERNAL_SERVER_ERROR = 500, - RESOURCE_UNAVAILABLE = 503, - TOO_MANY_REQUEST_5XX = 529, -} -/* eslint-enable */ - -export class DeepLException extends Error { - public readonly code: DeepLErrorCodes; - - constructor(code: DeepLErrorCodes) { - super(); - this.code = code; - } - - public static createFromStatusCodeAndMessage(code: number, message: string) { - const exception = new DeepLException(code); - exception.name = DeepLException.name; - switch (code) { - case DeepLErrorCodes.AUTHORIZATION_FAILED: - exception.message = "Authentication failed. Please check your DeepL API key. You may be using the DeepL Pro API by mistake."; - break; - - case DeepLErrorCodes.REQUEST_SIZE_EXCEEDED: - exception.message = "Please try again with a shorter text"; - break; - - case DeepLErrorCodes.TOO_MANY_REQUEST_4XX: - case DeepLErrorCodes.TOO_MANY_REQUEST_5XX: - exception.message = "You have done too many translations recently. Please try again later."; - break; - - case DeepLErrorCodes.CHARACTER_LIMIT_REACHED: - exception.message = "You have reached the maximum character limit. You can see your usage [here](https://www.deepl.com/pro-account/usage)"; - break; - - default: - exception.message = "Unfortunately, the DeepL API cannot accept any requests at the moment. Please try again later. (" + message + ")"; - break; +import { Language, SourceLanguageCode, TargetLanguageCode, TextResult, Translator } from 'deepl-node'; +import { EXTENSION_IDENTIFIER } from './constants'; + +const cache = { + targetLanguages: [] as readonly Language[], + sourceLanguages: [] as readonly Language[], +}; + +const createTranslator = (apiKey: string) => { + const extension = vscode.extensions.getExtension(EXTENSION_IDENTIFIER)?.packageJSON; + const appName = extension?.name; + const appVersion = extension?.version; + + return new Translator(apiKey, { + appInfo: { + appName: appName, + appVersion: appVersion + }, + sendPlatformInfo: false + }); +}; + +export function translate(texts: T, sourceLang: SourceLanguageCode | null, targetLang: TargetLanguageCode): Promise { + const translator = createTranslator(state.apiKey!); + return translator.translateText( + texts, + sourceLang ?? null, + targetLang, + { + formality: state.formality || undefined, + glossary: state.glossaryId || undefined, + ignoreTags: state.ignoreTags || undefined, + nonSplittingTags: state.nonSplittingTags || undefined, + splittingTags: state.splittingTags || undefined, + preserveFormatting: state.preserveFormatting || undefined, + splitSentences: state.splitSentences || undefined, + tagHandling: state.tagHandling || undefined } - return exception; - } + ); } -type ErrorHandler = (e: DeepLException) => void; - -const http = axios.create(); -const errorHandlers: ErrorHandler[] = []; - -http.interceptors.request.use((config) => { - config.baseURL = isFreeAccountAuthKey(state.apiKey!) - ? 'https://api-free.deepl.com' - : 'https://api.deepl.com'; - - if (!config.params) { - config.params = {}; - } - - config.headers.Authorization = `DeepL-Auth-Key ${state.apiKey}`; - - if (config.url!.includes('translate')) { - config.params.split_sentences = state.splitSentences; - config.params.preserve_formatting = state.preserveFormatting ? "1" : "0"; - - if (config.params.source_lang) { - config.params.glossary_id = state.glossaryId; - } - - const formalityAllowed: string[] = state.languages.target - .filter(x => x.supports_formality) - .map(x => x.language); - if (config.params.target_lang && formalityAllowed.includes(config.params.target_lang.toUpperCase())) { - config.params.formality = state.formality; - } - - if (state.tagHandling !== 'off') { - config.params.tag_handling = state.tagHandling; - config.params.ignore_tags = state.ignoreTags; - config.params.splitting_tags = state.splittingTags; - config.params.non_splitting_tags = state.nonSplittingTags; - } +export async function getTargetLanguages() { + if (cache.targetLanguages.length > 0) { + return cache.targetLanguages; } - return config; -}); - -http.interceptors.response.use( - res => res, - e => { - if (!e.response) { - throw e; - } + const translator = createTranslator(state.apiKey!); + const languages = await translator.getTargetLanguages(); + cache.targetLanguages = languages; + return languages; +} - const exception = DeepLException.createFromStatusCodeAndMessage(e.response.status, e.response.data.message); - for (const handler of errorHandlers) { - handler(exception); - } - throw exception; +export async function getSourceLanguages() { + if (cache.sourceLanguages.length > 0) { + return cache.sourceLanguages; } -); - -export async function translate(text: string, targetLanguage: string, sourceLanguage?: string): Promise { - const response = await http.post('/v2/translate', null, { - /* eslint-disable */ - params: { - text: text, - target_lang: targetLanguage, - source_lang: sourceLanguage - } - /* eslint-enable */ - }); - - return response.data.translations as Translation[]; -} - -export async function languages(type: LanguageType = 'source'): Promise { - const response = await http.get('/v2/languages', { params: { type } }); - return response.data as Language[]; -} -export const addErrorHandler = (handler: ErrorHandler) => errorHandlers.push(handler); + const translator = createTranslator(state.apiKey!); + const languages = await translator.getSourceLanguages(); + cache.sourceLanguages = languages; + return languages; +} \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index a9081d5..15346f7 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,20 +1,18 @@ -import * as deepl from './deepl'; import * as vscode from 'vscode'; import { setup } from './state'; import { createStatusBarItem } from './status-bar'; import { configureSettings, translate, translateFromTo, translateTo, translateBelow } from './commands'; +import { COMMAND_CONFIGURE, COMMAND_TRANSLATE, COMMAND_TRANSLATE_BELOW, COMMAND_TRANSLATE_FROM_TO, COMMAND_TRANSLATE_TO } from './constants'; export async function activate(context: vscode.ExtensionContext) { - deepl.addErrorHandler(e => vscode.window.showErrorMessage(e.message)); - await setup(context); context.subscriptions.push(createStatusBarItem()); - context.subscriptions.push(vscode.commands.registerCommand('deepl.configure', configureSettings)); - context.subscriptions.push(vscode.commands.registerCommand('deepl.translate', translate)); - context.subscriptions.push(vscode.commands.registerCommand('deepl.translateTo', translateTo)); - context.subscriptions.push(vscode.commands.registerCommand('deepl.translateFromTo', translateFromTo)); - context.subscriptions.push(vscode.commands.registerCommand('deepl.translateBelow', translateBelow)); + context.subscriptions.push(vscode.commands.registerCommand(COMMAND_CONFIGURE, configureSettings)); + context.subscriptions.push(vscode.commands.registerCommand(COMMAND_TRANSLATE, translate)); + context.subscriptions.push(vscode.commands.registerCommand(COMMAND_TRANSLATE_TO, translateTo)); + context.subscriptions.push(vscode.commands.registerCommand(COMMAND_TRANSLATE_FROM_TO, translateFromTo)); + context.subscriptions.push(vscode.commands.registerCommand(COMMAND_TRANSLATE_BELOW, translateBelow)); } export function deactivate() {} diff --git a/src/helper.ts b/src/helper.ts index 0d69141..5437518 100644 --- a/src/helper.ts +++ b/src/helper.ts @@ -1,11 +1,13 @@ import * as vscode from 'vscode'; +import { CONFIG_DEFAULT_TARGET_LANGUAGE, CONFIG_DEFAULT_SOURCE_LANGUAGE } from './constants'; +import { SourceLanguageCode, TargetLanguageCode } from 'deepl-node'; -export function getDefaultTargetLanguage(config?: vscode.WorkspaceConfiguration): string | null { - config = config ?? vscode.workspace.getConfiguration('deepl'); - return config.get('defaultTargetLanguage', null); +export function getDefaultTargetLanguage(config?: vscode.WorkspaceConfiguration): TargetLanguageCode | undefined { + config = config ?? vscode.workspace.getConfiguration(); + return config.get(CONFIG_DEFAULT_TARGET_LANGUAGE); } -export function getDefaultSourceLanguage(config?: vscode.WorkspaceConfiguration): string | null { - config = config ?? vscode.workspace.getConfiguration('deepl'); - return config.get('defaultSourceLanguage', null); +export function getDefaultSourceLanguage(config?: vscode.WorkspaceConfiguration): SourceLanguageCode | undefined { + config = config ?? vscode.workspace.getConfiguration(); + return config.get(CONFIG_DEFAULT_SOURCE_LANGUAGE); } \ No newline at end of file diff --git a/src/inputs.ts b/src/inputs.ts index a6fa1a2..866d098 100644 --- a/src/inputs.ts +++ b/src/inputs.ts @@ -1,24 +1,21 @@ import * as vscode from 'vscode'; -import { LanguageType } from './types'; -import { state } from './state'; +import * as deepl from './deepl'; +import { Language, SourceLanguageCode, TargetLanguageCode } from 'deepl-node'; -async function showLanguageInput(options: vscode.QuickPickOptions, type: LanguageType): Promise { - const languages = state.languages[type] - .map(language => ({ - label: language.name, - description: language.language - })); - - return vscode.window.showQuickPick(languages, options) - .then(item => item?.description ?? null); +function showLanguageInput(options: vscode.QuickPickOptions, languages: readonly Language[]): Thenable { + const items = languages.map(x => ({ label: x.name, description: x.code })); + return vscode.window.showQuickPick(items, options) + .then(item => item?.description as T | undefined); } -export function showTargetLanguageInput() { - return showLanguageInput({ placeHolder: 'Select the language you want to translate into' }, 'target'); +export async function showTargetLanguageInput() { + const languages = await deepl.getTargetLanguages(); + return showLanguageInput({ placeHolder: 'Select the language you want to translate into' }, languages); } -export function showSourceLanguageInput() { - return showLanguageInput({ placeHolder: 'Select the language you want to translate from' }, 'source'); +export async function showSourceLanguageInput() { + const languages = await deepl.getSourceLanguages(); + return showLanguageInput({ placeHolder: 'Select the language you want to translate from' }, languages); } export function showApiKeyInput() { @@ -26,5 +23,5 @@ export function showApiKeyInput() { title: 'Please enter your DeepL API key', placeHolder: 'Please enter your DeepL API key', ignoreFocusOut: true - }).then(x => x ?? null); + }).then(x => x ?? undefined); } \ No newline at end of file diff --git a/src/state.ts b/src/state.ts index 06e4038..a93e58b 100644 --- a/src/state.ts +++ b/src/state.ts @@ -1,100 +1,140 @@ import * as vscode from 'vscode'; -import * as debug from './debug'; import * as deepl from './deepl'; -import { ExtensionState } from './types'; +import * as debug from './debug'; +import type { ExtensionState } from './types'; import { reactive, effect, ref } from '@vue/reactivity'; import { getDefaultSourceLanguage, getDefaultTargetLanguage } from './helper'; +import { + DEEPL_CONFIGURATION_SECTION, + CONFIG_API_KEY, + CONFIG_FORMALITY, + CONFIG_IGNORE_TAGS, + CONFIG_TAG_HANDLING, + CONFIG_SPLITTING_TAGS, + CONFIG_SPLIT_SENTENCES, + CONFIG_NON_SPLITTING_TAGS, + CONFIG_PRESERVE_FORMATTING, + CONFIG_GLOSSARY_ID, + WORKSPACE_TARGET_LANGUAGE, + WORKSPACE_SOURCE_LANGUAGE +} from './constants'; +import { SourceLanguageCode, TargetLanguageCode } from 'deepl-node'; const initialized = ref(false); export const state = reactive({ - targetLanguage: null, - sourceLanguage: null, - apiKey: null, - tagHandling: "off", - ignoreTags: "", - nonSplittingTags: "", - splittingTags: "", - preserveFormatting: false, - formality: "default", - splitSentences: "1", - languages: { - source: [], - target: [] - }, - glossaryId: "" + apiKey: undefined, + targetLanguage: undefined, + sourceLanguage: undefined, + tagHandling: undefined, + ignoreTags: undefined, + nonSplittingTags: undefined, + splittingTags: undefined, + preserveFormatting: undefined, + formality: undefined, + splitSentences: undefined, + glossaryId: undefined }); -const fillStateFromConfig = async (config: vscode.WorkspaceConfiguration, context: vscode.ExtensionContext) => { - state.apiKey = config.get('apiKey') ?? null; +const loadExtensionState = async (config: vscode.WorkspaceConfiguration, context: vscode.ExtensionContext) => { + debug.write('Loading extension state...'); + state.apiKey = await context.secrets.get(CONFIG_API_KEY); - if (state.languages.source.length < 1 && !!state.apiKey) { - state.languages.source = await deepl.languages('source'); - } - if (state.languages.target.length < 1 && !!state.apiKey) { - state.languages.target = await deepl.languages('target'); - } + const sourceLanguages = await deepl.getSourceLanguages(); + const targetLanguages = await deepl.getTargetLanguages(); - state.formality = config.get('formality') ?? "default"; - state.ignoreTags = config.get('ignoreTags') ?? ""; - state.tagHandling = config.get('tagHandling') ?? "off"; - state.splittingTags = config.get('splittingTags') ?? ""; - state.splitSentences = config.get('splitSentences') ?? "1"; - state.nonSplittingTags = config.get('nonSplittingTags') ?? ""; - state.preserveFormatting = config.get('preserveFormatting') ?? false; - state.glossaryId = config.get('glossaryId') ?? ""; - - const targetLanguage = context.workspaceState.get('deepl:targetLanguage') ?? getDefaultTargetLanguage(config); - state.targetLanguage = targetLanguage && state.languages.target.map(x => x.language.toLowerCase()).includes(targetLanguage.toLowerCase()) + state.formality = config.get(CONFIG_FORMALITY) ?? undefined; + state.glossaryId = config.get(CONFIG_GLOSSARY_ID) ?? undefined; + state.ignoreTags = config.get(CONFIG_IGNORE_TAGS) ?? undefined; + state.tagHandling = config.get(CONFIG_TAG_HANDLING) ?? undefined; + state.splittingTags = config.get(CONFIG_SPLITTING_TAGS) ?? undefined; + state.splitSentences = config.get(CONFIG_SPLIT_SENTENCES) ?? undefined; + state.nonSplittingTags = config.get(CONFIG_NON_SPLITTING_TAGS) ?? undefined; + state.preserveFormatting = config.get(CONFIG_PRESERVE_FORMATTING) ?? undefined; + + const targetLanguage = context.workspaceState.get(WORKSPACE_TARGET_LANGUAGE) ?? getDefaultTargetLanguage(config); + state.targetLanguage = targetLanguage && targetLanguages.map(x => x.code.toLowerCase()).includes(targetLanguage.toLowerCase()) ? targetLanguage - : null; + : undefined; - const sourceLanguage = context.workspaceState.get('deepl:sourceLanguage') ?? getDefaultSourceLanguage(config); - state.sourceLanguage = sourceLanguage && state.languages.source.map(x => x.language.toLowerCase()).includes(sourceLanguage.toLowerCase()) + const sourceLanguage = context.workspaceState.get(WORKSPACE_SOURCE_LANGUAGE) ?? getDefaultSourceLanguage(config); + state.sourceLanguage = sourceLanguage && sourceLanguages.map(x => x.code.toLowerCase()).includes(sourceLanguage.toLowerCase()) ? sourceLanguage - : null; + : undefined; + + debug.write(`Loaded extension state:`); + debug.write(JSON.stringify(state, null, 2)); }; -export async function setup(context: vscode.ExtensionContext) { - if (initialized.value) { +const migrateApiKeyFromConfigToSecrets = async (config: vscode.WorkspaceConfiguration, context: vscode.ExtensionContext) => { + const apiKeyFromConfiguration = config.get(CONFIG_API_KEY); + if (!apiKeyFromConfiguration) { return; } - initialized.value = true; + await context.secrets.store(CONFIG_API_KEY, apiKeyFromConfiguration); + config.update(CONFIG_API_KEY, apiKeyFromConfiguration, vscode.ConfigurationTarget.Global); + debug.write('Moved api key from configuration to secret store.'); +}; - const config = vscode.workspace.getConfiguration('deepl'); +const migrateWorkspaceStates = async (context: vscode.ExtensionContext) => { + const sourceLanguageToMigrate = context.workspaceState.get('deepl:sourceLanguage'); + if (sourceLanguageToMigrate) { + context.workspaceState.update(WORKSPACE_SOURCE_LANGUAGE, sourceLanguageToMigrate); + context.workspaceState.update('deepl:sourceLanguage', undefined); + debug.write('Moved source language to new workspace state key'); + } - await fillStateFromConfig(config, context); + const targetLanguageToMigrate = context.workspaceState.get('deepl:targetLanguage'); + if (targetLanguageToMigrate) { + context.workspaceState.update(WORKSPACE_TARGET_LANGUAGE, sourceLanguageToMigrate); + context.workspaceState.update('deepl:targetLanguage', undefined); + debug.write('Moved target language to new workspace state key'); + } +}; - debug.write(`Initialized extension using state:`); - debug.write(JSON.stringify(state, null, 2)); +export async function setup(context: vscode.ExtensionContext) { + if (initialized.value) { + return; + } - effect(() => config.update('apiKey', state.apiKey, vscode.ConfigurationTarget.Global)); - effect(() => config.update('formality', state.formality, vscode.ConfigurationTarget.Global)); - effect(() => config.update('ignoreTags', state.ignoreTags, vscode.ConfigurationTarget.Global)); - effect(() => config.update('tagHandling', state.tagHandling, vscode.ConfigurationTarget.Global)); - effect(() => config.update('splittingTags', state.splittingTags, vscode.ConfigurationTarget.Global)); - effect(() => config.update('nonSplittingTags', state.nonSplittingTags, vscode.ConfigurationTarget.Global)); - effect(() => config.update('splitSentences', state.splitSentences, vscode.ConfigurationTarget.Global)); - effect(() => config.update('preserveFormatting', state.preserveFormatting, vscode.ConfigurationTarget.Global)); - effect(() => config.update('glossaryId', state.glossaryId, vscode.ConfigurationTarget.Global)); - effect(() => context.workspaceState.update('deepl:targetLanguage', state.targetLanguage)); - effect(() => context.workspaceState.update('deepl:sourceLanguage', state.sourceLanguage)); + initialized.value = true; - const configurationChangeListener = vscode.workspace.onDidChangeConfiguration(e => { - if (!e.affectsConfiguration('deepl')) { + const config = vscode.workspace.getConfiguration(); + + await migrateWorkspaceStates(context); + await migrateApiKeyFromConfigToSecrets(config, context); + await loadExtensionState(config, context); + + effect(() => state.apiKey ? context.secrets.store(CONFIG_API_KEY, state.apiKey) : context.secrets.delete(CONFIG_API_KEY)); + effect(() => config.update(CONFIG_FORMALITY, state.formality, vscode.ConfigurationTarget.Global)); + effect(() => config.update(CONFIG_IGNORE_TAGS, state.ignoreTags, vscode.ConfigurationTarget.Global)); + effect(() => config.update(CONFIG_TAG_HANDLING, state.tagHandling, vscode.ConfigurationTarget.Global)); + effect(() => config.update(CONFIG_SPLITTING_TAGS, state.splittingTags, vscode.ConfigurationTarget.Global)); + effect(() => config.update(CONFIG_NON_SPLITTING_TAGS, state.nonSplittingTags, vscode.ConfigurationTarget.Global)); + effect(() => config.update(CONFIG_SPLIT_SENTENCES, state.splitSentences, vscode.ConfigurationTarget.Global)); + effect(() => config.update(CONFIG_PRESERVE_FORMATTING, state.preserveFormatting, vscode.ConfigurationTarget.Global)); + effect(() => config.update(CONFIG_GLOSSARY_ID, state.glossaryId, vscode.ConfigurationTarget.Global)); + effect(() => context.workspaceState.update(WORKSPACE_TARGET_LANGUAGE, state.targetLanguage)); + effect(() => context.workspaceState.update(WORKSPACE_SOURCE_LANGUAGE, state.sourceLanguage)); + + const secretChangeListener = context.secrets.onDidChange((e) => { + if (e.key !== CONFIG_API_KEY) { return; } - debug.write(`Extension configuration has changed! Updating extension state...`); - - const config = vscode.workspace.getConfiguration('deepl'); + debug.write(`Secret (${CONFIG_API_KEY}) has been changed!`); + loadExtensionState(config, context); + }); - fillStateFromConfig(config, context); + const configurationChangeListener = vscode.workspace.onDidChangeConfiguration(e => { + if (!e.affectsConfiguration(DEEPL_CONFIGURATION_SECTION)) { + return; + } - debug.write(`Updated extension state to:`); - debug.write(JSON.stringify(state, null, 2)); + debug.write(`Extension configuration has been changed!`); + loadExtensionState(config, context); }); - context.subscriptions.push(configurationChangeListener); + context.subscriptions.push(secretChangeListener, configurationChangeListener); } \ No newline at end of file diff --git a/src/status-bar.ts b/src/status-bar.ts index 96307ff..39a1430 100644 --- a/src/status-bar.ts +++ b/src/status-bar.ts @@ -2,108 +2,36 @@ import * as vscode from 'vscode'; import * as deepl from './deepl'; import { state } from './state'; import { effect } from '@vue/reactivity'; -import { createMachine, assign, interpret } from 'xstate'; - -type StatusBarEvent = - { type: 'SET_TARGET_LANGUAGE'; value: string | null } | - { type: 'SET_API_KEY', value: string | null }; - -type StatusBarContext = { - apiKey: string | null; - targetLanguage: string | null; -}; - -export function createStatusBarStateMaschine() { - return createMachine({ - id: 'statusBar', - context: { - apiKey: null, - targetLanguage: null, - }, - initial: 'missingApiKey', - states: { - missingApiKey: {}, - configuredApiKey: { - initial: 'noTargetLanguageSelected', - states: { - targetLanguageSelected: {}, - noTargetLanguageSelected: {} - }, - on: { - /* eslint-disable-next-line */ - SET_TARGET_LANGUAGE: [ - { - target: 'configuredApiKey.targetLanguageSelected', - actions: assign({ - targetLanguage: (_, event) => event.value - }), - cond: 'valueIsNotNullOrEmpty' - }, - { - target: 'configuredApiKey.noTargetLanguageSelected' - } - ] - } - }, - }, - on: { - /* eslint-disable-next-line */ - SET_API_KEY: [ - { - target: 'configuredApiKey', - actions: assign({ - apiKey: (_, event) => event.value - }), - cond: 'valueIsNotNullOrEmpty' - }, - { - target: 'missingApiKey' - } - ] - } - }, - { - guards: { - valueIsNotNullOrEmpty: (_, event) => !!event.value - } - }); -} +import { COMMAND_CONFIGURE, COMMAND_TRANSLATE_TO } from './constants'; export function createStatusBarItem() { const statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 10); - const maschine = createStatusBarStateMaschine(); - const service = interpret(maschine) - .onTransition(async (xstate) => { - const prefix = '$(globe) '; + effect(async () => { + const prefix = '$(globe) '; - if (xstate.matches('missingApiKey')) { - statusBarItem.text = prefix + 'Set your DeepL API key'; - statusBarItem.command = 'deepl.configure'; - statusBarItem.tooltip = 'Set your DeepL API key'; - } - - if (xstate.matches('configuredApiKey')) { - statusBarItem.command = 'deepl.translateTo'; - statusBarItem.tooltip = 'Select the language you want to translate into'; - } + if (!state.apiKey) { + statusBarItem.text = prefix + 'Set your DeepL API key'; + statusBarItem.command = COMMAND_CONFIGURE; + statusBarItem.tooltip = 'Set your DeepL API key'; + } - if (xstate.matches('configuredApiKey.noTargetLanguageSelected')) { - statusBarItem.text = prefix + 'Select language'; - } + if (state.apiKey) { + statusBarItem.command = COMMAND_TRANSLATE_TO; + statusBarItem.tooltip = 'Select the language you want to translate into'; + } - if (xstate.matches('configuredApiKey.targetLanguageSelected')) { - const lang = xstate.context.targetLanguage; - const languages = await deepl.languages('target'); - const selectedLanguage = languages.find(x => x.language === lang); - statusBarItem.text = prefix + (selectedLanguage?.name ?? xstate.context.targetLanguage); - } - }) - .start(); + if (state.apiKey && !state.targetLanguage) { + statusBarItem.command = COMMAND_TRANSLATE_TO; + statusBarItem.text = prefix + 'Select language'; + } - effect(() => { - service.send({ type: 'SET_API_KEY', value: state.apiKey }); - service.send({ type: 'SET_TARGET_LANGUAGE', value: state.targetLanguage }); + if (state.apiKey && state.targetLanguage) { + const languages = await deepl.getTargetLanguages(); + const selectedLanguage = languages.find(x => x.code.toLocaleLowerCase() === state.targetLanguage!.toLocaleLowerCase()); + statusBarItem.command = COMMAND_TRANSLATE_TO; + statusBarItem.text = prefix + (selectedLanguage?.name ?? state.targetLanguage); + } }); statusBarItem.show(); diff --git a/src/types.ts b/src/types.ts index accae55..977d6a1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,44 +1,15 @@ -export interface Language { - language: string; - name: string; - /* eslint-disable-next-line */ - supports_formality?: boolean; -} - -export interface Translation { - /* eslint-disable-next-line */ - detected_source_language: string; - text: string; -} - -export type LanguageType = 'source' | 'target'; +import type { Formality, GlossaryId, SentenceSplittingMode, SourceLanguageCode, TagHandlingMode, TagList, TargetLanguageCode } from "deepl-node"; export interface ExtensionState { - targetLanguage: string | null, - sourceLanguage: string | null, - apiKey: string | null, - tagHandling: 'html' | 'xml' | 'off', - ignoreTags: string, - formality: string, - nonSplittingTags: string, - splittingTags: string, - splitSentences: string, - preserveFormatting: boolean, - languages: { - source: Language[], - target: Language[] - }, - glossaryId: string -} - -export interface TranslateCommandParam { - askForTargetLang: boolean - askForSourceLang: boolean - below: boolean -} - -export interface TranslateParam { - targetLang: string - sourceLang?: string - below: boolean -} + apiKey: string | undefined, + targetLanguage?: TargetLanguageCode, + sourceLanguage?: SourceLanguageCode, + tagHandling?: TagHandlingMode, + ignoreTags?: TagList, + formality?: Formality, + nonSplittingTags?: TagList, + splittingTags?: TagList, + splitSentences?: SentenceSplittingMode, + preserveFormatting?: boolean, + glossaryId?: GlossaryId +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 148e8f8..170bf4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -565,7 +565,7 @@ asynckit@^0.4.0: resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== -"axios@>=0.21.2 <1.2.0 || >=1.2.2", axios@^1.6.0: +"axios@>=0.21.2 <1.2.0 || >=1.2.2": version "1.6.5" resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.5.tgz#2c090da14aeeab3770ad30c3a1461bc970fb0cd8" integrity sha512-Ii012v05KEVuUoFWmMW/UQv9aRIc3ZwkWDcM+h5Il8izZCtRVpDUfwpoFf7eOtajT3QiGR4yDUx7lPqHJULgbg== @@ -2169,11 +2169,6 @@ wrappy@1: resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== -xstate@4.23.0: - version "4.23.0" - resolved "https://registry.yarnpkg.com/xstate/-/xstate-4.23.0.tgz#af0bd09c726dccedf16f8ba30d0d1efff7af5207" - integrity sha512-YIKb7thsDfpb6ooWJJuj+UnNZq923dG264zfpS2/vi4dkZz41ugO0ktC6QCBDeMfH8LBHhhqZ06sR4AYgWWnWg== - yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72"