@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:
TranslatableMixin-- Extend your entity class with the mixin to add translation methods.@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| Parameter | Type | Description |
|---|---|---|
key | string | The translatable field name (must be decorated with @Translatable()). |
locale | string | The locale code (e.g., "en", "ar", "fr"). |
value | string | null | The 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| Parameter | Type | Description |
|---|---|---|
key | string | The translatable field name. |
translations | Record<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 goneSignature:
replaceTranslations(key: string, translations: TranslationMap): this| Parameter | Type | Description |
|---|---|---|
key | string | The translatable field name. |
translations | Record<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| Parameter | Type | Description |
|---|---|---|
key | string | The translatable field name. |
locale | string | The 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| Parameter | Type | Description |
|---|---|---|
locale | string | The 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, descriptionThis 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
nullor 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.