refactor (backend): port translator to backend-rs
This commit is contained in:
parent
ec8c760b2c
commit
052a68fc7f
12 changed files with 438 additions and 147 deletions
181
Cargo.lock
generated
181
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"] }
|
||||
|
|
7
packages/backend-rs/index.d.ts
vendored
7
packages/backend-rs/index.d.ts
vendored
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
243
packages/backend-rs/src/misc/translate.rs
Normal file
243
packages/backend-rs/src/misc/translate.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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),
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: {}
|
||||
|
|
Loading…
Reference in a new issue