Skip to content

Commit f2d1541

Browse files
authored
feat(providers): bing translation (#16)
1 parent 0d89328 commit f2d1541

File tree

8 files changed

+184
-34
lines changed

8 files changed

+184
-34
lines changed

package.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@
136136
"default": "",
137137
"description": "Fallback language when the default language is not available."
138138
},
139+
"interline-translate.translator": {
140+
"type": "string",
141+
"default": "bing",
142+
"enum": ["google", "bing"],
143+
"description": "Translation service provider"
144+
},
139145
"interline-translate.googleTranslateProxy": {
140146
"type": "string",
141147
"default": "",

src/controller/translateSelected.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { useExtensionContext } from '~/dependence'
66
import { useTranslationMeta } from '~/model/translator'
77
import { translate } from '~/providers/tranlations/google'
88
import { displayOnGapLines } from '~/view'
9+
import { config } from '~/config'
10+
import { translators } from '~/providers/tranlations'
911

1012
export function RegisterTranslateSelectedText(ctx: Context) {
1113
const extCtx = useExtensionContext(ctx)
@@ -24,10 +26,11 @@ export function RegisterTranslateSelectedText(ctx: Context) {
2426

2527
const meta = useTranslationMeta()
2628

29+
const translator = translators[config.translator]
2730
const res = await translate({
2831
text: activeEditor.document.getText(range),
29-
from: meta.from as string as any,
30-
to: meta.to as string as any,
32+
from: meta.from as keyof typeof translator.supportLanguage,
33+
to: meta.to as keyof typeof translator.supportLanguage,
3134
})
3235

3336
if (!res.ok) {

src/model/translator.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import type { TextDocument } from 'vscode'
33
import { extractPhrases } from './extract'
44
import { CommentScopes, StringScopes, findScopesRange, isComment, isKeyword, isString, parseDocumentToTokens } from '~/model/grammar'
55
import { persistTranslationCache, useTranslationCache } from '~/model/cache'
6-
import { translate } from '~/providers/tranlations/google'
76
import { config } from '~/config'
87
import type { Context } from '~/context'
8+
import { translators } from '~/providers/tranlations'
99

1010
export function useTranslationMeta() {
1111
// TODO: use config or automatically recognize from language
@@ -99,10 +99,11 @@ export async function translateDocument(ctx: Context, options: TranslateDocument
9999
if (!phrasesFromDoc.length)
100100
return
101101

102-
const translationResult = await translate({
102+
const translator = translators[config.translator]
103+
const translationResult = await translator.translate({
103104
text: phrasesFromDoc.join('\n'),
104-
from: from as string as any,
105-
to: to as string as any,
105+
from: from as keyof typeof translator.supportLanguage,
106+
to: to as keyof typeof translator.supportLanguage,
106107
})
107108

108109
if (!translationResult.ok) {

src/providers/tranlations/bing.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { FetchError, ofetch } from 'ofetch'
2+
import type { TranslationParameters, TranslationProviderInfo, TranslationResult } from './types'
3+
import { createUnsupportedLanguageError } from './utils'
4+
import { config } from '~/config'
5+
6+
export const info: TranslationProviderInfo = {
7+
name: 'bing',
8+
label: 'Bing Translate',
9+
// https://learn.microsoft.com/zh-CN/azure/cognitive-services/translator/language-support
10+
supportLanguage: {
11+
'auto': '',
12+
'zh-CN': 'zh-Hans',
13+
'zh-TW': 'zh-Hant',
14+
'yue': 'yue',
15+
'en': 'en',
16+
'ja': 'ja',
17+
'ko': 'ko',
18+
'fr': 'fr',
19+
'es': 'es',
20+
'ru': 'ru',
21+
'de': 'de',
22+
'it': 'it',
23+
'tr': 'tr',
24+
'pt-PT': 'pt-pt',
25+
'pt-BR': 'pt',
26+
'vi': 'vi',
27+
'id': 'id',
28+
'th': 'th',
29+
'ms': 'ms',
30+
'ar': 'ar',
31+
'hi': 'hi',
32+
'mn-Cyrl': 'mn-Cyrl',
33+
'mn-Mong': 'mn-Mong',
34+
'km': 'km',
35+
'nb-NO': 'nb',
36+
'fa': 'fa',
37+
'sv': 'sv',
38+
'pl': 'pl',
39+
'nl': 'nl',
40+
'uk': 'uk',
41+
},
42+
needs: [],
43+
translate,
44+
}
45+
46+
export type SupportLanguage = keyof typeof info.supportLanguage
47+
48+
export interface BingTranslationParameters extends TranslationParameters {
49+
from: SupportLanguage
50+
to: SupportLanguage
51+
}
52+
53+
const tokenUrl = 'https://edge.microsoft.com/translate/auth'
54+
const translatorUrl = 'https://api-edge.cognitive.microsofttranslator.com/translate'
55+
56+
function msgPerfix(text: string) {
57+
return `[Interline Translate] Bing / ${text}`
58+
}
59+
60+
export async function translate(options: BingTranslationParameters): Promise<TranslationResult> {
61+
const { text, from, to } = options
62+
const { supportLanguage } = info
63+
64+
if (text === '') {
65+
return {
66+
ok: false,
67+
message: 'Empty Text',
68+
originalError: null,
69+
}
70+
}
71+
72+
if (!(from in supportLanguage))
73+
return createUnsupportedLanguageError('from', from)
74+
if (!(to in supportLanguage))
75+
return createUnsupportedLanguageError('to', to)
76+
77+
try {
78+
const tokenRes = await ofetch(`${config.corsProxy}${tokenUrl}`, {
79+
method: 'GET',
80+
}).then(res => ({
81+
ok: true as const,
82+
data: res,
83+
})).catch(e => ({
84+
ok: false as const,
85+
message: msgPerfix('Get Token Failed'),
86+
originalError: e,
87+
}))
88+
89+
if (!tokenRes.ok)
90+
return tokenRes
91+
92+
// https://cn.bing.com/translator/?ref=TThis&text=good&from=en&to=es
93+
const res = await ofetch(
94+
`${config.corsProxy}${translatorUrl}`,
95+
{
96+
method: 'POST',
97+
headers: {
98+
authorization: `Bearer ${tokenRes.data}`,
99+
},
100+
query: {
101+
'from': supportLanguage[from],
102+
'to': supportLanguage[to],
103+
'api-version': '3.0',
104+
'includeSentenceLength': 'true',
105+
},
106+
body: [{ Text: text }],
107+
},
108+
)
109+
110+
if (!res[0].translations) {
111+
console.error('Bing Translate Error with 200 status:', res)
112+
throw res
113+
}
114+
115+
return {
116+
ok: true,
117+
text: res[0].translations[0].text.trim(),
118+
}
119+
}
120+
catch (e) {
121+
if (e instanceof FetchError) {
122+
let message = msgPerfix('Http Request Error')
123+
if (e.status)
124+
message = `\nHttp Status: ${e.status}\n${JSON.stringify(e.data)}`
125+
message += '\nCheck your network connection or choose another translation provider'
126+
127+
return {
128+
ok: false,
129+
message,
130+
originalError: e,
131+
}
132+
}
133+
else {
134+
return {
135+
ok: false,
136+
message: msgPerfix(typeof e === 'object' ? (e as any)?.message : 'Unknown Error'),
137+
originalError: e,
138+
}
139+
}
140+
}
141+
}

src/providers/tranlations/google.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { config } from '~/config'
55

66
export const info: TranslationProviderInfo = {
77
name: 'google',
8+
label: 'Google Translate',
89
// https://cloud.google.com/translate/docs/languages?hl=zh-cn
910
supportLanguage: {
1011
'auto': 'auto',
@@ -34,6 +35,7 @@ export const info: TranslationProviderInfo = {
3435
place_hold: 'default: translate.google.com',
3536
},
3637
],
38+
translate,
3739
}
3840

3941
export type SupportLanguage = keyof typeof info.supportLanguage
@@ -69,9 +71,9 @@ export async function translate(options: GoogleTranslationParameters): Promise<T
6971
`${config.corsProxy}https://${domain}/translate_a/single?dt=at&dt=bd&dt=ex&dt=ld&dt=md&dt=qca&dt=rw&dt=rm&dt=ss&dt=t`,
7072
{
7173
method: 'GET',
72-
headers: {
73-
Origin: 'https://translate.google.com',
74-
},
74+
// headers: {
75+
// Origin: 'https://translate.google.com',
76+
// },
7577
query: {
7678
client: 'gtx',
7779
sl: supportLanguage[from],

src/providers/tranlations/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import { info as googleInfo } from './google'
2+
import { info as bingInfo } from './bing'
23

3-
export const supportLanguage = {
4-
google: googleInfo.supportLanguage,
4+
export const translators = {
5+
google: googleInfo,
6+
bing: bingInfo,
57
}
8+
export const translatorOptions = Object.values(translators)

src/providers/tranlations/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export interface TranslationParameters {
1919

2020
export interface TranslationProviderInfo {
2121
name: string
22+
label: string
2223
supportLanguage: Record<string, string | undefined>
2324
needs: { config_key: string; place_hold: string }[]
25+
translate: (options: TranslationParameters) => Promise<TranslationResult>
2426
}

src/view/quickInput.ts

Lines changed: 15 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { config, languageOptions } from '~/config'
44
import type { Context } from '~/context'
55
import { useStore } from '~/store'
66
import type { Fn } from '~/types'
7-
import { supportLanguage } from '~/providers/tranlations'
7+
import { translatorOptions, translators } from '~/providers/tranlations'
8+
import type { ConfigKeyTypeMap } from '~/generated-meta'
89

910
export function showTranslatePopmenu(ctx: Context) {
1011
const store = useStore(ctx)
@@ -43,7 +44,7 @@ export function showTranslatePopmenu(ctx: Context) {
4344
},
4445
{
4546
label: '$(cloud) Service:',
46-
description: 'Google Translate',
47+
description: translators[config.translator]?.label || `Unsupported: ${config.translator}`,
4748
callback: () => showSetTranslationService(ctx),
4849
},
4950
])
@@ -63,9 +64,10 @@ export function showSetLanguagePopmenu(ctx: Context, type: 'target' | 'source')
6364
? config.defaultTargetLanguage
6465
: 'en'
6566

67+
const translatorName = config.translator || 'google'
6668
quickPick.items = languageOptions
6769
.filter(item => type === 'target'
68-
? supportLanguage.google[item.description!]
70+
? translators[translatorName].supportLanguage[item.description!]
6971
: item.description === 'en',
7072
)
7173
.map((item) => {
@@ -112,29 +114,19 @@ export function showSetTranslationService(ctx: Context) {
112114
quickPick.title = 'Translation Service'
113115
quickPick.matchOnDescription = true
114116
quickPick.matchOnDetail = true
115-
defineQuickPickItems(quickPick, [
116-
{
117-
key: 'google',
118-
label: 'Google Translate',
119-
description: 'Powered by Google Translate',
120-
},
121-
// TODO add more translation services
122-
// {
123-
// label: 'Baidu Translate',
124-
// description: 'Powered by Baidu Translate',
125-
// },
126-
// {
127-
// label: 'Youdao Translate',
128-
// description: 'Powered by Youdao Translate',
129-
// },
130-
// {
131-
// label: 'More...',
132-
// description: 'Install more translate sources from Extensions Marketplace',
133-
// },
134-
])
117+
defineQuickPickItems(quickPick, translatorOptions.map(({ name, label }) => ({
118+
label: name === config.translator ? `$(check) ${label}` : `$(array) ${label}`,
119+
description: name,
120+
})))
135121

136122
quickPick.onDidChangeSelection((selection) => {
137123
window.showInformationMessage(`Selected service: ${selection[0].label}`)
124+
const translatorName = selection[0].description
125+
if (!translatorName || !(translatorName in translators)) {
126+
window.showErrorMessage('Invalid service')
127+
return
128+
}
129+
config.translator = translatorName as ConfigKeyTypeMap['interline-translate.translator']
138130
showTranslatePopmenu(ctx)
139131
})
140132

0 commit comments

Comments
 (0)