libretranslate/
lib.rs

1//! # libretranslate-rs
2//!! [![Crates.io](https:img.shields.io/crates/v/libretranslate.svg)](https://crates.io/crates/libretranslate)
3//! [![Crates.io](https://img.shields.io/crates/d/libretranslate)](https://crates.io/crates/libretranslate)
4//! [![API](https://docs.rs/libretranslate/badge.svg)](https://docs.rs/libretranslate)
5//! [![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod)](https://gitpod.io/#https://github.com/grantshandy/libretranslate-rs)
6//! ![GitHub Workflow Status](https://img.shields.io/github/workflow/status/grantshandy/libretranslate-rs/Rust)
7//!
8//! A LibreTranslate API client for Rust.
9//! ```
10//! libretranslate = "0.5"
11//! ```
12//!
13//! `libretranslate` allows you to use open source machine translation in your projects through an easy to use API that connects to the official [webpage](https://libretranslate.com/).
14//!
15//! ## Basic Example
16//! `libretranslate` is an async library, so you'll have to use an async runtime like [`tokio`](https://crates.io/crates/tokio) or [`async-std`](https://crates.io/crates/async-std).
17//!
18//! All translations are done through the [`translate`](crate::translate) function:
19//! ```rust
20//! use libretranslate::{translate, Language};
21//!
22//! #[tokio::main]
23//! async fn main() {
24//!     let source = Language::French;
25//!     let target = Language::English;
26//!     let input = "Le texte français.";
27//!
28//!     let data = translate(source, target, input, None).await.unwrap();
29//!
30//!     println!("Input {}: {}", data.source.as_pretty(), data.input);
31//!     println!("Output {}: {}", data.target.as_pretty(), data.output);
32//! }
33//! ```
34//!
35//! Output:
36//! ```
37//! Input French: le texte français.
38//! Output English: the French text.
39//! ```
40//!
41//! [See In Examples Folder](https://github.com/grantshandy/libretranslate-rs/blob/main/examples/basic.rs)
42//!
43//! ## Language Detection
44//! Here's a simple example.
45//! ```rust
46//! use libretranslate::{translate, Language};
47//!
48//! #[tokio::main]
49//! async fn main() {
50//!     let target = Language::English;
51//!     let text = "le texte français.";
52//!
53//!     let data = translate(Language::Detect, target, text, None).await.unwrap();
54//!
55//!     println!("Input {}: {}", data.source.as_pretty(), data.input);
56//!     println!("Output {}: {}", data.target.as_pretty(), data.output);
57//! }
58//! ```
59//!
60//! Output:
61//! ```
62//! Input French: le texte français.
63//! Output English: the French text.
64//! ```
65//!
66//! [See In Examples Folder](https://github.com/grantshandy/libretranslate-rs/blob/main/examples/detect.rs)
67//!
68//! ## Language Functionality
69//! The `Language` enum has a lot of functionality so you can create a `Language` from all sorts of different user inputs.
70//!
71//! You can return a `&str` with the language's name in English using `as_pretty()`, or the language's code using `as_code()`.
72//!
73//! `Language` also implements `FromStr` so you can create a `Language` using text like "en", or "English" (case doesn't matter). You can do this by either using `Language::from()` or `.parse::<Language>()`.
74//!
75//! Here's a simple example.
76//! ```rust
77//! let lang = Language::English;
78//! let lang_parse = "english".parse::<Language>().unwrap();
79//!
80//! assert_eq!(lang, lang_parse);
81//! assert_eq!("en", lang.as_code());
82//! assert_eq!("English", lang.as_pretty());
83//! ```
84//!
85//! [See In Examples Folder](https://github.com/grantshandy/libretranslate-rs/blob/main/examples/language.rs)
86//!
87//! ## String Methods
88//! The trait `Translate` implements [`AsRef<str>`](https://doc.rust-lang.org/std/convert/trait.AsRef.html), meaning that any `&str` or `String` can be translated into any other language.
89//!
90//! Here's a simple example.
91//! ```rust
92//! use libretranslate::{Language, Translate};
93//!
94//! #[tokio::main]
95//! async fn main() {
96//!     let text = "This is text, written on a computer, in English."
97//!         .to_lang(Language::German)
98//!         .from_lang(Language::English)
99//!         .translate()
100//!         .await
101//!         .unwrap();
102//!
103//!     println!("output: \"{}\"", text);
104//! }
105//! ```
106//!
107//! Output:
108//! ```
109//! Output: "Dies ist Text, geschrieben auf einem Computer, in Englisch."
110//! ```
111//!
112//! [See In Examples Folder](https://github.com/grantshandy/libretranslate-rs/blob/main/examples/method.rs)
113//!
114//! ## Available Languages
115//! - English
116//! - Arabic
117//! - Chinese
118//! - French
119//! - German
120//! - Italian
121//! - Japanese
122//! - Portuguese
123//! - Russian
124//! - Spanish
125//! - Polish
126//!
127
128use serde_json::Value;
129
130/// Data that is output by the [`translate`](translate) function.
131#[derive(Debug, Clone, PartialEq, Hash)]
132pub struct Translation {
133    pub url: String,
134    pub source: Language,
135    pub target: Language,
136    pub input: String,
137    pub output: String,
138}
139
140/// Translate text between two [`Language`](Language).
141pub async fn translate<T: AsRef<str>>(
142    source: Language,
143    target: Language,
144    input: T,
145    key: Option<T>,
146) -> Result<Translation, TranslateError> {
147    let url = "https://libretranslate.com/";
148
149    let key: Option<String> = key.map(|data| data.as_ref().to_string());
150
151    let data = translate_url(source, target, input.as_ref(), url, key).await?;
152
153    Ok(data)
154}
155
156/// Translate using a custom URL.
157pub async fn translate_url<T: AsRef<str>>(
158    source: Language,
159    target: Language,
160    input: T,
161    url: T,
162    key: Option<String>,
163) -> Result<Translation, TranslateError> {
164    let complete_url: String;
165
166    if url.as_ref().ends_with('/') {
167        complete_url = format!("{}translate", url.as_ref());
168    } else {
169        complete_url = format!("{}/translate", url.as_ref());
170    };
171
172    if input.as_ref().chars().count() >= 5000 {
173        return Err(TranslateError::LengthError);
174    };
175
176    let data: Value = match key {
177        Some(key) => {
178            serde_json::json!({
179                "q": input.as_ref(),
180                "source": source.as_code(),
181                "target": target.as_code(),
182                "api_key": key,
183            })
184        }
185        None => {
186            serde_json::json!({
187                "q": input.as_ref(),
188                "source": source.as_code(),
189                "target": target.as_code(),
190            })
191        }
192    };
193
194    let body = match surf::http::Body::from_json(&data) {
195        Ok(data) => data,
196        Err(error) => return Err(TranslateError::HttpError(error.to_string())),
197    };
198
199    let url = complete_url.clone();
200
201    let res = match surf::post(complete_url).body(body).recv_string().await {
202        Ok(data) => data,
203        Err(error) => return Err(TranslateError::HttpError(error.to_string())),
204    };
205
206    let parsed_json: Value = match serde_json::from_str(&res) {
207        Ok(parsed_json) => parsed_json,
208        Err(error) => {
209            return Err(TranslateError::ParseError(error.to_string()));
210        }
211    };
212
213    if let Value::String(error) = &parsed_json["error"] {
214        return Err(TranslateError::ParseError(error.to_string()));
215    }
216
217    let output = match &parsed_json["translatedText"] {
218        Value::String(output) => output,
219        _ => {
220            return Err(TranslateError::ParseError(String::from(
221                "Unable to find translatedText in parsed JSON",
222            )))
223        }
224    };
225
226    let input = input.as_ref().to_string();
227    let output = output.to_string();
228
229    Ok(Translation {
230        url,
231        source,
232        target,
233        input,
234        output,
235    })
236}
237
238use std::str::FromStr;
239
240/// Languages that can used for input and output of the [`translate`](crate::translate) function.
241#[derive(Debug, Clone, PartialEq, Copy, Hash)]
242pub enum Language {
243    Detect,
244    English,
245    Arabic,
246    Chinese,
247    French,
248    German,
249    Italian,
250    Japanese,
251    Portuguese,
252    Russian,
253    Spanish,
254    Polish,
255}
256
257impl Language {
258    /// Return the language with the language code name. (ex. "ar", "de")
259    pub fn as_code(&self) -> &'static str {
260        match self {
261            Language::Detect => "auto",
262            Language::English => "en",
263            Language::Arabic => "ar",
264            Language::Chinese => "zh",
265            Language::French => "fr",
266            Language::German => "de",
267            Language::Italian => "it",
268            Language::Japanese => "ja",
269            Language::Portuguese => "pt",
270            Language::Russian => "ru",
271            Language::Spanish => "es",
272            Language::Polish => "pl",
273        }
274    }
275
276    /// Return the Language with the full English name. (ex. "Arabic", "German")
277    pub fn as_pretty(&self) -> &'static str {
278        match self {
279            Language::Detect => "Detected",
280            Language::English => "English",
281            Language::Arabic => "Arabic",
282            Language::Chinese => "Chinese",
283            Language::French => "French",
284            Language::German => "German",
285            Language::Italian => "Italian",
286            Language::Japanese => "Japanese",
287            Language::Portuguese => "Portuguese",
288            Language::Russian => "Russian",
289            Language::Spanish => "Spanish",
290            Language::Polish => "pl",
291        }
292    }
293
294    /// Create a Language from &str like "en" or "French". Case Doesn't matter.
295    pub fn from<T: AsRef<str>>(s: T) -> Result<Self, LanguageError> {
296        return Self::from_str(s.as_ref());
297    }
298
299    /// Create a Language from a [`LanguageIdentifier`](unic_langid::LanguageIdentifier).
300    #[cfg(feature = "unicode_langid")]
301    pub fn from_unic_langid(s: unic_langid::LanguageIdentifier) -> Result<Self, LanguageError> {
302        match s.language.as_str() {
303            "en" => Ok(Language::English),
304            "ar" => Ok(Language::Arabic),
305            "zh" => Ok(Language::Chinese),
306            "fr" => Ok(Language::French),
307            "de" => Ok(Language::German),
308            "it" => Ok(Language::Italian),
309            "pt" => Ok(Language::Portuguese),
310            "ru" => Ok(Language::Russian),
311            "es" => Ok(Language::Spanish),
312            "ja" => Ok(Language::Japanese),
313            "pl" => Ok(Language::Polish),
314            &_ => Err(LanguageError::FormatError("Unknown Language".to_string())),
315        }
316    }
317}
318
319// TODO: Get locale from user to set Language::default().
320impl Default for Language {
321    fn default() -> Self {
322        Language::English
323    }
324}
325
326impl FromStr for Language {
327    type Err = LanguageError;
328
329    fn from_str(s: &str) -> Result<Self, Self::Err> {
330        match s.to_string().to_lowercase().as_str() {
331            "en" => Ok(Language::English),
332            "ar" => Ok(Language::Arabic),
333            "zh" => Ok(Language::Chinese),
334            "fr" => Ok(Language::French),
335            "de" => Ok(Language::German),
336            "it" => Ok(Language::Italian),
337            "pt" => Ok(Language::Portuguese),
338            "ru" => Ok(Language::Russian),
339            "es" => Ok(Language::Spanish),
340            "ja" => Ok(Language::Japanese),
341            "pl" => Ok(Language::Polish),
342            "english" => Ok(Language::English),
343            "arabic" => Ok(Language::Arabic),
344            "chinese" => Ok(Language::Chinese),
345            "french" => Ok(Language::French),
346            "german" => Ok(Language::German),
347            "italian" => Ok(Language::Italian),
348            "portuguese" => Ok(Language::Portuguese),
349            "russian" => Ok(Language::Russian),
350            "spanish" => Ok(Language::Spanish),
351            "japanese" => Ok(Language::Japanese),
352            "polish" => Ok(Language::Polish),
353            "auto" => Ok(Language::Detect),
354            &_ => Err(LanguageError::FormatError(s.to_string())),
355        }
356    }
357}
358
359impl std::fmt::Display for Language {
360    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
361        match self {
362            Language::Detect => write!(f, "auto"),
363            Language::English => write!(f, "en"),
364            Language::Arabic => write!(f, "ar"),
365            Language::Chinese => write!(f, "zh"),
366            Language::French => write!(f, "fr"),
367            Language::German => write!(f, "de"),
368            Language::Italian => write!(f, "it"),
369            Language::Portuguese => write!(f, "pt"),
370            Language::Russian => write!(f, "ru"),
371            Language::Spanish => write!(f, "es"),
372            Language::Japanese => write!(f, "ja"),
373            Language::Polish => write!(f, "pl"),
374        }
375    }
376}
377
378/// Errors that could be outputed by a [`Language`](Language).
379#[derive(Debug, Clone, PartialEq, Hash)]
380pub enum LanguageError {
381    FormatError(String),
382}
383
384impl std::error::Error for LanguageError {}
385
386impl std::fmt::Display for LanguageError {
387    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
388        match self {
389            LanguageError::FormatError(error) => {
390                write!(f, "Unknown Language: {}", error)
391            }
392        }
393    }
394}
395
396/// Errors that could be outputed by [`translate`](crate::translate).
397#[derive(Debug, Clone, PartialEq, Hash)]
398pub enum TranslateError {
399    HttpError(String),
400    ParseError(String),
401    DetectError,
402    LengthError,
403}
404
405impl std::error::Error for TranslateError {}
406
407impl std::fmt::Display for TranslateError {
408    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
409        match self {
410            TranslateError::HttpError(error) => {
411                write!(f, "HTTP request error: {}", error.to_string())
412            }
413            TranslateError::ParseError(error) => {
414                write!(f, "JSON parsing error: {}", error.to_string())
415            }
416            TranslateError::DetectError => {
417                write!(f, "Language detection error")
418            }
419            TranslateError::LengthError => {
420                write!(f, "Requested text is too long")
421            }
422        }
423    }
424}
425
426/// A struct created by a [`Translate`](Translate) that can be translated using the translate method.
427pub struct Query<'a> {
428    pub url: &'a str,
429    pub text: &'a str,
430    pub source: Language,
431    pub target: Language,
432}
433
434impl<'a> Query<'a> {
435    pub fn to_lang(mut self, language: Language) -> Query<'a> {
436        self.target = language;
437        self
438    }
439
440    pub fn from_lang(mut self, language: Language) -> Query<'a> {
441        self.source = language;
442        self
443    }
444
445    pub fn url(mut self, url: &'a str) -> Query {
446        self.url = url;
447        self
448    }
449
450    pub async fn translate(self) -> Result<String, TranslateError> {
451        let res = crate::translate_url(self.source, self.target, self.text, self.url, None).await?;
452        Ok(res.output)
453    }
454}
455
456/// Translate text from a [`String`](std::string::String) or [`str`](std::str) (anything that implements [`AsRef<str>`](std::convert::AsRef)).
457pub trait Translate {
458    fn to_lang(&self, language: Language) -> Query;
459    fn from_lang(&self, language: Language) -> Query;
460}
461
462impl<T> Translate for T
463where
464    T: AsRef<str>,
465{
466    fn to_lang(&self, language: Language) -> Query {
467        Query {
468            url: "https://libretranslate.com/",
469            text: self.as_ref(),
470            source: Language::Detect,
471            target: language,
472        }
473    }
474
475    fn from_lang(&self, language: Language) -> Query {
476        Query {
477            url: "https://libretranslate.com/",
478            text: self.as_ref(),
479            source: language,
480            target: Language::default(),
481        }
482    }
483}
484
485/// Build Translations more verbosely.
486#[derive(Debug, Clone, PartialEq, Hash)]
487pub struct TranslationBuilder {
488    pub url: String,
489    pub source: Language,
490    pub target: Language,
491    pub input: String,
492    key: Option<String>,
493}
494
495impl TranslationBuilder {
496    pub fn new() -> Self {
497        Self {
498            url: String::from("https://libretranslate.com/"),
499            source: Language::Detect,
500            target: Language::default(),
501            input: String::new(),
502            key: None,
503        }
504    }
505
506    pub fn url<T: AsRef<str>>(mut self, url: T) -> Self {
507        self.url = url.as_ref().to_string();
508        self
509    }
510
511    pub fn from_lang(mut self, lang: Language) -> Self {
512        self.source = lang;
513        self
514    }
515
516    pub fn to_lang(mut self, lang: Language) -> Self {
517        self.target = lang;
518        self
519    }
520
521    pub fn text<T: AsRef<str>>(mut self, text: T) -> Self {
522        self.input = text.as_ref().to_string();
523        self
524    }
525
526    pub fn key<T: AsRef<str>>(mut self, key: T) -> Self {
527        self.key = Some(key.as_ref().to_string());
528        self
529    }
530
531    pub async fn translate(mut self) -> Result<Translation, TranslateError> {
532        if self.input.is_empty() {
533            return Ok(Translation {
534                url: self.url,
535                source: self.source,
536                target: self.target,
537                input: self.input,
538                output: String::new(),
539            });
540        };
541
542        let data = translate_url(
543            self.source,
544            self.target,
545            self.input.clone(),
546            self.url.clone(),
547            self.key,
548        )
549        .await?;
550
551        self.source = data.source;
552        self.target = data.target;
553
554        Ok(Translation {
555            url: self.url,
556            source: self.source,
557            target: self.target,
558            input: self.input,
559            output: data.output,
560        })
561    }
562}
563impl Default for TranslationBuilder {
564    fn default() -> Self {
565        Self::new()
566    }
567}