NestboltNestbolt

@nestbolt/translatable

Validation

Validate translation map DTOs with the @IsTranslations() decorator -- structure validation and required locale enforcement.

The @IsTranslations() decorator validates that incoming data is a well-formed translation map. It integrates with class-validator and works with NestJS's ValidationPipe to reject invalid translation data before it reaches your service layer.

Basic Usage

Apply @IsTranslations() to translation map properties in your DTOs:

import { IsTranslations } from "@nestbolt/translatable";

class CreateProductDto {
  @IsTranslations()
  name: Record<string, string>;

  @IsTranslations()
  description: Record<string, string>;
}

With this DTO and NestJS's ValidationPipe, the following request body would pass validation:

{
  "name": { "en": "Laptop", "ar": "حاسوب محمول" },
  "description": { "en": "A powerful laptop" }
}

And the following would be rejected:

{
  "name": "Laptop",
  "description": { "en": 123 }
}

Validation Rules

The @IsTranslations() decorator enforces the following rules:

  1. Must be a plain object -- Arrays, strings, numbers, null, and other non-object types are rejected.
  2. All values must be strings or null -- Every value in the map must be a string or null. Numbers, booleans, nested objects, and arrays are rejected.
  3. Required locales must be present and non-empty -- If requiredLocales is specified, each required locale must exist in the map with a non-empty, non-null value.

Required Locales

Use the requiredLocales option to enforce that certain locales must always be provided:

class CreateProductDto {
  @IsTranslations({ requiredLocales: ["en"] })
  name: Record<string, string>;

  @IsTranslations({ requiredLocales: ["en", "ar"] })
  description: Record<string, string>;
}

With this configuration:

// Valid -- all required locales present
{
  "name": { "en": "Laptop", "fr": "Ordinateur portable" },
  "description": { "en": "A laptop", "ar": "حاسوب محمول" }
}

// Invalid -- "name" is missing required "en" locale
{
  "name": { "fr": "Ordinateur portable" },
  "description": { "en": "A laptop", "ar": "حاسوب محمول" }
}

// Invalid -- "description" is missing required "ar" locale
{
  "name": { "en": "Laptop" },
  "description": { "en": "A laptop" }
}

// Invalid -- "en" for name is empty string (treated as missing)
{
  "name": { "en": "", "fr": "Ordinateur portable" },
  "description": { "en": "A laptop", "ar": "حاسوب محمول" }
}

Custom Validation Messages

You can pass standard class-validator ValidationOptions as the second argument to customize the error message:

class CreateProductDto {
  @IsTranslations(
    { requiredLocales: ["en"] },
    { message: "Product name must include an English translation" },
  )
  name: Record<string, string>;
}

Default Messages

When no custom message is provided, the decorator uses these defaults:

  • Without requiredLocales: 'Must be a valid translation map (e.g. { "en": "Hello", "fr": "Bonjour" })'
  • With requiredLocales: 'Must be a valid translation map with required locales: en, ar' (listing the required locales)

DTO Examples

Create DTO with Required Default Locale

import { IsTranslations } from "@nestbolt/translatable";
import { IsString, IsNumber } from "class-validator";

class CreateProductDto {
  @IsTranslations({ requiredLocales: ["en"] })
  name: Record<string, string>;

  @IsTranslations()
  description: Record<string, string>;

  @IsString()
  slug: string;

  @IsNumber()
  price: number;
}

Update DTO with Optional Translations

For update operations, you may want translations to be optional:

import { IsTranslations } from "@nestbolt/translatable";
import { IsString, IsNumber, IsOptional } from "class-validator";

class UpdateProductDto {
  @IsOptional()
  @IsTranslations({ requiredLocales: ["en"] })
  name?: Record<string, string>;

  @IsOptional()
  @IsTranslations()
  description?: Record<string, string>;

  @IsOptional()
  @IsString()
  slug?: string;

  @IsOptional()
  @IsNumber()
  price?: number;
}

DTO with Multiple Required Locales

For applications that require all content in multiple languages:

class CreateArticleDto {
  @IsTranslations({ requiredLocales: ["en", "ar", "fr"] })
  title: Record<string, string>;

  @IsTranslations({ requiredLocales: ["en", "ar", "fr"] })
  body: Record<string, string>;

  @IsTranslations()
  excerpt: Record<string, string>;
}

Controller Usage

Use the DTO with NestJS's ValidationPipe as you normally would:

@Controller("products")
export class ProductController {
  constructor(private readonly productService: ProductService) {}

  @Post()
  create(@Body() dto: CreateProductDto) {
    return this.productService.create(dto);
  }

  @Patch(":id")
  update(@Param("id") id: number, @Body() dto: UpdateProductDto) {
    return this.productService.update(id, dto);
  }
}

When validation fails, NestJS returns a 400 Bad Request response with error details:

{
  "statusCode": 400,
  "message": [
    "Must be a valid translation map with required locales: en"
  ],
  "error": "Bad Request"
}

Validation Examples

The following table shows what passes and what fails validation with @IsTranslations({ requiredLocales: ["en"] }):

InputValidReason
{ "en": "Hello" }YesRequired locale present with non-empty value
{ "en": "Hello", "fr": "Bonjour" }YesRequired locale present, additional locales allowed
{ "en": "Hello", "fr": null }Yesnull values are allowed for non-required locales
{}NoMissing required locale "en"
{ "fr": "Bonjour" }NoMissing required locale "en"
{ "en": "" }NoRequired locale present but empty
{ "en": null }NoRequired locale present but null
"Hello"NoNot an object
["Hello"]NoArrays are not valid
nullNoNot an object
{ "en": 123 }NoValue is not a string
{ "en": { "nested": "value" } }NoValue is not a string

IsTranslationsOptions Interface

interface IsTranslationsOptions {
  requiredLocales?: string[];
}
PropertyTypeDefaultDescription
requiredLocalesstring[][]Locales that must be present with non-empty, non-null values.