Text translation using Google Cloud Translation API in a NestJS application

Connie Leung - Mar 31 - - Dev Community

Introduction

I am a language learner who learns Mandarin and Spanish in my spare time. When I discovered that text translation using Google Cloud Translation API is possible, I wanted to leverage the strength of Google Cloud and a translation-specialized large language model (LLM) in my hobby. Therefore, I built a NestJS application to translate texts between two languages through Cloud API and LLM.

Authenticate Application Default Credentials (ADC) in the local environment

Navigate to https://cloud.google.com/docs/authentication/provide-credentials-adc#local-dev and follow the instructions to create the credential. I use a Mac; therefore, the credential JSON file is created in ~/.config/gcloud/application_default_credentials.json when the authentication is successful.

Create a new NestJS Project

nest new nestjs-genai-translation
Enter fullscreen mode Exit fullscreen mode

Install dependencies

npm i --save-exact  zod @nestjs/swagger @nestjs/throttler dotenv compression helmet @google-cloud/translate
Enter fullscreen mode Exit fullscreen mode

Generate a Translation Module

nest g mo translation
nest g co translation/presenters/http/translator --flat
nest g s translation/application/googleTranslator --flat
Enter fullscreen mode Exit fullscreen mode

Create a Translation module, a controller, and a service for the API.

Define Translation API environment variables

// .env.example

PORT=3000
APP_ENV=development
GOOGLE_PROJECT_ID=<google project id>
AI_SERVICE=google_translate
Enter fullscreen mode Exit fullscreen mode

Copy .env.example to .env, and replace GOOGLE_PROJECT_ID with the id of an existing Google Cloud project.

  • APP_ENV - Application environment. Valid values are development and production, and the default value is production.
  • GOOGLE_PROJECT_ID - ID of an existing Google Cloud Project
  • AI_SERVICE - Generative AI service to be used in the application

Add .env to the .gitignore file to prevent accidentally committing the Gemini API Key to the GitHub repo.

// .gitignore

.env
Enter fullscreen mode Exit fullscreen mode

Add configuration files

The project has 3 configuration files. validate.config.ts validates the payload is valid before any request can route to the controller to execute

// validate.config.ts

import { ValidationPipe } from '@nestjs/common';

export const validateConfig = new ValidationPipe({
  whitelist: true,
  stopAtFirstError: true,
  forbidUnknownValues: false,
});
Enter fullscreen mode Exit fullscreen mode

env.config.ts extracts the environment variables from process.env and stores the values in the env object.

// app_env_names.enum.ts

export enum APP_ENV_NAMES {
  DEVELOPMENT = 'development',
  PRODUCTION = 'production',
}
Enter fullscreen mode Exit fullscreen mode
// env.config.ts

import dotenv from 'dotenv';
import { APP_ENV_NAMES } from '~core/enums/app_env_names.enum';
import { Integration } from '~core/types/integration.type';

dotenv.config();

export const env = {
  PORT: parseInt(process.env.PORT || '3000'),
  APP_ENV: (process.env.APP_ENV || APP_ENV_NAMES.PRODUCTION) as APP_ENV_NAMES,
  AI_SERVICE: (process.env.AI_SERVICE || 'langchain_googleChatModel') as Integration,
  GOOGLE: {
    PROJECT_ID: process.env.GOOGLE_PROJECT_ID || '',
  },
};
Enter fullscreen mode Exit fullscreen mode

throttler.config.ts defines the rate limit of the Translation API

// throttler.config.ts

import { ThrottlerModule } from '@nestjs/throttler';

export const throttlerConfig = ThrottlerModule.forRoot([
  {
    ttl: 60000,
    limit: 10,
  },
]);
Enter fullscreen mode Exit fullscreen mode

Each route allows ten requests in 60,000 milliseconds or 1 minute.

Bootstrap the application

// bootstrap.ts

export class Bootstrap {
  private app: NestExpressApplication;

  async initApp() {
    this.app = await NestFactory.create(AppModule);
  }

  enableCors() {
    this.app.enableCors();
  }

  setupMiddleware() {
    this.app.use(express.json({ limit: '1000kb' }));
    this.app.use(express.urlencoded({ extended: false }));
    this.app.use(compression());
    this.app.use(helmet());
  }

  setupGlobalPipe() {
    this.app.useGlobalPipes(validateConfig);
  }

  async startApp() {
    await this.app.listen(env.PORT);
  }

  setupSwagger() {
    const config = new DocumentBuilder()
      .setTitle('Generative AI Translator')
      .setDescription('Integrate with Generative AI to translate a text from one language to another language')
      .setVersion('1.0')
      .addTag('Azure OpenAI, Langchain, Gemini 1.0 Pro Model, Google Cloud Translation API')
      .build();
    const document = SwaggerModule.createDocument(this.app, config);
    SwaggerModule.setup('api', this.app, document);
  }
}
Enter fullscreen mode Exit fullscreen mode

Add a Bootstrap class to set up Swagger, middleware, global validation, CORS, and finally, application start.

// main.ts

import { Bootstrap } from '~core/bootstrap';

async function bootstrap() {
  const bootstrap = new Bootstrap();
  await bootstrap.initApp();
  bootstrap.enableCors();
  bootstrap.setupMiddleware();
  bootstrap.setupGlobalPipe();
  bootstrap.setupSwagger();
  await bootstrap.startApp();
}
bootstrap()
  .then(() => console.log('The application starts successfully'))
  .catch((error) => console.error(error));
Enter fullscreen mode Exit fullscreen mode

The bootstrap function enables CORS, registers middleware to the application, sets up Swagger documentation, and uses a global pipe to validate payloads.

I have laid down the groundwork, and the next step is to add routes to receive payload and translate texts between the source language and the target language.

Define Translation DTO

// languages_codes.validation.ts

import { z } from 'zod';

const LANGUAGE_CODES = {
  English: 'en',
  Spanish: 'es',
  'Simplified Chinese': 'zh-Hans',
  'Traditional Chinese': 'zh-Hant',
  Vietnamese: 'vi',
  Japanese: 'ja',
} as const;

export const ZOD_LANGUAGE_CODES = z.nativeEnum(LANGUAGE_CODES, {
  required_error: 'Language code is required',
  invalid_type_error: 'Language code is invalid',
});
export type LanguageCodesType = z.infer<typeof ZOD_LANGUAGE_CODES>;
Enter fullscreen mode Exit fullscreen mode
// translate-text.dto.ts

import { z } from 'zod';
import { ZOD_LANGUAGE_CODES } from '~translation/application/validations/language_codes.validation';

export const translateTextSchema = z
  .object({
    text: z.string({
      required_error: 'Text is required',
    }),
    srcLanguageCode: ZOD_LANGUAGE_CODES,
    targetLanguageCode: ZOD_LANGUAGE_CODES,
  })
  .required();

export type TranslateTextDto = z.infer<typeof translateTextSchema>;
Enter fullscreen mode Exit fullscreen mode

translateTextSchema accepts a text, a source language code, and a target language code. Then, I use zod to infer the type of translateTextSchema and assign it to TranslateTextDto.

Define Translator Interface

This application is designed to translate texts using Azure OpenAI, langchain.js, Gemini Pro 1.0 Model, or Google Cloud Translation API. Therefore, I created a Translator interface and all services that implement the interface must fulfill the contract.

//  translator-input.interface.ts

import { LanguageCodesType } from '../validations/language_codes.validation';

export interface TranslateInput {
  text: string;
  srcLanguageCode: LanguageCodesType;
  targetLanguageCode: LanguageCodesType;
}
Enter fullscreen mode Exit fullscreen mode
// translate-result.interface.ts

import { Integration } from '~core/types/integration.type';

export interface TranslationResult {
  text: string;
  aiService: Integration;
}
Enter fullscreen mode Exit fullscreen mode
// translator.interface.ts

import { TranslationResult } from './translation-result.interface';
import { TranslateInput } from './translator-input.interface';

export interface Translator {
  translate(input: TranslateInput): Promise<TranslationResult>;
}
Enter fullscreen mode Exit fullscreen mode

Implement Google Translator Service

// translator.constant.ts

export const GOOGLE_TRANSLATE = 'GOOGLE_TRANSLATE';
Enter fullscreen mode Exit fullscreen mode
// google-translate.provider.ts

import { v2 } from '@google-cloud/translate';
import { Provider } from '@nestjs/common';
import { env } from '~configs/env.config';
import { GOOGLE_TRANSLATE } from '../constants/translator.constant';

export const GOOGLE_TRANSLATE_PROVIDER: Provider = {
  provide: GOOGLE_TRANSLATE,
  useFactory: () => new v2.Translate({ projectId: env.GOOGLE.PROJECT_ID }),
};
Enter fullscreen mode Exit fullscreen mode

GOOGLE_TRANSLATE_PROVIDER is a provider that instantiates an instance of Google Translate.

// google-translator.service.ts

// Omit import statements due to brevity

@Injectable()
export class GoogleTranslatorService implements Translator {
  constructor(@Inject(GOOGLE_TRANSLATE) private translateApi: v2.Translate) {}

async translate({ text, srcLanguageCode: from, targetLanguageCode }: TranslateInput): Promise<TranslationResult> {
    const to = this.convertLanguageCode(targetLanguageCode);
    const [translatedText] = await this.translateApi.translate(text, {
      from,
      to,
    });

    return {
      text: translatedText,
      aiService: 'google_translate',
    };
  }

  private convertLanguageCode(languageCode: LanguageCodesType) {
    let toLanguage = `${languageCode}`;
    if (languageCode === 'zh-Hans') {
      toLanguage = 'zh-CN';
    } else if (languageCode === 'zh-Hant') {
      toLanguage = 'zh-TW';
    }
    return toLanguage;
  }
}
Enter fullscreen mode Exit fullscreen mode

The translate method of GoogleTranslatorService uses the language codes to translate the text. Azure OpenAI and Google Cloud Translation API have different language codes for Simplified and Traditional Chinese; therefore, I wrote a function, convertLanguageCode, to convert them. Then, the final language codes and the text are passed to the translate method to obtain the result. Finally, the service returns the translated text to the controller, and the controller returns it in the HTTP response.

Implement Translator Controller

// zod-validation.pipe.ts

export class ZodValidationPipe implements PipeTransform {
  constructor(private schema: ZodSchema) {}

  transform(value: unknown) {
    try {
      const parsedValue = this.schema.parse(value);
      return parsedValue;
    } catch (error) {
      console.error(error);
      if (error instanceof ZodError) {
        throw new BadRequestException(error.errors?.[0]?.message || 'Validation failed');
      } else if (error instanceof Error) {
        throw new BadRequestException(error.message);
      }
      throw error;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

ZodValidationPipe is a pipe that validates the payload against the Zod schema. When the validation is successful, the payload will be parsed and returned. When the validation fails, the pipe intercepts the ZodError and returns an instance of BadRequestException.

// translator.controller.ts

// Omit the import statements to save space

@ApiTags('Translator')
@Controller('translator')
export class TranslatorController {
  constructor(@Inject(TRANSLATOR) private translatorService: Translator) {}

  @HttpCode(200)
  @Post()
  @UsePipes(new ZodValidationPipe(translateTextSchema))
  translate(@Body() dto: TranslateTextDto): Promise<TranslationResult> {
    return this.translatorService.translate(dto);
  }
}
Enter fullscreen mode Exit fullscreen mode

The TranslatorController injects Translator that is an instance of GoogleTranslatorService. The endpoint invokes the translate method to perform text translation using the Google Cloud Translation API.

Dynamic registration

This application registers the translation service based on the AI_SERVICE environment variable. The value of the environment variable is one of azureOpenAI, langchain_googleChatModel, and google_translate.

// .env.example

AI_SERVICE=google_translate
Enter fullscreen mode Exit fullscreen mode
// integration.type.ts

export type Integration = 'azureOpenAI' | 'langchain_googleChatModel' | 'google_translate';
Enter fullscreen mode Exit fullscreen mode
// translator.module.ts

// Omit import statements for brevity

function createProviders(serviceType: Integration) {
  const serviceMap = new Map<Integration, any>();
  serviceMap.set('azureOpenAI', AzureTranslatorService);
  serviceMap.set('langchain_googleChatModel', LangchainTranslatorService);
  serviceMap.set('google_translate', GoogleTranslatorService);
  const translatorService = serviceMap.get(serviceType);

  const providers: Provider[] = [
    {
      provide: TRANSLATOR,
      useClass: translatorService,
    },
  ];

  if (serviceType === 'langchain_googleChatModel') {
    providers.push(GEMINI_LLM_CHAIN_PROVIDER);
  } else if (serviceType === 'google_translate') {
    providers.push(GOOGLE_TRANSLATE_PROVIDER);
  }

  return providers;
}

@Module({
  imports: [HttpModule],
  controllers: [TranslatorController],
})
export class TranslationModule {
  static register(type: Integration = 'azureOpenAI'): DynamicModule {
    const logger = new Logger(TranslationModule.name);
    const isProduction = env.APP_ENV === APP_ENV_NAMES.PRODUCTION;
    // google_translation works in local environment. Default to azureOpenAI in production
    const serviceType = isProduction && type === 'google_translate' ? 'azureOpenAI' : type;

    logger.log(`isProduction? ${isProduction}`);
    logger.log(`serviceType? ${serviceType}`);

    return {
      module: TranslationModule,
      providers: createProviders(serviceType),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

In TranslationModule, I define a register method that returns a DynamicModule. When type is google_translate, the TRANSLATOR token provides GoogleTranslatorService. Next, TranslationModule.register(env.AI_SERVICE) creates a TranslationModule that I import in the AppModule.

// app.module.ts

@Module({
  imports: [throttlerConfig, TranslationModule.register(env.AI_SERVICE)],
  controllers: [AppController],
  providers: [
    AppService,
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Test the endpoints

I can test the endpoints with cURL, Postman or Swagger documentation after launching the application.

npm run start:dev
Enter fullscreen mode Exit fullscreen mode

The URL of the Swagger documentation is http://localhost:3000/api.

In cURL

curl --location 'http://localhost:3000/translator' \
--header 'Content-Type: application/json' \
--data '{
    "text": "My name is John\n\nI am a Chinese",
    "srcLanguageCode": "en",
    "targetLanguageCode": "es"
}'
Enter fullscreen mode Exit fullscreen mode

Dockerize the application

// .dockerignore

.git
.gitignore
node_modules/
dist/
Dockerfile
.dockerignore
npm-debug.log
Enter fullscreen mode Exit fullscreen mode

Create a .dockerignore file for Docker to ignore some files and directories.

// Dockerfile

# Use an official Node.js runtime as the base image
FROM node:20-alpine

# Set the working directory in the container
WORKDIR /app

# Copy package.json and package-lock.json to the working directory
COPY package*.json tsconfig.json ./

# Install the dependencies
RUN npm install

# Build the NestJS application
RUN npm run build

# Copy the rest of the application code to the working directory
COPY . .

# Expose a port (if your application listens on a specific port)
EXPOSE 3000

# Define the command to run your application
CMD [ "npm", "start" ]
Enter fullscreen mode Exit fullscreen mode

I added the Dockerfile that installs the dependencies, builds the NestJS application, and starts it at port 3000.

//  .env.docker.example

PORT=3000
APP_ENV=<application environment>
AZURE_OPENAI_TRANSLATOR_API_KEY=<translator api key>
AZURE_OPENAI_TRANSLATOR_URL=<translator url>/translate
AZURE_OPENAI_TRANSLATOR_API_VERSION="3.0"
AZURE_OPENAI_LOCATION=eastasia
GOOGLE_GEMINI_API_KEY=<google gemini api key>
GOOGLE_GEMINI_MODEL=gemini-pro
AI_SERVICE=langchain_googleChatModel
GOOGLE_PROJECT_ID=<google project id>
Enter fullscreen mode Exit fullscreen mode

.env.docker.example stores the relevant environment variables that I copied from the NestJS application.

// docker-compose.yaml

version: '3.8'

services:
  backend:
    build:
      context: ./nestjs-genai-translation
      dockerfile: Dockerfile
    volumes:
      - ~/.config/gcloud/application_default_credentials.json:/gcp/creds.json:ro
    environment:
      - PORT=${PORT}
      - APP_ENV=${APP_ENV}
      - AZURE_OPENAI_TRANSLATOR_API_KEY=${AZURE_OPENAI_TRANSLATOR_API_KEY}
      - AZURE_OPENAI_TRANSLATOR_URL=${AZURE_OPENAI_TRANSLATOR_URL}
      - AZURE_OPENAI_TRANSLATOR_API_VERSION=${AZURE_OPENAI_TRANSLATOR_API_VERSION}
      - AZURE_OPENAI_LOCATION=${AZURE_OPENAI_LOCATION}
      - GOOGLE_GEMINI_API_KEY=${GOOGLE_GEMINI_API_KEY}
      - GOOGLE_GEMINI_MODEL=${GOOGLE_GEMINI_MODEL}
      - AI_SERVICE=${AI_SERVICE}
      - GOOGLE_PROJECT_ID=${GOOGLE_PROJECT_ID}
      - GOOGLE_APPLICATION_CREDENTIALS=/gcp/creds.json
    ports:
      - "${PORT}:${PORT}"
    networks:
      - ai
    restart: always

networks:
  ai:
Enter fullscreen mode Exit fullscreen mode

In docker compose file, I mounted /gcp/creds.json:ro to ~/.config/gcloud/application_default_credentials.json on my local machine. application_default_credentials.json is the credential JSON file that Google Cloud generated for me when I signed up.

I added the docker-compose.yaml in the root folder, which was responsible for creating the NestJS application container.

This concludes my blog post about using Google Cloud Translation API to solve a real-world problem. I only scratched the surface of Google Cloud APIs that can solve problems in different domains. I hope you like the content and continue to follow my learning experience in Angular, NestJS, and other technologies.

Resources:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .