Text translation using Azure OpenAI Translator in a NestJS application

Reading Time: 7 minutes

Loading

Introduction

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

let's go

Sign up for Azure OpenAI 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

nest new nestjs-genai-translation

Install dependencies

npm i --save-exact  zod @nestjs/swagger @nestjs/throttler dotenv compression helmet

Generate a Translation Module

nest g mo translation
nest g co translation/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

// .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, 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

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

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

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

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

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

// 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>;
// 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 and 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.

//  translator-input.interface.ts

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

export interface TranslateInput {
  text: string;
  srcLanguageCode: LanguageCodeType;
  targetLanguageCode: LanguageCodeType;
}
// translate-result.interface.ts

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

export interface TranslationResult {
  text: string;
  aiService: Integration;
}
// translator.interface.ts

export interface Translator {
  translate(input: TranslateInput): Promise<TranslationResult>;
}
// 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

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

// 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 that 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.

// .env.example

AI_SERVICE=azureOpenAI
// integration.type.ts

export type Integration = 'azureOpenAI' | 'langchain_googleChatModel' | 'google_translate';
// 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.

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

npm run start:dev

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

Dockerize the application

// .dockerignore

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

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

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

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

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

  1. Github Repo: https://github.com/railsstudent/fullstack-genai-translation/tree/main/nestjs-genai-translation
  2. Azure OpenAI Translator Service: https://learn.microsoft.com/en-us/azure/ai-services/translator/text-translation-overview