refactor (backend): port translator to backend-rs

This commit is contained in:
naskya 2024-07-21 05:19:20 +09:00
parent ec8c760b2c
commit 052a68fc7f
No known key found for this signature in database
GPG Key ID: 712D413B3A9FED5C
12 changed files with 438 additions and 147 deletions

181
Cargo.lock generated
View File

@ -235,6 +235,7 @@ dependencies = [
"url",
"urlencoding",
"web-push",
"zhconv",
]
[[package]]
@ -500,6 +501,16 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc"
dependencies = [
"cfg-if",
"wasm-bindgen",
]
[[package]]
name = "const-oid"
version = "0.6.2"
@ -681,6 +692,12 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "daachorse"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b7ef7a4be509357f4804d0a22e830daddb48f19fd604e4ad32ddce04a94c36"
[[package]]
name = "der"
version = "0.4.5"
@ -1188,6 +1205,12 @@ version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
[[package]]
name = "hex-literal"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46"
[[package]]
name = "hkdf"
version = "0.12.4"
@ -1518,6 +1541,8 @@ dependencies = [
"mime",
"once_cell",
"polling",
"serde",
"serde_json",
"slab",
"sluice",
"tracing",
@ -1526,6 +1551,15 @@ dependencies = [
"waker-fn",
]
[[package]]
name = "itertools"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
dependencies = [
"either",
]
[[package]]
name = "itertools"
version = "0.12.1"
@ -2038,6 +2072,15 @@ dependencies = [
"libc",
]
[[package]]
name = "num_threads"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
dependencies = [
"libc",
]
[[package]]
name = "object"
version = "0.36.1"
@ -2530,7 +2573,7 @@ dependencies = [
"built",
"cfg-if",
"interpolate_name",
"itertools",
"itertools 0.12.1",
"libc",
"libfuzzer-sys",
"log",
@ -2798,6 +2841,23 @@ dependencies = [
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6"
[[package]]
name = "ruzstd"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3ffab8f9715a0d455df4bbb9d21e91135aab3cd3ca187af0cd0c3c3f868fdc"
dependencies = [
"byteorder",
"thiserror-core",
"twox-hash",
]
[[package]]
name = "ryu"
version = "1.0.18"
@ -2860,7 +2920,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum",
"strum 0.25.0",
"thiserror",
"time",
"tracing",
@ -3381,12 +3441,34 @@ dependencies = [
"unicode-properties",
]
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
]
[[package]]
name = "subtle"
version = "2.6.1"
@ -3492,6 +3574,26 @@ dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-core"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c001ee18b7e5e3f62cbf58c7fe220119e68d902bb7443179c0c8aef30090e999"
dependencies = [
"thiserror-core-impl",
]
[[package]]
name = "thiserror-core-impl"
version = "1.0.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4c60d69f36615a077cc7663b9cb8e42275722d23e58a7fa3d2c7f2915d09d04"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.71",
]
[[package]]
name = "thiserror-impl"
version = "1.0.63"
@ -3531,7 +3633,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
@ -3733,6 +3838,16 @@ dependencies = [
"tracing-core",
]
[[package]]
name = "twox-hash"
version = "1.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675"
dependencies = [
"cfg-if",
"static_assertions",
]
[[package]]
name = "typenum"
version = "1.17.0"
@ -3851,6 +3966,18 @@ version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
[[package]]
name = "vergen"
version = "8.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2990d9ea5967266ea0ccf413a4aa5c42a93dbcfda9cb49a97de6931726b12566"
dependencies = [
"anyhow",
"cfg-if",
"rustversion",
"time",
]
[[package]]
name = "version-compare"
version = "0.2.0"
@ -4287,6 +4414,56 @@ dependencies = [
"syn 2.0.71",
]
[[package]]
name = "zhconv"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a5764e8c3c48dce7dd281cdae65c785536d1da3078b484c2254e7bea7b42323"
dependencies = [
"console_error_panic_hook",
"daachorse",
"hex-literal",
"itertools 0.10.5",
"lazy_static",
"once_cell",
"regex",
"ruzstd",
"sha2",
"strum 0.24.1",
"vergen",
"wasm-bindgen",
"zstd",
]
[[package]]
name = "zstd"
version = "0.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "6.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.12+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.4.12"

View File

@ -49,6 +49,7 @@ tracing-subscriber = { version = "0.3.18", default-features = false }
url = { version = "2.5.2", default-features = false }
urlencoding = { version = "2.1.3", default-features = false }
web-push = { git = "https://github.com/pimeys/rust-web-push.git", rev = "40febe4085e3cef9cdfd539c315e3e945aba0656", default-features = false }
zhconv = "0.3.1"
# subdependencies
## explicitly list OpenSSL to use the vendored version

View File

@ -29,7 +29,7 @@ cuid2 = { workspace = true }
emojis = { workspace = true }
idna = { workspace = true, features = ["std", "compiled_data"] }
image = { workspace = true, features = ["avif", "bmp", "gif", "ico", "jpeg", "png", "tiff", "webp"] }
isahc = { workspace = true, features = ["http2", "text-decoding"] }
isahc = { workspace = true, features = ["http2", "text-decoding", "json"] }
nom-exif = { workspace = true }
once_cell = { workspace = true }
openssl = { workspace = true, features = ["vendored"] }
@ -49,6 +49,7 @@ tracing-subscriber = { workspace = true, features = ["ansi"] }
url = { workspace = true }
urlencoding = { workspace = true }
web-push = { workspace = true, features = ["isahc-client"] }
zhconv = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true, features = ["std"] }

View File

@ -1349,6 +1349,13 @@ export declare function toDbReaction(reaction?: string | undefined | null, host?
export declare function toPuny(host: string): string
export declare function translate(text: string, sourceLang: string | undefined | null, targetLang: string): Promise<Translation>
export interface Translation {
sourceLang: string
text: string
}
export declare function unwatchNote(watcherId: string, noteId: string): Promise<void>
export declare function updateAntennaCache(): Promise<void>

View File

@ -443,6 +443,7 @@ module.exports.storageUsage = nativeBinding.storageUsage
module.exports.stringToAcct = nativeBinding.stringToAcct
module.exports.toDbReaction = nativeBinding.toDbReaction
module.exports.toPuny = nativeBinding.toPuny
module.exports.translate = nativeBinding.translate
module.exports.unwatchNote = nativeBinding.unwatchNote
module.exports.updateAntennaCache = nativeBinding.updateAntennaCache
module.exports.updateAntennasOnNewNote = nativeBinding.updateAntennasOnNewNote

View File

@ -17,4 +17,5 @@ pub mod reaction;
pub mod remove_old_attestation_challenges;
pub mod should_nyaify;
pub mod system_info;
pub mod translate;
pub mod user;

View File

@ -0,0 +1,243 @@
use crate::{
config::{local_server_info, server, CONFIG},
util::http_client,
};
#[macros::errors]
pub enum Error {
#[doc = "database error"]
#[error(transparent)]
Db(#[from] sea_orm::DbErr),
#[error("failed to acquire an HTTP client")]
HttpClient(#[from] http_client::Error),
#[error("invalid http request body")]
InvalidRequestBody(#[from] isahc::http::Error),
#[error("http request failed")]
HttpRequest(#[from] isahc::Error),
#[error("failed to serialize the request body")]
Serialize(#[from] serde_json::Error),
#[error("Libretranslate API url is not set")]
MissingApiUrl,
#[error("DeepL API key is not set")]
MissingApiKey,
#[error("no response")]
NoResponse,
#[error("translator is not set")]
NoTranslator,
}
#[macros::export(object)]
pub struct Translation {
pub source_lang: String,
pub text: String,
}
#[macros::export]
pub async fn translate(
text: &str,
source_lang: Option<&str>,
target_lang: &str,
) -> Result<Translation, Error> {
let config = local_server_info().await?;
let mut translation = if let Some(api_key) = config.deepl_auth_key {
deepl_translate::translate(
text,
source_lang,
target_lang,
&api_key,
config.deepl_is_pro,
)
.await?
} else if let Some(api_url) = config.libre_translate_api_url {
libre_translate::translate(
text,
source_lang,
target_lang,
&api_url,
config.libre_translate_api_key.as_deref(),
)
.await?
} else if let Some(server::DeepLConfig {
auth_key, is_pro, ..
}) = CONFIG.deepl.as_ref()
{
deepl_translate::translate(
text,
source_lang,
target_lang,
auth_key.as_ref().ok_or(Error::MissingApiKey)?,
is_pro.unwrap_or(false),
)
.await?
} else if let Some(server::LibreTranslateConfig {
api_url, api_key, ..
}) = CONFIG.libre_translate.as_ref()
{
libre_translate::translate(
text,
source_lang,
target_lang,
api_url.as_ref().ok_or(Error::MissingApiUrl)?,
api_key.as_deref(),
)
.await?
} else {
return Err(Error::NoTranslator);
};
// DeepL translate and LibreTranslate don't provide zh-Hant-TW translations,
// so we convert zh-Hans-CN translations into zh-Hant-TW using zhconv.
if ["zh-tw", "zh-hant", "zh-hant-tw"].contains(&target_lang.to_ascii_lowercase().as_str()) {
translation.text = zhconv::zhconv(&translation.text, zhconv::Variant::ZhTW)
}
Ok(translation)
}
mod deepl_translate {
use crate::util::http_client;
use isahc::{AsyncReadResponseExt, Request};
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize)]
struct Response {
translations: Vec<Translation>,
}
#[derive(Deserialize, Clone)]
struct Translation {
detected_source_language: Option<String>,
text: String,
}
pub(super) async fn translate(
text: &str,
source_lang: Option<&str>,
target_lang: &str,
api_key: &str,
is_pro: bool,
) -> Result<super::Translation, super::Error> {
let client = http_client::client()?;
let api_url = if is_pro {
"https://api.deepl.com/v2/translate"
} else {
"https://api-free.deepl.com/v2/translate"
};
let mut target_lang = target_lang.split('-').collect::<Vec<&str>>()[0];
// DeepL API requires us to specify "en-US" or "en-GB" for English
// translations ("en" does not work), so we need to address it
if target_lang == "en" {
target_lang = "en-US";
}
let body = if let Some(source_lang) = source_lang {
let source_lang = source_lang.split('-').collect::<Vec<&str>>()[0];
json!({
"text": [text],
"source_lang": source_lang,
"target_lang": target_lang
})
} else {
json!({
"text": [text],
"target_lang": target_lang
})
};
let request = Request::post(api_url)
.header("Authorization", format!("DeepL-Auth-Key {}", api_key))
.header("Content-Type", "application/json")
.body(serde_json::to_string(&body)?)?;
let response = client.send_async(request).await?.json::<Response>().await?;
let result = response
.translations
.first()
.ok_or(super::Error::NoResponse)?
.to_owned();
Ok(super::Translation {
source_lang: source_lang
.map(|s| s.to_owned())
.or(result.detected_source_language)
.unwrap_or_else(|| "unknown".to_owned()),
text: result.text,
})
}
}
mod libre_translate {
use crate::util::http_client;
use isahc::{AsyncReadResponseExt, Request};
use serde::Deserialize;
use serde_json::json;
#[derive(Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
struct Translation {
translated_text: String,
detected_language: DetectedLanguage,
}
#[derive(Deserialize, Clone)]
struct DetectedLanguage {
language: String,
}
pub(super) async fn translate(
text: &str,
source_lang: Option<&str>,
target_lang: &str,
api_url: &str,
api_key: Option<&str>,
) -> Result<super::Translation, super::Error> {
let client = http_client::client()?;
let target_lang = target_lang.split('-').collect::<Vec<&str>>()[0];
let body = if let Some(source_lang) = source_lang {
let source_lang = source_lang.split('-').collect::<Vec<&str>>()[0];
json!({
"q": [text],
"source": source_lang,
"target": target_lang,
"format": "text",
"alternatives": 1,
"api_key": api_key.unwrap_or_default()
})
} else {
json!({
"q": [text],
"source": "auto",
"target": target_lang,
"format": "text",
"alternatives": 1,
"api_key": api_key.unwrap_or_default()
})
};
let request = Request::post(api_url)
.header("Content-Type", "application/json")
.body(serde_json::to_string(&body)?)?;
let result = client
.send_async(request)
.await?
.json::<Translation>()
.await?;
Ok(super::Translation {
source_lang: source_lang
.map(|s| s.to_owned())
.unwrap_or(result.detected_language.language),
text: result.translated_text,
})
}
}

View File

@ -50,7 +50,6 @@
"date-fns": "3.6.0",
"decompress": "4.2.1",
"deep-email-validator": "0.1.21",
"deepl-node": "1.13.0",
"escape-regexp": "0.0.1",
"feed": "4.2.2",
"file-type": "19.2.0",
@ -84,7 +83,6 @@
"nested-property": "4.0.0",
"node-fetch": "3.3.2",
"nodemailer": "6.9.14",
"opencc-js": "1.0.5",
"otpauth": "9.3.1",
"parse5": "7.1.2",
"pg": "8.12.0",

View File

@ -1,93 +0,0 @@
import fetch from "node-fetch";
import { Converter } from "opencc-js";
import { getAgentByUrl } from "@/misc/fetch.js";
import { fetchMeta } from "backend-rs";
import type { PostLanguage } from "firefish-js";
import * as deepl from "deepl-node";
// DeepL translate and LibreTranslate don't provide
// zh-Hant-TW translations, so we convert zh-Hans-CN
// translations into zh-Hant-TW using opencc-js.
function convertChinese(convert: boolean, src: string) {
if (!convert) return src;
const converter = Converter({ from: "cn", to: "twp" });
return converter(src);
}
function stem(lang: PostLanguage): string {
let toReturn = lang as string;
if (toReturn.includes("-")) toReturn = toReturn.split("-")[0];
if (toReturn.includes("_")) toReturn = toReturn.split("_")[0];
return toReturn;
}
export async function translate(
text: string,
from: PostLanguage | null,
to: PostLanguage,
) {
const instance = await fetchMeta();
if (instance.deeplAuthKey == null && instance.libreTranslateApiUrl == null) {
throw Error("No translator is set up on this server.");
}
const source = from == null ? null : stem(from);
const target = stem(to);
if (instance.libreTranslateApiUrl != null) {
const jsonBody = {
q: text,
source: source ?? "auto",
target,
format: "text",
api_key: instance.libreTranslateApiKey ?? "",
};
const url = new URL(instance.libreTranslateApiUrl);
if (url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (!url.pathname.endsWith("/translate")) {
url.pathname += "/translate";
}
const res = await fetch(url.toString(), {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(jsonBody),
agent: getAgentByUrl,
});
const json = (await res.json()) as {
detectedLanguage?: {
confidence: number;
language: string;
};
translatedText: string;
};
return {
sourceLang: source ?? json.detectedLanguage?.language,
text: convertChinese(
["zh-hant", "zh-TW"].includes(to),
json.translatedText,
),
};
}
const deeplTranslator = new deepl.Translator(instance.deeplAuthKey ?? "");
const result = await deeplTranslator.translateText(
text,
source as deepl.SourceLanguageCode | null,
// DeepL API requires us to specify "en-US" or "en-GB" for English
// translations ("en" does not work), so we need to address it
(target === "en" ? to : target) as deepl.TargetLanguageCode,
);
return {
sourceLang: source ?? result.detectedSourceLang,
text: convertChinese(["zh-hant", "zh-TW"].includes(to), result.text),
};
}

View File

@ -1,7 +1,6 @@
import { ApiError } from "@/server/api/error.js";
import { getNote } from "@/server/api/common/getters.js";
import { translate } from "@/misc/translate.js";
import type { PostLanguage } from "firefish-js";
import { translate } from "backend-rs";
import define from "@/server/api/define.js";
export const meta = {
@ -47,7 +46,7 @@ export default define(meta, paramDef, async (ps, user) => {
return translate(
note.text,
note.lang as PostLanguage | null,
ps.targetLang as PostLanguage,
note.lang as string | null,
ps.targetLang,
);
});

View File

@ -40,7 +40,7 @@ import {
getStubMastoContext,
type MastoContext,
} from "@/server/api/mastodon/index.js";
import { translate } from "@/misc/translate.js";
import { translate } from "backend-rs";
import { createScheduledNoteJob } from "@/queue/index.js";
export class NoteHelpers {

View File

@ -129,9 +129,6 @@ importers:
deep-email-validator:
specifier: 0.1.21
version: 0.1.21
deepl-node:
specifier: 1.13.0
version: 1.13.0
escape-regexp:
specifier: 0.0.1
version: 0.0.1
@ -231,9 +228,6 @@ importers:
nodemailer:
specifier: 6.9.14
version: 6.9.14
opencc-js:
specifier: 1.0.5
version: 1.0.5
otpauth:
specifier: 9.3.1
version: 9.3.1
@ -1001,13 +995,11 @@ packages:
'@biomejs/cli-darwin-arm64@1.8.3':
resolution: {integrity: sha512-9DYOjclFpKrH/m1Oz75SSExR8VKvNSSsLnVIqdnKexj6NwmiMlKk94Wa1kZEdv6MCOHGHgyyoV57Cw8WzL5n3A==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@1.8.3':
resolution: {integrity: sha512-UeW44L/AtbmOF7KXLCoM+9PSgPo0IDcyEUfIoOXYeANaNXXf9mLUwV1GeF2OWjyic5zj6CnAJ9uzk2LT3v/wAw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@1.8.3':
@ -1019,7 +1011,6 @@ packages:
'@biomejs/cli-linux-arm64@1.8.3':
resolution: {integrity: sha512-fed2ji8s+I/m8upWpTJGanqiJ0rnlHOK3DdxsyVLZQ8ClY6qLuPc9uehCREBifRJLl/iJyQpHIRufLDeotsPtw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@1.8.3':
@ -1031,7 +1022,6 @@ packages:
'@biomejs/cli-linux-x64@1.8.3':
resolution: {integrity: sha512-I8G2QmuE1teISyT8ie1HXsjFRz9L1m5n83U1O6m30Kw+kPMPSKjag6QGUn+sXT8V+XWIZxFFBoTDEDZW2KPDDw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@1.8.3':
@ -3662,10 +3652,6 @@ packages:
deep-equal@1.0.1:
resolution: {integrity: sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==}
deepl-node@1.13.0:
resolution: {integrity: sha512-pm8Al5B+/fRHiIKoreoSmv2RlXidF18+CznhtLILiYcj3EbxZpIhxWO8cgXCCsCTrUDMAbScIl8CuH3AqLPpGg==}
engines: {node: '>=12.0'}
deepmerge@4.3.1:
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
engines: {node: '>=0.10.0'}
@ -4085,10 +4071,6 @@ packages:
resolution: {integrity: sha512-KQVhvhK8ZkWzxKxOr56CPulAhH3dobtuQ4+hNQ+HekH/Wp5gSOafqRAeTphQUJAIk0GBvHZgJ2ZGRWd5kphMuw==}
engines: {node: '>= 18'}
form-data@3.0.1:
resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
engines: {node: '>= 6'}
form-data@4.0.0:
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
engines: {node: '>= 6'}
@ -5032,10 +5014,6 @@ packages:
resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==}
engines: {node: '>=10'}
loglevel@1.9.1:
resolution: {integrity: sha512-hP3I3kCrDIMuRwAwHltphhDM1r8i55H33GgqjXbrisuJhF4kRhW1dNuxsRklp4bXl8DSdLaNLuiL4A/LWRfxvg==}
engines: {node: '>= 0.6.0'}
long@5.2.3:
resolution: {integrity: sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==}
@ -5363,9 +5341,6 @@ packages:
only@0.0.2:
resolution: {integrity: sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==}
opencc-js@1.0.5:
resolution: {integrity: sha512-LD+1SoNnZdlRwtYTjnQdFrSVCAaYpuDqL5CkmOaHOkKoKh7mFxUicLTRVNLU5C+Jmi1vXQ3QL4jWdgSaa4sKjg==}
opencollective-postinstall@2.0.3:
resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==}
hasBin: true
@ -9852,15 +9827,6 @@ snapshots:
deep-equal@1.0.1: {}
deepl-node@1.13.0:
dependencies:
'@types/node': 20.14.11
axios: 1.7.2
form-data: 3.0.1
loglevel: 1.9.1
transitivePeerDependencies:
- debug
deepmerge@4.3.1: {}
defer-to-connect@2.0.1: {}
@ -10292,12 +10258,6 @@ snapshots:
form-data-encoder@4.0.2: {}
form-data@3.0.1:
dependencies:
asynckit: 0.4.0
combined-stream: 1.0.8
mime-types: 2.1.35
form-data@4.0.0:
dependencies:
asynckit: 0.4.0
@ -11559,8 +11519,6 @@ snapshots:
chalk: 4.1.2
is-unicode-supported: 0.1.0
loglevel@1.9.1: {}
long@5.2.3: {}
lowercase-keys@2.0.0: {}
@ -11868,8 +11826,6 @@ snapshots:
only@0.0.2: {}
opencc-js@1.0.5: {}
opencollective-postinstall@2.0.3: {}
opentype.js@0.4.11: {}