Text translation using Azure AI Translator in a NestJS application
Tags: nestjs, generativeai, tutorial
Published: 2024-03-27 17:23:26

##Introduction

I am a language learner who learns Mandarin and Spanish in my spare time. When I discovered that text translation using Azure AI is possible, I wanted to leverage the strength of the translation service of Azure Cognitive Service in my hobby. Therefore, I built a NestJS application to translate texts between two languages through Azure AI's translator service. 

###Sign up for Azure AI Service for free

1. Navigate to https://azure.microsoft.com/en-us/free/ai-services/ to sign up for a free account.
2. Create a Translator service in the Azure Portal
3. Click APIs and Keys in the menu, and copy the API Key, location, and Translation URL

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

###Generate a Translation Module
```
nest g mo translation
nest g co translation/presenters/http/translator --flat
nest g s translation/application/azureTranslator --flat
```

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

###Define Azure OpenAI environment variables
```text
// .env.example

PORT=3000
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
AI_SERVICE=azureOpenAI
```

Copy `.env.example` to `.env`, and replace `AZURE_OPENAI_TRANSLATOR_API_KEY` and `AZURE_OPENAI_TRANSLATOR_URL` with the actual API Key and the translator URL, respectively.

- AZURE_OPENAI_TRANSLATOR_API_KEY - API key of Azure OpenAI translator service
- AZURE_OPENAI_TRANSLATOR_URL - Translator URL of Azure OpenAI translator service
- AZURE_OPENAI_TRANSLATOR_API_VERSION - API version of Azure OpenAI translator service
- AZURE_OPENAI_LOCATION - Location of Azure OpenAI translator service
- AI_SERVICE - Generative AI service to be used in the application

Add `.env` to the `.gitignore` file to ensure we don't accidentally commit the Azure OpenAI Translator API Key to the GitHub repo.

###Add configuration files

The project has three 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
// env.config.ts

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

dotenv.config();

export const env = {
  PORT: parseInt(process.env.PORT || '3000'),
  AZURE_OPENAI_TRANSLATOR: {
    KEY: process.env.AZURE_OPENAI_TRANSLATOR_API_KEY || '',
    URL: process.env.AZURE_OPENAI_TRANSLATOR_URL || '',
    LOCATION: process.env.AZURE_OPENAI_LOCATION || 'eastasia',
    VERSION: process.env.AZURE_OPENAI_TRANSLATOR_API_VERSION || '3.0',
  },
  AI_SERVICE: (process.env.AI_SERVICE || 'azureOpenAI') as Integration,
};
```

`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 AI Model, Google Translate Cloud 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 target language. 

###Define Translation DTO 

```typescript
// languages.enum

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 LanguageCodeType = z.infer<typeof ZOD_LANGUAGE_CODES>;
```

```typescript
// translate-text.dto.ts

import { z } from 'zod';
import { ZOD_LANGUAGE_CODES } from '~translation/application/enums/languages.enum';

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.

###Implement Azure Translator Service

This application will also support langchain.js, Google Gemini, and Google Translate Cloud API to translate texts. Therefore, I created a Translator interface, and all services that implement the interface must fulfill the contract. 

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

import { LanguageCodeType } from '../enums/languages.enum';

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

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

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

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

```typescript
// translator.interface.ts

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

```typescript
// azure-translator.service.ts

type AzureTranslateResponse = {
  translations: [
    {
      text: string;
      to: string;
    },
  ];
};

@Injectable()
export class AzureTranslatorService implements Translator {
  constructor(private httpService: HttpService) {}

  async translate({ text, srcLanguageCode, targetLanguageCode }: TranslateInput): Promise<TranslationResult> {
    const data = [{ text }];
    const result$ = this.httpService
      .post<AzureTranslateResponse[]>(env.AZURE_OPENAI_TRANSLATOR.URL, data, {
        headers: {
          'Ocp-Apim-Subscription-Key': env.AZURE_OPENAI_TRANSLATOR.KEY,
          'Ocp-Apim-Subscription-Region': env.AZURE_OPENAI_TRANSLATOR.LOCATION,
          'Content-type': 'application/json',
          'X-ClientTraceId': v4(),
        },
        params: {
           'api-version': env.AZURE_OPENAI_TRANSLATOR.VERSION,
           from: srcLanguageCode,
           to: targetLanguageCode,
        },
        responseType: 'json',
      })
      .pipe(
         map(({ data }) => data?.[0]?.translations?.[0].text || 'No result'),
         map((text) => ({
           text,
           aiService: <Integration>'azureOpenAI',
         })),
      );
    return firstValueFrom(result$);
  }
}
```

The translate method of AzureTranslatorService makes a POST request to translate the text from the source language code to the target language code. The `httpService` returns an Observable; therefore, the Observable is passed to the `firstValueFrom` RxJS operator to convert into a Promise.

###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) {}

  @ApiBody({
    description: 'An intance of TranslatTextDto',
    required: true,
    schema: {
      type: 'object',
      properties: {
        text: {
          type: 'string',
          description: 'text to be translated',
        },
        srcLanguageCode: {
          type: 'string',
          description: 'source language code',
          enum: ['en', 'es', 'zh-Hans', 'zh-Hant', 'vi', 'ja'],
        },
        targetLanguageCode: {
          type: 'string',
          description: 'target language code',
          enum: ['en', 'es', 'zh-Hans', 'zh-Hant', 'vi', 'ja'],
        },
      },
    },
    examples: {
      greeting: {
        value: {
          text: 'Good morning, good afternoon, good evening.',
          srcLanguageCode: 'en',
          targetLanguageCode: 'es',
        },
      },
    },
  })
  @ApiResponse({
    description: 'The translated text',
    schema: {
      type: 'object',
      properties: {
        text: { type: 'string', description: 'translated text' },
        aiService: { type: 'string', description: 'AI service' },
      },
    },
    status: 200,
  })
  @HttpCode(200)
  @Post()
  @UsePipes(new ZodValidationPipe(translateTextSchema))
  translate(@Body() dto: TranslateTextDto): Promise<TranslationResult> {
    return this.translatorService.translate(dto);
  }
}
```

The `TranslatorController` injects `Translator`, which is an instance of `AzureTranslatorService`. The endpoint invokes the `translate` method to perform text translation using Azure OpenAI.

###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=azureOpenAI
```

```typescript
// integration.type.ts

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

```typescript
// translator.module.ts

@Module({
  imports: [HttpModule],
  controllers: [TranslatorController],
})
export class TranslationModule {
  static register(type: Integration = 'azureOpenAI'): DynamicModule {
    const serviceMap = new Map<Integration, any>();
    serviceMap.set('azureOpenAI', AzureTranslatorService);
    const translatorService = serviceMap.get(type) || AzureTranslatorService;

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

    return {
      module: TranslationModule,
      providers,
    };
  }
}
```

In `TranslationModule`, I define a register method that returns a `DynamicModule`. When `type` is azureOpenAI, the `TRANSLATOR` token provides `AzureTranslatorService`. 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

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

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

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

###Dockerize the application

```typescript
// .dockerignore

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

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

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

# Install the dependencies
RUN npm install

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.

```text
//  .env.docker.example

PORT=3000
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
```

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

```typescript
// docker-compose.yaml

version: '3.8'

services:
  backend:
    build:
      context: ./nestjs-genai-translation
      dockerfile: Dockerfile
    environment:
      - PORT=${PORT}
      - 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}
    ports:
      - "${PORT}:${PORT}"
    networks:
      - ai
    restart: always

networks:
  ai:
```

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 Azure OpenAI Translator Service to solve a real-world problem. I only scratched the surface of Azure OpenAI, and Microsoft offers many services. 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
- Azure OpenAI Translator Service: https://learn.microsoft.com/en-us/azure/ai-services/translator/text-translation-overview