diff --git a/package.json b/package.json index 8dbcf20..a9becb5 100644 --- a/package.json +++ b/package.json @@ -136,6 +136,12 @@ "default": "", "description": "Fallback language when the default language is not available." }, + "interline-translate.translator": { + "type": "string", + "default": "bing", + "enum": ["google", "bing"], + "description": "Translation service provider" + }, "interline-translate.googleTranslateProxy": { "type": "string", "default": "", diff --git a/src/controller/translateSelected.ts b/src/controller/translateSelected.ts index 7702741..cd21320 100644 --- a/src/controller/translateSelected.ts +++ b/src/controller/translateSelected.ts @@ -6,6 +6,8 @@ import { useExtensionContext } from '~/dependence' import { useTranslationMeta } from '~/model/translator' import { translate } from '~/providers/tranlations/google' import { displayOnGapLines } from '~/view' +import { config } from '~/config' +import { translators } from '~/providers/tranlations' export function RegisterTranslateSelectedText(ctx: Context) { const extCtx = useExtensionContext(ctx) @@ -24,10 +26,11 @@ export function RegisterTranslateSelectedText(ctx: Context) { const meta = useTranslationMeta() + const translator = translators[config.translator] const res = await translate({ text: activeEditor.document.getText(range), - from: meta.from as string as any, - to: meta.to as string as any, + from: meta.from as keyof typeof translator.supportLanguage, + to: meta.to as keyof typeof translator.supportLanguage, }) if (!res.ok) { diff --git a/src/model/translator.ts b/src/model/translator.ts index e2109de..bb22145 100644 --- a/src/model/translator.ts +++ b/src/model/translator.ts @@ -3,9 +3,9 @@ import type { TextDocument } from 'vscode' import { extractPhrases } from './extract' import { CommentScopes, StringScopes, findScopesRange, isComment, isKeyword, isString, parseDocumentToTokens } from '~/model/grammar' import { persistTranslationCache, useTranslationCache } from '~/model/cache' -import { translate } from '~/providers/tranlations/google' import { config } from '~/config' import type { Context } from '~/context' +import { translators } from '~/providers/tranlations' export function useTranslationMeta() { // TODO: use config or automatically recognize from language @@ -99,10 +99,11 @@ export async function translateDocument(ctx: Context, options: TranslateDocument if (!phrasesFromDoc.length) return - const translationResult = await translate({ + const translator = translators[config.translator] + const translationResult = await translator.translate({ text: phrasesFromDoc.join('\n'), - from: from as string as any, - to: to as string as any, + from: from as keyof typeof translator.supportLanguage, + to: to as keyof typeof translator.supportLanguage, }) if (!translationResult.ok) { diff --git a/src/providers/tranlations/bing.ts b/src/providers/tranlations/bing.ts new file mode 100644 index 0000000..4e841de --- /dev/null +++ b/src/providers/tranlations/bing.ts @@ -0,0 +1,141 @@ +import { FetchError, ofetch } from 'ofetch' +import type { TranslationParameters, TranslationProviderInfo, TranslationResult } from './types' +import { createUnsupportedLanguageError } from './utils' +import { config } from '~/config' + +export const info: TranslationProviderInfo = { + name: 'bing', + label: 'Bing Translate', + // https://learn.microsoft.com/zh-CN/azure/cognitive-services/translator/language-support + supportLanguage: { + 'auto': '', + 'zh-CN': 'zh-Hans', + 'zh-TW': 'zh-Hant', + 'yue': 'yue', + 'en': 'en', + 'ja': 'ja', + 'ko': 'ko', + 'fr': 'fr', + 'es': 'es', + 'ru': 'ru', + 'de': 'de', + 'it': 'it', + 'tr': 'tr', + 'pt-PT': 'pt-pt', + 'pt-BR': 'pt', + 'vi': 'vi', + 'id': 'id', + 'th': 'th', + 'ms': 'ms', + 'ar': 'ar', + 'hi': 'hi', + 'mn-Cyrl': 'mn-Cyrl', + 'mn-Mong': 'mn-Mong', + 'km': 'km', + 'nb-NO': 'nb', + 'fa': 'fa', + 'sv': 'sv', + 'pl': 'pl', + 'nl': 'nl', + 'uk': 'uk', + }, + needs: [], + translate, +} + +export type SupportLanguage = keyof typeof info.supportLanguage + +export interface BingTranslationParameters extends TranslationParameters { + from: SupportLanguage + to: SupportLanguage +} + +const tokenUrl = 'https://edge.microsoft.com/translate/auth' +const translatorUrl = 'https://api-edge.cognitive.microsofttranslator.com/translate' + +function msgPerfix(text: string) { + return `[Interline Translate] Bing / ${text}` +} + +export async function translate(options: BingTranslationParameters): Promise { + const { text, from, to } = options + const { supportLanguage } = info + + if (text === '') { + return { + ok: false, + message: 'Empty Text', + originalError: null, + } + } + + if (!(from in supportLanguage)) + return createUnsupportedLanguageError('from', from) + if (!(to in supportLanguage)) + return createUnsupportedLanguageError('to', to) + + try { + const tokenRes = await ofetch(`${config.corsProxy}${tokenUrl}`, { + method: 'GET', + }).then(res => ({ + ok: true as const, + data: res, + })).catch(e => ({ + ok: false as const, + message: msgPerfix('Get Token Failed'), + originalError: e, + })) + + if (!tokenRes.ok) + return tokenRes + + // https://cn.bing.com/translator/?ref=TThis&text=good&from=en&to=es + const res = await ofetch( + `${config.corsProxy}${translatorUrl}`, + { + method: 'POST', + headers: { + authorization: `Bearer ${tokenRes.data}`, + }, + query: { + 'from': supportLanguage[from], + 'to': supportLanguage[to], + 'api-version': '3.0', + 'includeSentenceLength': 'true', + }, + body: [{ Text: text }], + }, + ) + + if (!res[0].translations) { + console.error('Bing Translate Error with 200 status:', res) + throw res + } + + return { + ok: true, + text: res[0].translations[0].text.trim(), + } + } + catch (e) { + if (e instanceof FetchError) { + let message = msgPerfix('Http Request Error') + if (e.status) + message = `\nHttp Status: ${e.status}\n${JSON.stringify(e.data)}` + message += '\nCheck your network connection or choose another translation provider' + + return { + ok: false, + message, + originalError: e, + } + } + else { + return { + ok: false, + message: msgPerfix(typeof e === 'object' ? (e as any)?.message : 'Unknown Error'), + originalError: e, + } + } + } +} diff --git a/src/providers/tranlations/google.ts b/src/providers/tranlations/google.ts index 5c9b2e5..985729c 100644 --- a/src/providers/tranlations/google.ts +++ b/src/providers/tranlations/google.ts @@ -5,6 +5,7 @@ import { config } from '~/config' export const info: TranslationProviderInfo = { name: 'google', + label: 'Google Translate', // https://cloud.google.com/translate/docs/languages?hl=zh-cn supportLanguage: { 'auto': 'auto', @@ -34,6 +35,7 @@ export const info: TranslationProviderInfo = { place_hold: 'default: translate.google.com', }, ], + translate, } export type SupportLanguage = keyof typeof info.supportLanguage @@ -69,9 +71,9 @@ export async function translate(options: GoogleTranslationParameters): Promise needs: { config_key: string; place_hold: string }[] + translate: (options: TranslationParameters) => Promise } diff --git a/src/view/quickInput.ts b/src/view/quickInput.ts index f0a5e93..c264d68 100644 --- a/src/view/quickInput.ts +++ b/src/view/quickInput.ts @@ -4,7 +4,8 @@ import { config, languageOptions } from '~/config' import type { Context } from '~/context' import { useStore } from '~/store' import type { Fn } from '~/types' -import { supportLanguage } from '~/providers/tranlations' +import { translatorOptions, translators } from '~/providers/tranlations' +import type { ConfigKeyTypeMap } from '~/generated-meta' export function showTranslatePopmenu(ctx: Context) { const store = useStore(ctx) @@ -43,7 +44,7 @@ export function showTranslatePopmenu(ctx: Context) { }, { label: '$(cloud) Service:', - description: 'Google Translate', + description: translators[config.translator]?.label || `Unsupported: ${config.translator}`, callback: () => showSetTranslationService(ctx), }, ]) @@ -63,9 +64,10 @@ export function showSetLanguagePopmenu(ctx: Context, type: 'target' | 'source') ? config.defaultTargetLanguage : 'en' + const translatorName = config.translator || 'google' quickPick.items = languageOptions .filter(item => type === 'target' - ? supportLanguage.google[item.description!] + ? translators[translatorName].supportLanguage[item.description!] : item.description === 'en', ) .map((item) => { @@ -112,29 +114,19 @@ export function showSetTranslationService(ctx: Context) { quickPick.title = 'Translation Service' quickPick.matchOnDescription = true quickPick.matchOnDetail = true - defineQuickPickItems(quickPick, [ - { - key: 'google', - label: 'Google Translate', - description: 'Powered by Google Translate', - }, - // TODO add more translation services - // { - // label: 'Baidu Translate', - // description: 'Powered by Baidu Translate', - // }, - // { - // label: 'Youdao Translate', - // description: 'Powered by Youdao Translate', - // }, - // { - // label: 'More...', - // description: 'Install more translate sources from Extensions Marketplace', - // }, - ]) + defineQuickPickItems(quickPick, translatorOptions.map(({ name, label }) => ({ + label: name === config.translator ? `$(check) ${label}` : `$(array) ${label}`, + description: name, + }))) quickPick.onDidChangeSelection((selection) => { window.showInformationMessage(`Selected service: ${selection[0].label}`) + const translatorName = selection[0].description + if (!translatorName || !(translatorName in translators)) { + window.showErrorMessage('Invalid service') + return + } + config.translator = translatorName as ConfigKeyTypeMap['interline-translate.translator'] showTranslatePopmenu(ctx) })