NestboltNestbolt

@nestbolt/translatable

Translatable Entities

Create translatable entities with @Translatable() decorator and TranslatableMixin -- set, replace, and remove translations.

Translatable entities are TypeORM entities that have one or more fields marked with the @Translatable() decorator and extend TranslatableMixin. This page covers how to define these entities and use the write methods to manage translations.

Creating a Translatable Entity

A translatable entity requires two things:

  1. TranslatableMixin -- Extend your entity class with the mixin to add translation methods.
  2. @Translatable() decorator -- Mark each translatable property with this decorator.
import { Entity, PrimaryGeneratedColumn, Column, BaseEntity } from "typeorm";
import { Translatable, TranslatableMixin } from "@nestbolt/translatable";

@Entity()
export class NewsItem 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: "timestamp", default: () => "NOW()" })
  publishedAt: Date;
}

Column Requirements

Each translatable column must use type: "jsonb" (PostgreSQL) and should set default: {} so that new rows start with an empty translation map rather than NULL. The TypeScript type should be Record<string, string>, representing a map of locale codes to translation strings.

The @Translatable() Decorator

The @Translatable() decorator registers a property as translatable by storing the field name in Reflect metadata on the entity class. This metadata is used by the mixin methods, the interceptor, and the subscriber to identify which fields contain translation maps.

import { Translatable, getTranslatableFields } from "@nestbolt/translatable";

@Entity()
export class Product extends TranslatableMixin(BaseEntity) {
  @Translatable()
  @Column({ type: "jsonb", default: {} })
  name: Record<string, string>;

  @Translatable()
  @Column({ type: "jsonb", default: {} })
  tagline: Record<string, string>;

  @Column()
  sku: string;
}

// You can inspect registered fields at runtime
getTranslatableFields(Product);
// → ["name", "tagline"]

The TranslatableMixin

TranslatableMixin is a function that accepts a base class and returns a new class extended with all translation methods. You can use it with BaseEntity, a plain class, or any custom base class:

// Extend TypeORM's BaseEntity (Active Record pattern)
class Product extends TranslatableMixin(BaseEntity) {
  // ...
}

// Extend a plain class (Repository pattern)
class Product extends TranslatableMixin(class {}) {
  // ...
}

// Extend a custom base class
class AuditableEntity {
  createdAt: Date;
  updatedAt: Date;
}

class Product extends TranslatableMixin(AuditableEntity) {
  // has both audit fields and translation methods
}

Writing Translations

setTranslation

Set a single translation for a field in a specific locale. Returns this for chaining.

const product = new Product();

product
  .setTranslation("name", "en", "Laptop")
  .setTranslation("name", "ar", "حاسوب محمول")
  .setTranslation("name", "fr", "Ordinateur portable");

await repo.save(product);

Signature:

setTranslation(key: string, locale: string, value: string | null): this
ParameterTypeDescription
keystringThe translatable field name (must be decorated with @Translatable()).
localestringThe locale code (e.g., "en", "ar", "fr").
valuestring | nullThe translation value. Passing null or an empty string removes the translation for that locale.

If the key is not a translatable attribute, an AttributeIsNotTranslatableException is thrown.

When @nestjs/event-emitter is installed, each call to setTranslation emits a translatable.translation-set event with the old and new values.

setTranslations

Set multiple translations for a field at once. Existing translations for other locales are preserved.

product.setTranslations("name", {
  en: "Laptop",
  ar: "حاسوب محمول",
  fr: "Ordinateur portable",
});

Signature:

setTranslations(key: string, translations: TranslationMap): this
ParameterTypeDescription
keystringThe translatable field name.
translationsRecord<string, string>An object mapping locale codes to translation strings.

This method calls setTranslation for each entry in the map, so events are emitted for each individual translation if the event emitter is installed.

replaceTranslations

Replace all translations for a field, discarding any previously set translations. Only the locales in the provided map will remain.

// Before: { en: "Laptop", ar: "حاسوب محمول", fr: "Ordinateur portable" }

product.replaceTranslations("name", {
  en: "Notebook Computer",
  de: "Notebook",
});

// After: { en: "Notebook Computer", de: "Notebook" }
// The 'ar' and 'fr' translations are gone

Signature:

replaceTranslations(key: string, translations: TranslationMap): this
ParameterTypeDescription
keystringThe translatable field name.
translationsRecord<string, string>The new translation map. All previous translations for this field are removed.

forgetTranslation

Remove a single translation for a field in a specific locale.

product.forgetTranslation("name", "fr");

// Before: { en: "Laptop", ar: "حاسوب محمول", fr: "Ordinateur portable" }
// After:  { en: "Laptop", ar: "حاسوب محمول" }

Signature:

forgetTranslation(key: string, locale: string): this
ParameterTypeDescription
keystringThe translatable field name.
localestringThe locale to remove.

forgetAllTranslations

Remove a locale across all translatable fields on the entity.

// Remove all French translations
product.forgetAllTranslations("fr");

// Before:
//   name:        { en: "Laptop", fr: "Ordinateur portable" }
//   description: { en: "A laptop", fr: "Un ordinateur portable" }
//
// After:
//   name:        { en: "Laptop" }
//   description: { en: "A laptop" }

Signature:

forgetAllTranslations(locale: string): this
ParameterTypeDescription
localestringThe locale to remove from all translatable fields.

Error Handling

If you attempt to use any translation method on a field that is not decorated with @Translatable(), an AttributeIsNotTranslatableException is thrown:

const product = new Product();

// "slug" is not marked with @Translatable()
product.setTranslation("slug", "en", "laptop");
// → throws AttributeIsNotTranslatableException:
//   Cannot translate attribute "slug" as it's not one of the
//   translatable attributes: name, description

This exception includes the list of valid translatable attributes to help with debugging.

TypeORM Subscriber

The package includes a TranslatableSubscriber that is registered automatically by the module. It handles two important tasks:

  • After load: Ensures that translation columns loaded from the database are proper JavaScript objects. If the database returns a JSON string (which can happen with some TypeORM configurations), the subscriber parses it. If a column is NULL, it initializes it to an empty object {}.

  • Before insert/update: Cleans the translation maps before persisting, removing any keys with null or empty string values. This keeps the stored JSON tidy.

You do not need to configure or register the subscriber manually -- it is provided by TranslatableModule.