Translating my Resume (with Rust)
Table of Contents
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:
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
- Rust, cargo, etc: follow the installation instructions
##
Down to it!
#
Project Creation
- Run
$ cargo new rosette
, this will create a directory calledrosette
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
);
}
#
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"
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"
}
]
}
}
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"
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(())
}
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)
}
#
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];
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())
}
And we have to import it at the top of our src/main.rs
file.
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:
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
:
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.
Thanks for reading!