Google Cloud Translation w/ gRPC 3 ways
- 6 minutes read - 1077 wordsGeneral
You’ll need a Google Cloud project with Cloud Translation (translate.googleapis.com
) enabled and a Service Account (and key) with suitable permissions in order to run the following.
BILLING="..." # Your Billing ID (gcloud billing accounts list)
PROJECT="..." # Your Project ID
ACCOUNT="tester"
EMAIL="${ACCOUNT}@${PROJECT}.iam.gserviceaccount.com"
ROLES=(
"roles/cloudtranslate.user"
"roles/serviceusage.serviceUsageConsumer"
)
# Create Project
gcloud projects create ${PROJECT}
# Associate Project with your Billing Account
gcloud billing accounts link ${PROJECT} \
--billing-account=${BILLING}
# Enable Cloud Translation
gcloud services enable translate.googleapis.com \
--project=${PROJECT}
# Create Service Account
gcloud iam service-accounts create ${ACCOUNT} \
--project=${PROJECT}
# Create Service Account Key
gcloud iam service-accounts keys create ${PWD}/${ACCOUNT}.json \
--iam-account=${EMAIL} \
--project=${PROJECT}
# Update Project IAM permissions
for ROLE in "${ROLES[@]}"
do
gcloud projects add-iam-policy-binding ${PROJECT} \
--member=serviceAccount:${EMAIL} \
--role=${ROLE}
done
For the code, you’ll need to install protoc
and preferably have it in your path.
You’ll also need to clone Google’s googleapis
repo. The repo contains Google’s protobuf definitions for all its services
gRPCurl
Let’s test a translation using a known working client:
PROJECT="..." # Your Project ID
# Retain current default auth account
AUTH=$(\
gcloud config get account)
# Activate the Service Account (for use with gcloud)
# Side effect is to change the current auth account
# This account is associated with {EMAIL}
# We will use it to create tokens to access the service
gcloud auth activate-service-account \
--key-file=${PWD}/${ACCOUNT}.json
# Revert to previous default auth account
gcloud config set account ${AUTH}
# Get a token for the service using the Service Account
TOKEN=$(\
gcloud auth print-access-token \
--account=${EMAIL})
GOOGLEAPIS="/path/to/googleapis"
LOCATION="global"
# Path is "translate" but Package is "translation"
PACKAGE_PATH="google/cloud/translate/v3"
PACKAGE_NAME="google.cloud.translation.v3"
SERVICE="TranslationService"
METHOD="TranslateText"
DATA="{
\"contents\": [\"The quick brown fox jumps over the lazy dog\"],
\"source_language_code\": \"en-US\",
\"target_language_code\": \"zh-CN\",
\"parent\": \"projects/${PROJECT}/locations/${LOCATION}\"
}"
ENDPOINT="translate.googleapis.com:443"
grpcurl \
-H "X-Goog-User-Project: ${PROJECT}" \
-H "Authorization: Bearer ${TOKEN}" \
--import-path ${GOOGLEAPIS} \
--proto ${GOOGLEAPIS}/${PACKAGE_PATH}/translation_service.proto \
-d "${DATA}" \
${ENDPOINT} \
${PACKAGE_NAME}.${SERVICE}.${METHOD}
Yields:
{
"translations": [
{
"translatedText": "敏捷的棕色狐狸跳过了懒狗"
}
]
}
.NET
I prefer to use .NET through a container:
# Use the Service Account key with Application Default Credentials
export GOOGLE_APPLICATION_CREDENTIALS=${PWD}/${ACCOUNT}.json
# Folder on the host to contain the app
mkdir app
# Microsoft .NET SDK
# Mount app folder
# Mount Application Default Credentials
podman run \
--interactive --tty --rm \
--volume=${PWD}/app:/app \
--volume=${PWD}/${ACCOUNT}.json:/secrets/key.json \
--env=GOOGLE_APPLICATION_CREDENTIALS=/secrets/key.json \
--workdir=/app mcr.microsoft.com/dotnet/sdk:8.0 \
bash
Google provides .NET|NuGet packages for Cloud Translate v3.
From within the container shell:
dotnet new console
dotnet add package Google.Cloud.Translate.V3 --version 3.6.0
And then edit Program.cs
to contain:
using System;
using Google.Cloud.Translate.V3;
const string project = "{PROJECT}";
const string location = "global";
var parent = String.Format("projects/{0}/locations/{1}",project,location);
var client = TranslationServiceClient.Create();
TranslateTextRequest request = new()
{
Contents = {
"The quick brown fox jumps over the lazy dog"
},
SourceLanguageCode = "en-US",
TargetLanguageCode = "zh-CN",
Parent = parent,
};
TranslateTextResponse response = client.TranslateText(request);
Console.WriteLine(response);
Then:
dotnet run
Rust
# Use the Service Account key with Application Default Credentials
export GOOGLE_APPLICATION_CREDENTIALS=${PWD}/${ACCOUNT}.json
cargo new --bin rust
Because rust’s tonic-build
uses protoc
, it can be confusing if you don’t have protoc
in your PATH and wonder why code isn’t being generated.
Cargo.toml
:
[package]
name = "rust"
version = "0.0.1"
edition = "2021"
[dependencies]
tokio = { version = "1.0", features = ["full"] }
prost = "0.12.4"
prost-types = "0.12.4"
tonic = { version = "0.11.0", features = ["tls", "tls-roots","transport"] }
gcp_auth = "0.11.0"
[build-dependencies]
prost = "0.12.4"
tonic-build = "0.11.0"
build.rs
:
use std::{env, path::PathBuf};
const GOOGLEAPIS: &str = "/path/to/googleapis";
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Required by prost-reflection to create descriptors.bin in correct OUT_DIR
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("unable to determine `OUT_DIR`"));
// `file_descriptor_set_path` appends `.bin` to the file that's created
tonic_build::configure()
.build_client(true)
.build_server(false)
.file_descriptor_set_path(out_dir.join("translate.bin"))
.compile(
&[format!(
"/{}/google/cloud/translate/v3/translation_service.proto",
${GOOGLEAPIS},
],
&[GOOGLEAPIS],
)?;
Ok(())
}
And:
client.rs
:
pub mod google {
pub mod cloud {
pub mod translation {
pub mod v3 {
tonic::include_proto!("google.cloud.translation.v3");
}
}
}
pub mod longrunning {
tonic::include_proto!("google.longrunning");
}
pub mod rpc {
tonic::include_proto!("google.rpc");
}
}
use gcp_auth::AuthenticationManager;
use google::cloud::translation::v3::{
translation_service_client::TranslationServiceClient, TranslateTextRequest,
};
use std::{collections::HashMap, env};
use tonic::{
metadata::MetadataValue,
transport::{channel::ClientTlsConfig, Channel},
Request,
};
const URL: &str = "https://translate.googleapis.com:443";
const CLOUD_PLATFORM: &str = "https://www.googleapis.com/auth/cloud-platform";
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let _ = env::var("GOOGLE_APPLICATION_CREDENTIALS")
.expect("expected GOOGLE_APPLICATION_CREDENTIALS in environment");
let project = env::var("PROJECT")
.expect("expected PROJECT in environment");
let location = env::var("LOCATION")
.unwrap_or_else(|_| "global".to_string());
let uri: tonic::transport::Uri = URL
.parse()
.expect("valid gRPC URL");
let config = ClientTlsConfig::new();
let endpoint = Channel::builder(uri)
.tls_config(config)
.expect("unable to configure TLS");
let channel = endpoint
.connect()
.await
.expect("Unable to connect");
let authentication_manager = AuthenticationManager::new().await?;
let scopes = &[CLOUD_PLATFORM];
let token = authentication_manager.get_token(scopes).await?;
let token: MetadataValue<_> = format!("Bearer {}", token.as_str())
.parse()
.expect("unable to parse token");
let mut client =
TranslationServiceClient::with_interceptor(channel, move |mut req: Request<()>| {
req.metadata_mut().insert("authorization", token.clone());
Ok(req)
});
let parent = format!("projects/{}/locations/{}", project, location);
let msg = TranslateTextRequest {
contents: vec!["The quick brown fox jumps over the lazy dog".to_owned()],
mime_type: "".to_owned(),
source_language_code: "en-US".to_owned(),
target_language_code: "zh-CN".to_owned(),
parent: parent,
model: "".to_owned(),
glossary_config: None,
labels: HashMap::new(),
};
let request = tonic::Request::new(msg);
let response = client.translate_text(request).await?;
let message = response.get_ref();
message.translations.iter().for_each(|translation| {
println!("Translated text: {}", translation.translated_text);
});
Ok(())
}
Golang
For Go, we can use the Google-generated sources for Cloud Translation:
go.mod
:
module golang
go 1.22.0
require (
cloud.google.com/go/translate v1.10.2
golang.org/x/oauth2 v0.17.0
google.golang.org/grpc v1.62.1
)
And:
main.go
:
package main
import (
"context"
"crypto/tls"
"flag"
"fmt"
"log/slog"
"os"
"time"
pb "cloud.google.com/go/translate/apiv3/translatepb"
"golang.org/x/oauth2/google"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/oauth"
)
var (
endpoint = flag.String("endpoint", "translate.googleapis.com:443", "Endpoint of Google Translate service")
)
var (
project string = os.Getenv("PROJECT")
location string = os.Getenv("LOCATION")
)
func main() {
flag.Parse()
parent := fmt.Sprintf("projects/%s/locations/%s",
project,
location,
)
message := &pb.TranslateTextRequest{
Contents: []string{
"The quick brown fox jumps over the lazy dog",
},
MimeType: "",
SourceLanguageCode: "en-US",
TargetLanguageCode: "zh-CN",
Parent: parent,
Model: "",
GlossaryConfig: nil,
Labels: nil,
}
ctx := context.Background()
scopes := []string{
"https://www.googleapis.com/auth/cloud-platform",
}
tokensource, err := google.DefaultTokenSource(ctx, scopes...)
if err != nil {
slog.Info("unable to create DefaultTokenSource", "err", err)
os.Exit(1)
}
conn, err := grpc.Dial(
*endpoint,
grpc.WithTransportCredentials(
credentials.NewTLS(
&tls.Config{
InsecureSkipVerify: true,
},
),
),
grpc.WithPerRPCCredentials(
oauth.TokenSource{
TokenSource: tokensource,
},
),
)
if err != nil {
slog.Info("unable to dial", "err", err)
}
defer conn.Close()
c := pb.NewTranslationServiceClient(conn)
ctx, cancel := context.WithTimeout(
context.Background(),
time.Second,
)
defer cancel()
resp, err := c.TranslateText(ctx, message)
if err != nil {
slog.Error("failed to call TranslateText", "err", err)
}
for _, translation := range resp.Translations {
slog.Info("Translation", "text", translation.TranslatedText)
}
}
Tidy
You may want to revoke the Service Account’s credentials when you’re done:
gcloud auth revoke ${EMAIL}
And delete the Project if you’re certain that you no longer need it:
gcloud auth delete projects ${PROJECT} --quiet