Skip to content

Commit

Permalink
feat(providers): translation providers from extensions (#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
LittleSound authored Jul 12, 2024
1 parent 4d2501d commit a9f9aeb
Show file tree
Hide file tree
Showing 10 changed files with 184 additions and 17 deletions.
3 changes: 2 additions & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/controller/translateSelected.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -24,6 +25,7 @@ export async function activate(extCtx: vscode.ExtensionContext) {

registerConfig(ctx)
await RegisterGrammar(ctx)
registerExtensionTranslate(ctx)
RegisterControllers(ctx)
registerEntryButton(ctx)
}
Expand Down
2 changes: 1 addition & 1 deletion src/model/translator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
141 changes: 141 additions & 0 deletions src/providers/tranlations/extensions.ts
Original file line number Diff line number Diff line change
@@ -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<any>
}

export interface ITranslateRegistry {
(translation: string, translate: new () => any): void
}

const translateConfig: Map<string, ITranslateExtensionConfig> = 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<string>()
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<TranslationResult> {
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
})
10 changes: 8 additions & 2 deletions src/providers/tranlations/index.ts
Original file line number Diff line number Diff line change
@@ -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))
1 change: 1 addition & 0 deletions src/providers/tranlations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface TranslationProviderInfo {
name: string
label: string
supportLanguage: Record<string, string | undefined>
maxLen?: number
needs: { config_key: string; place_hold: string }[]
translate: (options: TranslationParameters) => Promise<TranslationResult>
}
2 changes: 1 addition & 1 deletion src/view/quickInput/setLanguagePopmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
36 changes: 26 additions & 10 deletions src/view/quickInput/setTranslationService.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -11,33 +11,49 @@ 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 = [
QuickInputButtons.Back,
]
quickPick.onDidTriggerButton((button) => {
if (button === QuickInputButtons.Back)
showTranslatePopmenu(ctx)
quickPick.hide()
})

quickPick.onDidHide(() => {
quickPick.dispose()
showTranslatePopmenu(ctx)
if (!notGoingHome)
showTranslatePopmenu(ctx)
})
quickPick.show()
}
2 changes: 1 addition & 1 deletion src/view/quickInput/translatePopmenu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
{
Expand Down

0 comments on commit a9f9aeb

Please sign in to comment.