DEV Community

Thomas Alcala Schneider
Thomas Alcala Schneider

Posted on

Translating My Resume (and any text file) with Rust

tl; dr: this is about creating a CLI that uses Google Translate to translate a text file to a supported language. The code can be found here

Something that sucks about moving to a country that doesn't care too much about the English language and look for a job there is that you have to translate your resume.

So here I am with my well-written and tested resume in English, and have to spend hours (a couple, at least) translating it to French. And French is my mother language too, I just moved around a bit, so a lot of different companies, that's why it's long to translate.

And to be fair, it's even longer to write a CLI to do it for me 😅, even more so in a language that I am currently learning, and I still have to double-check and fix some errors... But I think it'll be helpful in the long run, I'll be able to translate my posts too.

That the developer's paradox: we'd rather rebuild our blog with a crazy stack but have almost no posts in it. Develop in DRY mode that takes longer than actually repeat stuff (like this one). That reminds me of some funny quote from Parcs and Recs:

Ron Swanson on doing nothing

Full disclosure: I am new to Rust, so the code at the end will not be optimal, and this is as much an exercise as it is a post.

Prerequisites

Down to it!

Project Creation

  • Run $ cargo new rosette, this will create a directory called rosette in your current folder
  • Run $ cargo run to test it

Basic Variables

So we need to set the endpoint and get the GOOGLE_API_KEY from the environment variables. Here's how we get them:

use std::env;

fn main() {
    println!("Hello, world!");

    let base_endpoint = "https://translation.googleapis.com/language/translate/v2";
    let args: Vec<String> = env::args().collect();
    let api_key = match env::var("GOOGLE_API_KEY") {
        Ok(val) => val,
        Err(_e) => panic!("Set up the GOOGLE_API_KEY environment variable first"),
    };

    println!(
        "endpoint: {:?}, args: {:?}, key: {:?}",
        base_endpoint, args, api_key
    );
}
Enter fullscreen mode Exit fullscreen mode

Dummy Call

Now let's try doing a call to the API.
A dummy call to that API with the curl command line tool looks like

curl -X POST "https://translation.googleapis.com/language/translate/v2?key=$GOOGLE_API_KEY&q=rust&source=en&target=fr"
Enter fullscreen mode Exit fullscreen mode

We are sending 3 parameters:

  • key: the Google API key
  • q: the words to translate, here rust
  • source: the language of the words, here en for English
  • target: the target language to translate to, here fr for French

The list of languages available can be found here.

The output is

{
  "data": {
    "translations": [
      {
        "translatedText": "rouiller"
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Which isn't exact. to rust == rouiller (verb), rust == rouille (noun).

Anyway...

Now, let's do the same with Rust!

Call in Rust with Hardcoded Data

First in Cargo.toml, add the dependencies:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.10", features = ["json", "native-tls", "cookies"] }
tokio = { version = "0.2", features = ["full"] }
serde_json = "1.0"
Enter fullscreen mode Exit fullscreen mode

And now the code in src/main.rs:

use std::env;
use std::collections::HashMap;

use reqwest::Error;
use serde::Deserialize;

#[derive(Deserialize, Debug)]
struct Translation {
    #[serde(alias = "translatedText")]
    translated_text: String,
}

#[derive(Deserialize, Debug)]
struct Translations {
    translations: Vec<Translation>,
}

#[derive(Deserialize, Debug)]
struct Data {
    data: Translations,
}

#[tokio::main]
async fn main() -> Result<(), Error> {
    let base_endpoint = "https://translation.googleapis.com/language/translate/v2";
    let args: Vec<String> = env::args().collect();
    let api_key = match env::var("GOOGLE_API_KEY") {
        Ok(val) => val,
        Err(_e) => panic!("Set up the GOOGLE_API_KEY environment variable first"),
    };

    println!(
        "endpoint: {:?}, args: {:?}, key: {:?}",
        base_endpoint, args, api_key
    );

    let query = "rust";
    let source = "en";
    let target = "fr";

    let mut map = HashMap::new();
    map.insert("q", query);
    map.insert("source", source);
    map.insert("target", target);
    map.insert("key", &api_key);

    let request_url = format!("{base}?key={key}&q={query}&source={source}&target={target}", base = base_endpoint, key = api_key, query = query, source = source, target = target);
    // let request_url = format!("{base}?key={key}", base = base_endpoint, key = &api_key);

    let client = reqwest::Client::new();

    let response = client.post(&request_url).form(&map).send().await?;

    let text_response = response.text().await?;

    let translations= serde_json::from_str::<Data>(&text_response).unwrap();

    println!("{:?}", translations);

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

This works ok, now for the next steps:

Translate in a Function

Let's put the code to translate in its own function below the main function, so we can reuse it on every sentence of a file.

#[tokio::main]
async fn translate(query: &str, source: &str, target: &str, api_key: &str) -> Result<Data, Error> {
    let base_endpoint = "https://translation.googleapis.com/language/translate/v2";
    let mut map = HashMap::new();
    map.insert("q", query);
    map.insert("source", source);
    map.insert("target", target);
    map.insert("key", &api_key);

    let request_url = format!("{base}?key={key}&q={query}&source={source}&target={target}", base = base_endpoint, key = api_key, query = query, source = source, target = target);
    // let request_url = format!("{base}?key={key}", base = base_endpoint, key = &api_key);

    let client = reqwest::Client::new();

    let response = client.post(&request_url).form(&map).send().await?;

    let text_response = response.text().await?;

    let translations= serde_json::from_str::<Data>(&text_response).unwrap();

    Ok(translations)
}
Enter fullscreen mode Exit fullscreen mode

Take Arguments

Now we'll take a file name, source and target languages as command arguments.
Where we were getting arguments before, now we'll make sure that we have the exact number

    if args.len() != 4 {
        panic!("You need to pass a text file name, a source and a target language");
    }

    println!(
        "args: {:?}, key: {:?}",
        args, api_key
    );

    let source = &args[2];
    let target = &args[3];
Enter fullscreen mode Exit fullscreen mode

We used to build and run the app with $ cargo run. Now to pass arguments, there's a trick. This is not needed when running the built app directly, but since we are running ir with cargo, we have to use -- to separate arguments for cargo and arguments for the app. So we do $ cargo run -- file.txt fr en for example.

Read Lines

We'll create a src/utils.rs file with a function to read lines.

use std::fs::File;
use std::io::{self, BufRead};
use std::path::Path;

pub fn read_lines<P>(filename: P) -> io::Result<io::Lines<io::BufReader<File>>>
where
    P: AsRef<Path>,
{
    let file = File::open(filename)?;
    Ok(io::BufReader::new(file).lines())
}
Enter fullscreen mode Exit fullscreen mode

And we have to import it at the top of our src/main.rs file.

mod utils;
use crate::utils::read_lines;
Enter fullscreen mode Exit fullscreen mode

Translate Lines

Let's read the file, loop through the file and translate the lines one by one:

    if let Ok(lines) = read_lines(&args[1]) {
        for line in lines {
            if let Ok(row) = line {
                let parsed_row = row.replace("\u{a0}", "");
                println!("{:?}", parsed_row);
                let translation = match translate(&parsed_row, source, target, &api_key) {
                    Ok(t) => {
                        let t2 = String::from(&t.data.translations[0].translated_text);
                        t2
                    }
                    Err(_) => String::from(""),
                };
                println!("{:?}", translation);
            }
        }
    }
Enter fullscreen mode Exit fullscreen mode

Conclusion

So now we have this src/main.rs:

use std::env;
use std::collections::HashMap;

use reqwest::Error;
use serde::Deserialize;

mod utils;
use crate::utils::read_lines;

#[derive(Deserialize, Debug)]
struct Translation {
    #[serde(alias = "translatedText")]
    translated_text: String,
}

#[derive(Deserialize, Debug)]
struct Translations {
    translations: Vec<Translation>,
}

#[derive(Deserialize, Debug)]
struct Data {
    data: Translations,
}

fn main() {
    let api_key = match env::var("GOOGLE_API_KEY") {
        Ok(val) => val,
        Err(_e) => panic!("Set up the GOOGLE_API_KEY environment variable first"),
    };

    let args: Vec<String> = env::args().collect();

    if args.len() != 4 {
        panic!("You need to pass a text file name, a source and a target language");
    }

    println!(
        "args: {:?}, key: {:?}",
        args, api_key
    );

    let source = &args[2];
    let target = &args[3];

    // let translations = translate(query, source, target, &api_key);

    // println!("{:?}", translations);

    if let Ok(lines) = read_lines(&args[1]) {
        for line in lines {
            if let Ok(row) = line {
                let parsed_row = row.replace("\u{a0}", "");
                // println!("{}", parsed_row);
                let translation = match translate(&parsed_row, source, target, &api_key) {
                    Ok(t) => {
                        let t2 = String::from(&t.data.translations[0].translated_text);
                        t2
                    }
                    Err(_) => String::from(""),
                };
                println!("{}", translation);
            }
        }
    }
}

#[tokio::main]
async fn translate(query: &str, source: &str, target: &str, api_key: &str) -> Result<Data, Error> {
    let base_endpoint = "https://translation.googleapis.com/language/translate/v2";
    let mut map = HashMap::new();
    map.insert("q", query);
    map.insert("source", source);
    map.insert("target", target);
    map.insert("key", &api_key);

    let request_url = format!("{base}?key={key}&q={query}&source={source}&target={target}", base = base_endpoint, key = api_key, query = query, source = source, target = target);

    let client = reqwest::Client::new();

    let response = client.post(&request_url).form(&map).send().await?;

    let text_response = response.text().await?;

    let translations= serde_json::from_str::<Data>(&text_response).unwrap();

    Ok(translations)
}
Enter fullscreen mode Exit fullscreen mode

And the complete code can be found in this repository.

Thanks for reading!

Discussion (0)