@nestbolt/translatable
Quick Start
Get up and running with @nestbolt/translatable in minutes -- module setup, entity creation, and automatic API locale resolution.
This guide walks you through a complete setup: registering the module, creating a translatable entity, setting and reading translations, and seeing automatic locale resolution in your API responses.
1. Register the Module
Import TranslatableModule and apply the TranslatableMiddleware in your AppModule:
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm";
import {
TranslatableModule,
TranslatableMiddleware,
} from "@nestbolt/translatable";
@Module({
imports: [
TypeOrmModule.forRoot({
type: "postgres",
host: "localhost",
port: 5432,
database: "myapp",
entities: [__dirname + "/**/*.entity{.ts,.js}"],
synchronize: true, // development only
}),
TranslatableModule.forRoot({
defaultLocale: "en",
fallbackLocales: ["en"],
}),
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(TranslatableMiddleware).forRoutes("*");
}
}The TranslatableModule.forRoot() call registers the module globally -- you do not need to import it in every feature module. The TranslatableInterceptor is automatically registered as a global provider by the module.
The middleware reads the Accept-Language header from incoming requests and sets the locale for the request lifecycle using AsyncLocalStorage. The interceptor then uses this locale to transform translatable fields in the response.
2. Create a Translatable Entity
Create an entity that uses the @Translatable() decorator and TranslatableMixin:
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { Translatable, TranslatableMixin } from "@nestbolt/translatable";
@Entity()
export class Product extends TranslatableMixin(BaseEntity) {
@PrimaryGeneratedColumn()
id: number;
@Translatable()
@Column({ type: "jsonb", default: {} })
name: Record<string, string>;
@Translatable()
@Column({ type: "jsonb", default: {} })
description: Record<string, string>;
@Column()
slug: string;
@Column({ type: "decimal", precision: 10, scale: 2 })
price: number;
}The @Translatable() decorator marks name and description as translatable fields. The TranslatableMixin(BaseEntity) call adds all the translation methods (setTranslation, getTranslation, etc.) to your entity class. Non-translatable fields like slug and price are left untouched.
3. Set Translations
Use the chainable setTranslation method in your service:
import { Injectable } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Product } from "./product.entity";
@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product)
private readonly repo: Repository<Product>,
) {}
async create() {
const product = new Product();
product
.setTranslation("name", "en", "Laptop")
.setTranslation("name", "ar", "حاسوب محمول")
.setTranslation("name", "fr", "Ordinateur portable");
product
.setTranslation("description", "en", "A powerful laptop for professionals")
.setTranslation("description", "ar", "حاسوب محمول قوي للمحترفين")
.setTranslation("description", "fr", "Un ordinateur portable puissant pour les professionnels");
product.slug = "laptop";
product.price = 999.99;
return this.repo.save(product);
}
}You can also set multiple translations at once with setTranslations:
product.setTranslations("name", {
en: "Laptop",
ar: "حاسوب محمول",
fr: "Ordinateur portable",
});4. Read Translations Programmatically
// Get a translation for a specific locale
product.getTranslation("name", "en");
// → "Laptop"
product.getTranslation("name", "ar");
// → "حاسوب محمول"
// Get all translations for a field
product.getTranslations("name");
// → { en: "Laptop", ar: "حاسوب محمول", fr: "Ordinateur portable" }
// Get all locales that have translations
product.locales();
// → ["en", "ar", "fr"]5. Create a Controller
Your controller does not need any translation-specific code. The interceptor handles everything automatically:
import { Controller, Get, Param } from "@nestjs/common";
import { InjectRepository } from "@nestjs/typeorm";
import { Repository } from "typeorm";
import { Product } from "./product.entity";
@Controller("products")
export class ProductController {
constructor(
@InjectRepository(Product)
private readonly repo: Repository<Product>,
) {}
@Get()
findAll() {
return this.repo.find();
}
@Get(":id")
findOne(@Param("id") id: number) {
return this.repo.findOneBy({ id });
}
}6. API Response Examples
With Accept-Language Header
When a client sends a request with the Accept-Language header, translatable fields are resolved to a single string in the requested locale:
GET /products/1
Accept-Language: ar{
"id": 1,
"name": "حاسوب محمول",
"description": "حاسوب محمول قوي للمحترفين",
"slug": "laptop",
"price": 999.99
}Without Accept-Language Header
When no Accept-Language header is present, translatable fields return the full translation map:
GET /products/1{
"id": 1,
"name": {
"en": "Laptop",
"ar": "حاسوب محمول",
"fr": "Ordinateur portable"
},
"description": {
"en": "A powerful laptop for professionals",
"ar": "حاسوب محمول قوي للمحترفين",
"fr": "Un ordinateur portable puissant pour les professionnels"
},
"slug": "laptop",
"price": 999.99
}Listing Endpoints
The interceptor handles arrays and nested structures automatically:
GET /products
Accept-Language: en[
{
"id": 1,
"name": "Laptop",
"description": "A powerful laptop for professionals",
"slug": "laptop",
"price": 999.99
},
{
"id": 2,
"name": "Keyboard",
"description": "Mechanical keyboard with RGB lighting",
"slug": "keyboard",
"price": 149.99
}
]Wrapped/Paginated Responses
If your controller returns a wrapped response (for example, paginated data), the interceptor traverses nested objects and arrays:
@Get()
async findAll() {
const [data, total] = await this.repo.findAndCount();
return { data, total };
}GET /products
Accept-Language: ar{
"data": [
{
"id": 1,
"name": "حاسوب محمول",
"description": "حاسوب محمول قوي للمحترفين",
"slug": "laptop",
"price": 999.99
}
],
"total": 1
}Fallback Behavior
When the requested locale is missing for a field, the system tries each locale in the fallbackLocales chain:
GET /products/1
Accept-Language: deIf the product has no German translation but has English and French, and your fallback chain is ["en", "fr"], the response will use the English translation:
{
"id": 1,
"name": "Laptop",
"slug": "laptop",
"price": 999.99
}See Configuration for details on configuring fallback behavior.