mirror of
https://gitlab.com/soapbox-pub/ditto.git
synced 2025-12-06 03:19:46 +00:00
108 lines
3 KiB
TypeScript
108 lines
3 KiB
TypeScript
import { z } from 'zod';
|
|
|
|
import { languageSchema } from './schema.ts';
|
|
|
|
import type { LanguageCode } from 'iso-639-1';
|
|
import type { DittoTranslator } from './DittoTranslator.ts';
|
|
|
|
interface LibreTranslateTranslatorOpts {
|
|
/** Libretranslate endpoint to use. Default: 'https://libretranslate.com' */
|
|
baseUrl?: string;
|
|
/** Libretranslate API key. */
|
|
apiKey: string;
|
|
/** Custom fetch implementation. */
|
|
fetch?: typeof fetch;
|
|
}
|
|
|
|
export class LibreTranslateTranslator implements DittoTranslator {
|
|
private readonly baseUrl: string;
|
|
private readonly apiKey: string;
|
|
private readonly fetch: typeof fetch;
|
|
|
|
readonly provider = 'libretranslate.com';
|
|
|
|
constructor(opts: LibreTranslateTranslatorOpts) {
|
|
this.baseUrl = opts.baseUrl ?? 'https://libretranslate.com';
|
|
this.fetch = opts.fetch ?? globalThis.fetch;
|
|
this.apiKey = opts.apiKey;
|
|
}
|
|
|
|
async translate(
|
|
texts: string[],
|
|
source: LanguageCode | undefined,
|
|
dest: LanguageCode,
|
|
opts?: { signal?: AbortSignal },
|
|
): Promise<{ results: string[]; sourceLang: LanguageCode }> {
|
|
const translations = await Promise.all(
|
|
texts.map((text) => this.translateOne(text, source, dest, 'html', { signal: opts?.signal })),
|
|
);
|
|
|
|
return {
|
|
results: translations.map((value) => value.translatedText),
|
|
sourceLang: (translations[0]?.detectedLanguage?.language ?? source) as LanguageCode, // cast is ok
|
|
};
|
|
}
|
|
|
|
private async translateOne(
|
|
q: string,
|
|
sourceLanguage: string | undefined,
|
|
targetLanguage: string,
|
|
format: 'html' | 'text',
|
|
opts?: { signal?: AbortSignal },
|
|
) {
|
|
const body = {
|
|
q,
|
|
source: sourceLanguage?.toLowerCase() ?? 'auto',
|
|
target: targetLanguage.toLowerCase(),
|
|
format,
|
|
api_key: this.apiKey,
|
|
};
|
|
|
|
const url = new URL('/translate', this.baseUrl);
|
|
|
|
const request = new Request(url, {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
},
|
|
signal: opts?.signal,
|
|
});
|
|
|
|
const response = await this.fetch(request);
|
|
const json = await response.json();
|
|
|
|
console.log(json);
|
|
|
|
if (!response.ok) {
|
|
const result = LibreTranslateTranslator.errorSchema().safeParse(json);
|
|
|
|
if (result.success) {
|
|
throw new Error(result.data.error);
|
|
} else {
|
|
throw new Error(`Unexpected LibreTranslate error: ${response.statusText} (${response.status})`);
|
|
}
|
|
}
|
|
|
|
return LibreTranslateTranslator.schema().parse(json);
|
|
}
|
|
|
|
/** Libretranslate response schema.
|
|
* https://libretranslate.com/docs/#/translate/post_translate */
|
|
private static schema() {
|
|
return z.object({
|
|
translatedText: z.string(),
|
|
/** This field is only available if the 'source' is set to 'auto' */
|
|
detectedLanguage: z.object({
|
|
language: languageSchema,
|
|
}).optional(),
|
|
});
|
|
}
|
|
|
|
/** Libretranslate error response schema. */
|
|
private static errorSchema() {
|
|
return z.object({
|
|
error: z.string(),
|
|
});
|
|
}
|
|
}
|