Text translation using Google Cloud Translation API in a NestJS application
Tags: nestjs, tutorial, googletranslate, typescript
Published: 2024-03-31 10:27:13


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

```bash
nest new nestjs-genai-translation
```

###Install dependencies

```bash
npm i --save-exact  zod @nestjs/swagger @nestjs/throttler dotenv compression helmet @google-cloud/translate
```

###Generate a Translation Module

```bash
nest g mo translation
nest g co translation/presenters/http/translator --flat
nest g s translation/application/googleTranslator --flat
```

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

###Define Translation API environment variables

```text
// .env.example

PORT=3000
APP_ENV=development
GOOGLE_PROJECT_ID=<google project id>
AI_SERVICE=google_translate
```

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.

```typescript
// .gitignore

.env
```

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

```typescript
// validate.config.ts

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

export const validateConfig = new ValidationPipe({
  whitelist: true,
  stopAtFirstError: true,
  forbidUnknownValues: false,
});
```

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

```typescript
// app_env_names.enum.ts

export enum APP_ENV_NAMES {
  DEVELOPMENT = 'development',
  PRODUCTION = 'production',
}
```

```typescript
// 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 || '',
  },
};
```

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

```typescript
// throttler.config.ts

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

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

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

###Bootstrap the application

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

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

```typescript
// 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));
```

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 

```typescript
// 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>;
```

```typescript
// 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>;
```

`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. 

```typescript
//  translator-input.interface.ts

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

export interface TranslateInput {
  text: string;
  srcLanguageCode: LanguageCodesType;
  targetLanguageCode: LanguageCodesType;
}
```

```typescript
// translate-result.interface.ts

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

export interface TranslationResult {
  text: string;
  aiService: Integration;
}
```

```typescript
// translator.interface.ts

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

export interface Translator {
  translate(input: TranslateInput): Promise<TranslationResult>;
}
```

###Implement Google Translator Service

```typescript
// translator.constant.ts

export const GOOGLE_TRANSLATE = 'GOOGLE_TRANSLATE';
```

```typescript
// 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 }),
};
```

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

```typescript
// 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;
  }
}
```

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

```typescript
// 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;
    }
  }
}
```

`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`.  

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

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`.

```text
// .env.example

AI_SERVICE=google_translate
```

```typescript
// integration.type.ts

export type Integration = 'azureOpenAI' | 'langchain_googleChatModel' | 'google_translate';
```

```typescript
// 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),
    };
  }
}
```

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`.

```typescript
// app.module.ts

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

###Test the endpoints

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

```bash
npm run start:dev
```

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

In cURL

```bash
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"
}'
```

###Dockerize the application

```text
// .dockerignore

.git
.gitignore
node_modules/
dist/
Dockerfile
.dockerignore
npm-debug.log
```

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

```text
// 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" ]
```

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

```text
//  .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>
```

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

```yaml
// 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:
```

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:

- Github Repo: https://github.com/railsstudent/fullstack-genai-translation/tree/main/nestjs-genai-translation
- Google translate npm: https://www.npmjs.com/package/@google-cloud/translate