@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:
- Must be a plain object -- Arrays, strings, numbers,
null, and other non-object types are rejected. - 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. - Required locales must be present and non-empty -- If
requiredLocalesis 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"] }):
| Input | Valid | Reason |
|---|---|---|
{ "en": "Hello" } | Yes | Required locale present with non-empty value |
{ "en": "Hello", "fr": "Bonjour" } | Yes | Required locale present, additional locales allowed |
{ "en": "Hello", "fr": null } | Yes | null values are allowed for non-required locales |
{} | No | Missing required locale "en" |
{ "fr": "Bonjour" } | No | Missing required locale "en" |
{ "en": "" } | No | Required locale present but empty |
{ "en": null } | No | Required locale present but null |
"Hello" | No | Not an object |
["Hello"] | No | Arrays are not valid |
null | No | Not an object |
{ "en": 123 } | No | Value is not a string |
{ "en": { "nested": "value" } } | No | Value is not a string |
IsTranslationsOptions Interface
interface IsTranslationsOptions {
requiredLocales?: string[];
}| Property | Type | Default | Description |
|---|---|---|---|
requiredLocales | string[] | [] | Locales that must be present with non-empty, non-null values. |