diff --git a/Cargo.lock b/Cargo.lock index ee60e5f838..107f311058 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 784f800384..f91aca2819 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/packages/backend-rs/Cargo.toml b/packages/backend-rs/Cargo.toml index 59bacd04a4..f6176ec0a4 100644 --- a/packages/backend-rs/Cargo.toml +++ b/packages/backend-rs/Cargo.toml @@ -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"] } diff --git a/packages/backend-rs/index.d.ts b/packages/backend-rs/index.d.ts index 4892304c5d..68502da54d 100644 --- a/packages/backend-rs/index.d.ts +++ b/packages/backend-rs/index.d.ts @@ -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> diff --git a/packages/backend-rs/index.js b/packages/backend-rs/index.js index 139739ac00..a5273f7849 100644 --- a/packages/backend-rs/index.js +++ b/packages/backend-rs/index.js @@ -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 diff --git a/packages/backend-rs/src/misc/mod.rs b/packages/backend-rs/src/misc/mod.rs index afa3b1396e..4c715f965c 100644 --- a/packages/backend-rs/src/misc/mod.rs +++ b/packages/backend-rs/src/misc/mod.rs @@ -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; diff --git a/packages/backend-rs/src/misc/translate.rs b/packages/backend-rs/src/misc/translate.rs new file mode 100644 index 0000000000..fa19e26feb --- /dev/null +++ b/packages/backend-rs/src/misc/translate.rs @@ -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, + }) + } +} diff --git a/packages/backend/package.json b/packages/backend/package.json index 47e7e3a653..3719b614f7 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -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", diff --git a/packages/backend/src/misc/translate.ts b/packages/backend/src/misc/translate.ts deleted file mode 100644 index d1bcd5e72e..0000000000 --- a/packages/backend/src/misc/translate.ts +++ /dev/null @@ -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), - }; -} diff --git a/packages/backend/src/server/api/endpoints/notes/translate.ts b/packages/backend/src/server/api/endpoints/notes/translate.ts index 8b84baa0b2..c939ab231a 100644 --- a/packages/backend/src/server/api/endpoints/notes/translate.ts +++ b/packages/backend/src/server/api/endpoints/notes/translate.ts @@ -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, ); }); diff --git a/packages/backend/src/server/api/mastodon/helpers/note.ts b/packages/backend/src/server/api/mastodon/helpers/note.ts index 81df7c1924..15ae9b5d4a 100644 --- a/packages/backend/src/server/api/mastodon/helpers/note.ts +++ b/packages/backend/src/server/api/mastodon/helpers/note.ts @@ -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 { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 076a935d93..8f83d81d26 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {}