Translating My Resume (and any text file) with Rust
Tags: rust, translate, learning, resume
Published: 2021-03-14 19:57:02

---
title: Translating My Resume (and any text file) with Rust
published: true
description: Creating a simple CLI that uses Google Translate to translate a text file to a supported language
tags: rust, translate, learning, resume
//cover_image: https://direct_url_to_image.jpg
---

__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](https://github.com/ThomasMarcel/rosette)__

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5v4nx0nqj46qh1h4uyse.jpeg)

__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

* An API keyHere we are going to be using the Google Translate API, so you need to create an API key in the [Google Cloud Platform (GCP) dashboard](https://console.cloud.google.com/apis/credentials)
* Rust, cargo, etc: follow the [installation instructions](https://www.rust-lang.org/tools/install)

# 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:

```rust
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
    );
}
```

## 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

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

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](https://cloud.google.com/translate/docs/languages).

The output is

```json
{
  "data": {
    "translations": [
      {
        "translatedText": "rouiller"
      }
    ]
  }
}
```

_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:

```toml
[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"
```

And now the code in `src/main.rs`:

```rust
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(())
}
```

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.

```rust
#[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)
}
```

## 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

```rust
    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];
```

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.

```rust
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())
}
```

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

```rust
mod utils;
use crate::utils::read_lines;
```

## Translate Lines

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

```rust
    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);
            }
        }
    }
```

## Conclusion

So now we have this `src/main.rs`:

```rust
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)
}
```

And the complete code can be found [in this repository](https://github.com/ThomasMarcel/rosette).

Thanks for reading!