diff --git a/.gitignore b/.gitignore index 44d6cdb..5584c95 100644 --- a/.gitignore +++ b/.gitignore @@ -174,4 +174,5 @@ dist # Finder (MacOS) folder config .DS_Store -data/temp/* \ No newline at end of file +data/temp/* +src/scripts/sandbox.ts diff --git a/data/gtrans-langcodes.json b/data/gtrans-langcodes.json new file mode 100644 index 0000000..73566c1 --- /dev/null +++ b/data/gtrans-langcodes.json @@ -0,0 +1,251 @@ +[ + "aa", + "ab", + "ace", + "ach", + "af", + "ak", + "alz", + "am", + "ar", + "as", + "av", + "awa", + "ay", + "az", + "ba", + "bal", + "ban", + "bbc", + "bci", + "be", + "bem", + "ber", + "ber-Latn", + "bew", + "bg", + "bho", + "bik", + "bm", + "bm-Nkoo", + "bn", + "bo", + "br", + "bs", + "bts", + "btx", + "bua", + "ca", + "ce", + "ceb", + "cgg", + "ch", + "chk", + "chm", + "ckb", + "cnh", + "co", + "crh", + "crh-Latn", + "crs", + "cs", + "cv", + "cy", + "da", + "de", + "din", + "doi", + "dov", + "dv", + "dyu", + "dz", + "ee", + "el", + "en", + "eo", + "es", + "et", + "eu", + "fa", + "fa-AF", + "ff", + "fi", + "fj", + "fo", + "fon", + "fr", + "fr-CA", + "fur", + "fy", + "ga", + "gaa", + "gd", + "gl", + "gn", + "gom", + "gu", + "gv", + "ha", + "haw", + "hi", + "hil", + "hmn", + "hr", + "hrx", + "ht", + "hu", + "hy", + "iba", + "id", + "ig", + "ilo", + "is", + "it", + "iu", + "iu-Latn", + "iw", + "ja", + "jam", + "jw", + "ka", + "kac", + "kek", + "kg", + "kha", + "kk", + "kl", + "km", + "kn", + "ko", + "kr", + "kri", + "ktu", + "ku", + "kv", + "ky", + "la", + "lb", + "lg", + "li", + "lij", + "lmo", + "ln", + "lo", + "lt", + "ltg", + "lua", + "luo", + "lus", + "lv", + "mad", + "mai", + "mak", + "mam", + "mfe", + "mg", + "mh", + "mi", + "min", + "mk", + "ml", + "mn", + "mni-Mtei", + "mr", + "ms", + "ms-Arab", + "mt", + "mwr", + "my", + "ndc-ZW", + "ne", + "new", + "nhe", + "nl", + "no", + "nr", + "nso", + "nus", + "ny", + "oc", + "om", + "or", + "os", + "pa", + "pa-Arab", + "pag", + "pam", + "pap", + "pl", + "ps", + "pt", + "pt-PT", + "qu", + "rn", + "ro", + "rom", + "ru", + "rw", + "sa", + "sah", + "sat", + "sat-Latn", + "scn", + "sd", + "se", + "sg", + "shn", + "si", + "sk", + "sl", + "sm", + "sn", + "so", + "sq", + "sr", + "ss", + "st", + "su", + "sus", + "sv", + "sw", + "szl", + "ta", + "tcy", + "te", + "tet", + "tg", + "th", + "ti", + "tiv", + "tk", + "tl", + "tn", + "to", + "tpi", + "tr", + "trp", + "ts", + "tt", + "tum", + "ty", + "tyv", + "udm", + "ug", + "uk", + "ur", + "uz", + "ve", + "vec", + "vi", + "war", + "wo", + "xh", + "yi", + "yo", + "yua", + "yue", + "zap", + "zh-CN", + "zh-TW", + "zu" +] diff --git a/src/commands/language/translate.ts b/src/commands/language/translate.ts index 1d5bd39..e34463c 100644 --- a/src/commands/language/translate.ts +++ b/src/commands/language/translate.ts @@ -9,13 +9,14 @@ import { defineCommand } from ".."; import { getSourceLanguages, getTargetLanguages, - isSourceLanguageSupported, - isTargetLanguageSupported, - translate, + isSourceLanguage, + isTargetLanguage, + translate as translateDeepl, } from "../../utils/deepl"; import { abort } from "../../utils/error"; import type { OCRResult } from "../../types/ocr"; import { capitalize, languageCodeToName } from "../../utils/functions"; +import { translate as translateGoogle } from "../../utils/gtrans"; export async function translateAutocompleteImpl( interaction: AutocompleteInteraction @@ -44,15 +45,18 @@ export async function translateImpl( ocrModel?: OCRResult["model"], imageUrl?: string ): Promise { - const { - text: translatedText, - detectedSourceLang, - billedCharacters, - } = await translate({ + let { translatedText, detectedSourceLang, model } = await translateDeepl( text, source, - target, - }); + target + ).catch(() => translateGoogle(text, "auto", "en")); + + if (translatedText.trim() === text.trim() && model === "deepl") { + const result = await translateGoogle(text, "auto", "en"); + translatedText = result.translatedText; + detectedSourceLang = result.detectedSourceLang; + model = result.model; + } const displaySource = languageCodeToName(detectedSourceLang); const displayTarget = languageCodeToName(target); @@ -78,20 +82,20 @@ export async function translateImpl( { title: `From ${displaySource} to ${displayTarget}`, description: translatedText, - color: 0x0f2b46, + color: model === "deepl" ? 0x0f2b46 : 0x4285f4, ...(imageUrl ? { image: { url: imageUrl } } : {}), author: { - name: "DeepL", - icon_url: "https://www.google.com/s2/favicons?domain=deepl.com&sz=64", - }, - footer: { - text: ocrModel - ? `OCR: ${capitalize(ocrModel)}` - : `Billed characters: ${billedCharacters}`, - icon_url: ocrModel - ? `https://www.google.com/s2/favicons?domain=${ocrModel}.com&sz=64` - : undefined, + name: model === "deepl" ? "DeepL" : "Google Translate", + icon_url: `https://www.google.com/s2/favicons?domain=${model}.com&sz=64`, }, + ...(ocrModel + ? { + footer: { + text: `OCR: ${capitalize(ocrModel)}`, + icon_url: `https://www.google.com/s2/favicons?domain=${ocrModel}.com&sz=64`, + }, + } + : {}), }, ], }; @@ -100,7 +104,9 @@ export async function translateImpl( export default defineCommand({ data: new SlashCommandBuilder() .setName("translate") - .setDescription("Translates text using DeepL") + .setDescription( + "Translates text using DeepL or Google Translate as fallback" + ) .addStringOption((option) => option .setName("text") @@ -129,10 +135,10 @@ export default defineCommand({ await interaction.deferReply(); - if (source && !(await isSourceLanguageSupported(source))) { + if (source && !(await isSourceLanguage(source))) { abort("Source language not supported"); } - if (target && !(await isTargetLanguageSupported(target))) { + if (target && !(await isTargetLanguage(target))) { abort("Target language not supported"); } diff --git a/src/commands/ocr/ocr.ts b/src/commands/ocr/ocr.ts index 97169f0..2108790 100644 --- a/src/commands/ocr/ocr.ts +++ b/src/commands/ocr/ocr.ts @@ -97,7 +97,7 @@ export async function ocrImpl(url: string) { export default defineCommand({ data: new SlashCommandBuilder() .setName("ocr") - .setDescription("OCR an image using Yandex") + .setDescription("OCR an image using Google Lens or Yandex as fallback") .addAttachmentOption((option) => option.setName("image").setDescription("The image to OCR") ) diff --git a/src/commands/ocr/ocrTranslate.ts b/src/commands/ocr/ocrTranslate.ts index c0e050b..de2815d 100644 --- a/src/commands/ocr/ocrTranslate.ts +++ b/src/commands/ocr/ocrTranslate.ts @@ -1,10 +1,7 @@ import { SlashCommandBuilder } from "discord.js"; import { defineCommand } from ".."; import { abort } from "../../utils/error"; -import { - isSourceLanguageSupported, - isTargetLanguageSupported, -} from "../../utils/deepl"; +import { isSourceLanguage, isTargetLanguage } from "../../utils/deepl"; import { translateAutocompleteImpl, translateImpl, @@ -16,7 +13,7 @@ export default defineCommand({ data: new SlashCommandBuilder() .setName("ocrtranslate") .setDescription( - "OCR an image using Yandex and translate the result using DeepL" + "OCR an image using Google Lens or Yandex and translate the result using DeepL or Google Translate" ) .addAttachmentOption((option) => option.setName("image").setDescription("The image to OCR") @@ -50,10 +47,10 @@ export default defineCommand({ await interaction.deferReply(); - if (source && !(await isSourceLanguageSupported(source))) { + if (source && !(await isSourceLanguage(source))) { abort("Source language not supported"); } - if (target && !(await isTargetLanguageSupported(target))) { + if (target && !(await isTargetLanguage(target))) { abort("Target language not supported"); } diff --git a/src/scripts/sandbox.ts b/src/scripts/sandbox.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/types/translate.ts b/src/types/translate.ts new file mode 100644 index 0000000..b13793b --- /dev/null +++ b/src/types/translate.ts @@ -0,0 +1,5 @@ +export type TranslateResult = { + translatedText: string; + detectedSourceLang: string; + model: "deepl" | "google"; +}; diff --git a/src/utils/deepl.ts b/src/utils/deepl.ts index b1d6791..17ed504 100644 --- a/src/utils/deepl.ts +++ b/src/utils/deepl.ts @@ -5,38 +5,34 @@ import { } from "deepl-node"; import { env } from "../env"; import { lazy } from "./functions"; +import type { TranslateResult } from "../types/translate"; const translator = new Translator(env.DEEPL_API_KEY); export const getSourceLanguages = lazy(() => translator.getSourceLanguages()); export const getTargetLanguages = lazy(() => translator.getTargetLanguages()); -type TranslateOptions = { - text: string; - source?: string | null; - target?: string; -}; - -export async function translate({ - text, - source = null, - target = "en-US", -}: TranslateOptions) { - return translator.translateText( +export async function translate( + text: string, + source: string | null = null, + target = "en-US" +): Promise { + const result = await translator.translateText( text, - source as SourceLanguageCode, + source as SourceLanguageCode | null, target as TargetLanguageCode ); + return { + translatedText: result.text, + detectedSourceLang: result.detectedSourceLang, + model: "deepl", + }; } export async function getUsage() { return translator.getUsage(); } -export async function getLanguages() { - return (await getSourceLanguages()).concat(await getTargetLanguages()); -} - -export async function isSourceLanguageSupported(code: string) { +export async function isSourceLanguage(code: string) { const sourceLanguages = await getSourceLanguages(); return ( sourceLanguages.find((l) => l.code.toLowerCase() === code.toLowerCase()) !== @@ -44,7 +40,7 @@ export async function isSourceLanguageSupported(code: string) { ); } -export async function isTargetLanguageSupported(code: string) { +export async function isTargetLanguage(code: string) { const targetLanguages = await getTargetLanguages(); return ( targetLanguages.find((l) => l.code.toLowerCase() === code.toLowerCase()) !== diff --git a/src/utils/gtrans.ts b/src/utils/gtrans.ts new file mode 100644 index 0000000..c0ec01f --- /dev/null +++ b/src/utils/gtrans.ts @@ -0,0 +1,60 @@ +import ky from "ky"; +import { languageCodeToName, lazy } from "./functions"; +import { readFileSync } from "node:fs"; +import type { TranslateResult } from "../types/translate"; + +type TranslationResponse = { + src: string; + sentences: { + trans: string; + }[]; +}; + +const client = ky.create({ + prefixUrl: "https://translate.googleapis.com/translate_a", +}); + +const languageCodes = lazy( + () => + JSON.parse(readFileSync("data/gtrans-langcodes.json", "utf8")) as string[] +); + +export const getLanguages = lazy(() => + languageCodes().map((code) => ({ + code, + name: languageCodeToName(code), + })) +); + +export function isLanguage(code: string) { + return languageCodes().includes(code); +} + +export async function translate( + text: string, + source = "auto", + target = "en" +): Promise { + const res = await client.get("single", { + searchParams: { + sl: source, + tl: target, + q: text, + client: "gtx", + dt: "t", + dj: "1", + source: "input", + }, + }); + + const { sentences, src } = await res.json(); + + return { + translatedText: sentences + .map((s) => s?.trans) + .filter(Boolean) + .join(""), + detectedSourceLang: src, + model: "google", + }; +}