diff --git a/.eslintrc b/.eslintrc index ad6f582..3c1a9fa 100644 --- a/.eslintrc +++ b/.eslintrc @@ -12,7 +12,8 @@ "src/generated-meta.ts" ], "rules": { - "no-console": ["warn", { "allow": ["warn", "error"] }] + "no-console": ["warn", { "allow": ["warn", "error"] }], + "no-void": "off" // "@typescript-eslint/naming-convention": "warn", // "@typescript-eslint/semi": "warn", diff --git a/src/controller/translateSelected.ts b/src/controller/translateSelected.ts index cd21320..f81c630 100644 --- a/src/controller/translateSelected.ts +++ b/src/controller/translateSelected.ts @@ -26,7 +26,7 @@ export function RegisterTranslateSelectedText(ctx: Context) { const meta = useTranslationMeta() - const translator = translators[config.translator] + const translator = translators.value[config.translator] const res = await translate({ text: activeEditor.document.getText(range), from: meta.from as keyof typeof translator.supportLanguage, diff --git a/src/extension.ts b/src/extension.ts index 1a50fa9..7749876 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import { useExtensionContext } from './dependence' import { RegisterControllers } from './controller' import { registerEntryButton } from './view/statusBar' import { registerStore } from './store' +import { registerExtensionTranslate } from './providers/tranlations/extensions' import { createContext } from '~/context' import { RegisterGrammar } from '~/model/grammar' import { registerConfig } from '~/config' @@ -24,6 +25,7 @@ export async function activate(extCtx: vscode.ExtensionContext) { registerConfig(ctx) await RegisterGrammar(ctx) + registerExtensionTranslate(ctx) RegisterControllers(ctx) registerEntryButton(ctx) } diff --git a/src/model/translator.ts b/src/model/translator.ts index bb22145..7aa26c4 100644 --- a/src/model/translator.ts +++ b/src/model/translator.ts @@ -99,7 +99,7 @@ export async function translateDocument(ctx: Context, options: TranslateDocument if (!phrasesFromDoc.length) return - const translator = translators[config.translator] + const translator = translators.value[config.translator] const translationResult = await translator.translate({ text: phrasesFromDoc.join('\n'), from: from as keyof typeof translator.supportLanguage, diff --git a/src/providers/tranlations/extensions.ts b/src/providers/tranlations/extensions.ts new file mode 100644 index 0000000..46ad763 --- /dev/null +++ b/src/providers/tranlations/extensions.ts @@ -0,0 +1,141 @@ +import { computed, effect, reactive, shallowReactive } from '@vue/reactivity' +import { extensions } from 'vscode' +import type { TranslationParameters, TranslationProviderInfo, TranslationResult } from './types' +import type { Context } from '~/context' +import { invoke } from '~/utils' +import { config } from '~/config' + +export interface ITranslateExtensionConfig { + extensionId: string + title: string + category?: string + Ctor?: new () => any + translate: string + instance?: any + + promise?: Promise +} + +export interface ITranslateRegistry { + (translation: string, translate: new () => any): void +} + +const translateConfig: Map = reactive(new Map()) + +export const externalTranslators = computed(() => { + return Object.fromEntries(Array.from(translateConfig.entries()) + .map(([name, item]) => <[string, TranslationProviderInfo]>[name, { + name, + label: item.title, + needs: [], + supportLanguage: { + 'zh-CN': 'zh-CN', + }, + translate: options => translateWithConf(name, item, options), + }])) +}) + +// eslint-disable-next-line unused-imports/no-unused-vars +export function registerExtensionTranslate(ctx: Context) { + loadExtensionTranslate() + extensions.onDidChange(() => loadExtensionTranslate()) +} + +export function loadExtensionTranslate() { + const currentKeys = new Set() + extensions.all + .filter(ext => ext?.packageJSON?.contributes?.translates?.length > 0) + .forEach((extension) => { + const translates = extension.packageJSON.contributes.translates + + for (const { title, translate, category } of translates) { + if (!title || !translate) + return + const key = `${extension.id}-${translate}` + currentKeys.add(key) + + if (!translateConfig.get(key)) { + translateConfig.set(key, shallowReactive({ + extensionId: extension.id, + translate, + title, + category, + })) + } + } + }) + + for (const key of translateConfig.keys()) { + if (!currentKeys.has(key)) + translateConfig.delete(key) + } +} + +async function translateWithConf(name: string, conf: ITranslateExtensionConfig, { text, from, to }: TranslationParameters): Promise { + function msgPerfix(text: string) { + return `[Interline Translate] ${conf.title} (${name}) / ${text}` + } + + try { + if (!conf.instance) + await activateWithConf(name, conf) + } + catch (e) { + return { + ok: false, + message: msgPerfix('Activate Failed'), + originalError: e, + } + } + + try { + const res = await conf.instance.translate(text, { from, to }) + return { + ok: true, + text: res, + } + } + catch (e) { + return { + ok: false, + message: msgPerfix(typeof e === 'object' ? (e as any)?.message : 'Translate Failed: Unknown Error'), + originalError: e, + } + } +} + +async function activateWithConf(name: string, conf: ITranslateExtensionConfig) { + if (conf.promise) + return conf.promise + + const extension = extensions.all.find(extension => extension.id === conf.extensionId) + if (!extension) + return + try { + conf.promise = invoke(async () => { + await extension.activate() + if (!extension.exports || !extension.exports.extendTranslate) + throw new Error(`Invalid extension: ${name}`) + + await extension + .exports + .extendTranslate((_: any, Translate: new () => any) => { + conf.Ctor = Translate + conf.instance = new conf.Ctor() + }) + }) + await conf.promise + } + finally { + conf.promise = undefined + } +} + +// clear instance +let oldTranslator: string | undefined +effect(() => { + const name = config.translator + if (name !== oldTranslator && translateConfig.has(name)) + translateConfig.get(name)!.instance = undefined + oldTranslator = name +}) diff --git a/src/providers/tranlations/index.ts b/src/providers/tranlations/index.ts index b7c490f..6b8aa71 100644 --- a/src/providers/tranlations/index.ts +++ b/src/providers/tranlations/index.ts @@ -1,8 +1,14 @@ +import { computed } from '@vue/reactivity' import { info as googleInfo } from './google' import { info as bingInfo } from './bing' +import { externalTranslators } from './extensions' -export const translators = { +const builtInTranslators = { google: googleInfo, bing: bingInfo, } -export const translatorOptions = Object.values(translators) + +export const translators = computed(() => { + return Object.assign({}, builtInTranslators, externalTranslators.value) +}) +export const translatorOptions = computed(() => Object.values(translators.value)) diff --git a/src/providers/tranlations/types.ts b/src/providers/tranlations/types.ts index acd719c..ff26aa6 100644 --- a/src/providers/tranlations/types.ts +++ b/src/providers/tranlations/types.ts @@ -21,6 +21,7 @@ export interface TranslationProviderInfo { name: string label: string supportLanguage: Record + maxLen?: number needs: { config_key: string; place_hold: string }[] translate: (options: TranslationParameters) => Promise } diff --git a/src/view/quickInput/setLanguagePopmenu.ts b/src/view/quickInput/setLanguagePopmenu.ts index acb83e5..4d97f06 100644 --- a/src/view/quickInput/setLanguagePopmenu.ts +++ b/src/view/quickInput/setLanguagePopmenu.ts @@ -19,7 +19,7 @@ export function showSetLanguagePopmenu(ctx: Context, type: 'target' | 'source') const translatorName = config.translator || 'google' quickPick.items = languageOptions .filter(item => type === 'target' - ? translators[translatorName].supportLanguage[item.description!] + ? translators.value[translatorName].supportLanguage[item.description!] : item.description === 'en', ) .map((item) => { diff --git a/src/view/quickInput/setTranslationService.ts b/src/view/quickInput/setTranslationService.ts index 5e0fb90..d618f02 100644 --- a/src/view/quickInput/setTranslationService.ts +++ b/src/view/quickInput/setTranslationService.ts @@ -1,4 +1,4 @@ -import { QuickInputButtons, window } from 'vscode' +import { QuickInputButtons, commands, window } from 'vscode' import { defineQuickPickItems } from './utils' import { showTranslatePopmenu } from './translatePopmenu' import { config } from '~/config' @@ -11,20 +11,35 @@ export function showSetTranslationService(ctx: Context) { quickPick.title = 'Translation Service' quickPick.matchOnDescription = true quickPick.matchOnDetail = true - defineQuickPickItems(quickPick, translatorOptions.map(({ name, label }) => ({ + let notGoingHome = false + const moreItem = { + label: '$(extensions) More...', + description: 'Install more translate sources from Extensions Marketplace', + } + defineQuickPickItems(quickPick, translatorOptions.value.map(({ name, label }) => ({ label: name === config.translator ? `$(check) ${label}` : `$(array) ${label}`, description: name, - }))) + })).concat([moreItem])) - quickPick.onDidChangeSelection((selection) => { - window.showInformationMessage(`Selected service: ${selection[0].label}`) + quickPick.onDidChangeSelection(async (selection) => { const translatorName = selection[0].description - if (!translatorName || !(translatorName in translators)) { - window.showErrorMessage('Invalid service') + + // Search Plugin Marketplace + if (translatorName === moreItem.description) { + commands.executeCommand('workbench.extensions.search', '@tag:translateSource') + notGoingHome = true + quickPick.hide() + return + } + + if (!translatorName || !(translatorName in translators.value)) { + window.showErrorMessage(`Invalid service: ${translatorName}`) return } + + window.showInformationMessage(`Selected service: ${selection[0].label.split(') ')[1]}`) config.translator = translatorName as ConfigKeyTypeMap['interline-translate.translator'] - showTranslatePopmenu(ctx) + quickPick.hide() }) quickPick.buttons = [ @@ -32,12 +47,13 @@ export function showSetTranslationService(ctx: Context) { ] quickPick.onDidTriggerButton((button) => { if (button === QuickInputButtons.Back) - showTranslatePopmenu(ctx) + quickPick.hide() }) quickPick.onDidHide(() => { quickPick.dispose() - showTranslatePopmenu(ctx) + if (!notGoingHome) + showTranslatePopmenu(ctx) }) quickPick.show() } diff --git a/src/view/quickInput/translatePopmenu.ts b/src/view/quickInput/translatePopmenu.ts index 3297e70..5df6887 100644 --- a/src/view/quickInput/translatePopmenu.ts +++ b/src/view/quickInput/translatePopmenu.ts @@ -45,7 +45,7 @@ export function showTranslatePopmenu(ctx: Context) { }, { label: '$(cloud) Service:', - description: translators[config.translator]?.label || `Unsupported: ${config.translator}`, + description: translators.value[config.translator]?.label || `Unsupported: ${config.translator}`, callback: () => showSetTranslationService(ctx), }, {