bing-translate-api
Version:
A simple and free API for Bing & Microsoft Translator for Node.js
165 lines (142 loc) • 4.8 kB
JavaScript
/**
* @typedef {{
* token: string,
* tokenExpiresAt: number
* }} GlobalConfig
*
* @typedef {import('got').Got} Got
* @typedef {import('got').Options} GotOptions
*
* @typedef {import('../../index').MET.MetTranslateOptions} TranslateOptions
* @typedef {import('../../index').MET.MetTranslationResult} TranslationResult
*/
/** @type {Got} */
const got = require('got')
const lang = require('./lang')
const { userAgent: DEFAULT_USER_AGENT } = require('../config.json')
const API_AUTH = 'https://edge.microsoft.com/translate/auth'
const API_TRANSLATE = 'https://api.cognitive.microsofttranslator.com/translate'
/**
* @type {GlobalConfig | undefined}
*/
let globalConfig
/**
* @type {Promise<GlobalConfig> | undefined}
*/
let globalConfigPromise
/**
* @param {string} [userAgent]
*/
async function fetchGlobalConfig(userAgent) {
try {
const authJWT = await got(API_AUTH, {
headers: {
'User-Agent': userAgent || DEFAULT_USER_AGENT
}
}).text()
const jwtPayload = JSON.parse(Buffer.from(authJWT.split('.')[1], 'base64').toString('utf-8'))
globalConfig = {
token: authJWT,
// valid in 10 minutes
tokenExpiresAt: jwtPayload.exp * 1e3
}
} catch (e) {
console.error('failed to fetch auth token')
throw e
}
}
function isTokenExpired() {
// consider the token as expired if the rest time is less than 1 minute
return !globalConfig || (globalConfig.tokenExpiresAt || 0) - Date.now() < 6e4
}
/**
* To translate
*
* @param {string | string[]} text content to be translated
* @param {string} [from] source language code
* @param {string | string[]} to target language code(s). `en` by default.
* @param {TranslateOptions} [options] optional translate options
*
* @returns {Promise<TranslationResult | undefined>}
*/
async function translate(text, from, to, options) {
if (!text || !text.length) {
return
}
// compatible with the bing translator
from && from.toLocaleLowerCase() === 'auto-detect' && (from = void 0)
from = lang.getLangCode(from)
// target language fallbacks to `en`
Array.isArray(to) || (to = [to])
to = to.map(toLang => lang.getLangCode(toLang) || 'en')
to.length || (to = ['en'])
// check if the source and target languages are supported
const fromSupported = !from || lang.isSupported(from)
const toSupported = to.every(lang.isSupported)
if (!fromSupported || !toSupported) {
throw new Error(`Unsupported language(s): ${!fromSupported
? `'${from}'`
: !toSupported ? to.map(t => `'${t}'`).join(', ') : ''
}`)
}
// The MET mode no longer pre-checks the text length for simplicity
Array.isArray(text) || (text = [text])
options ||= {}
// Skip to fetch the free authorization if the `authenticationHeaders` is provided
// You will have to check if the authorization is expired by yourself
// See https://learn.microsoft.com/azure/ai-services/translator/reference/v3-0-reference#authentication
const authenticationHeaders = options.authenticationHeaders
if (!authenticationHeaders) {
if (!globalConfigPromise) {
globalConfigPromise = fetchGlobalConfig(options.userAgent)
}
await globalConfigPromise
if (isTokenExpired()) {
globalConfigPromise = fetchGlobalConfig(options.userAgent)
}
await globalConfigPromise
}
const gotOptions = Object.assign({}, options.gotOptions)
// for customized headers
const gotHeaders = gotOptions.headers || {}
delete gotOptions.headers
const requestPayload = text.map(txt => ({ Text: txt }))
try {
const { body } = await got.post(API_TRANSLATE, {
searchParams: new URLSearchParams([
...to.map(toLang => ['to', toLang]),
...Object.entries({
'api-version': '3.0',
from,
// See https://learn.microsoft.com/azure/ai-services/translator/reference/v3-0-translate#optional-parameters
...(options.translateOptions || {})
}).filter(([_, val]) => val != null && val !== '')
]),
json: requestPayload,
headers: {
'User-Agent': DEFAULT_USER_AGENT,
Authorization: authenticationHeaders ? void 0 : 'Bearer ' + globalConfig.token,
...(authenticationHeaders || {}),
...gotHeaders
},
responseType: 'json',
// the customized `got` options
...gotOptions
})
return body
} catch (e) {
let errMsg
if (e instanceof got.RequestError) {
const response = e.response
const responseBody = JSON.stringify(response.body, null, 2)
errMsg = ` with a status code: ${response.statusCode} (${response.statusMessage})\n${responseBody}\n`
} else {
errMsg = `: ${e.message}`
}
throw new Error(`failed to translate${errMsg}`)
}
}
module.exports = {
translate,
lang
}