Skip to main content

Implementing Translatable

Defining translatable entities

Making an entity translatable means that string properties of the entity can have a different values for multiple languages. To make an entity translatable, you need to implement the Translatable interface and add a translations property to the entity.

src/plugins/requests/entities/product-request.entity.ts
import { DeepPartial } from '@vendure/common/lib/shared-types';
import { VendureEntity, Product, EntityId, ID, Translatable } from '@vendure/core';
import { Column, Entity, ManyToOne } from 'typeorm';

import { ProductRequestTranslation } from './product-request-translation.entity';

@Entity()
class ProductRequest extends VendureEntity implements Translatable {
constructor(input?: DeepPartial<ProductRequest>) {
super(input);
}
text: LocaleString;

@ManyToOne(type => Product)
product: Product;

@EntityId()
productId: ID;


@OneToMany(() => ProductRequestTranslation, translation => translation.base, { eager: true })
translations: Array<Translation<ProductRequest>>;
}

The translations property is a OneToMany relation to the translations. Any fields that are to be translated are of type LocaleString, and do not have a @Column() decorator. This is because the text field here does not in fact exist in the database in the product_request table. Instead, it belongs to the product_request_translations table of the ProductRequestTranslation entity:

src/plugins/requests/entities/product-request-translation.entity.ts
import { DeepPartial } from '@vendure/common/lib/shared-types';
import { HasCustomFields, Translation, VendureEntity, LanguageCode } from '@vendure/core';
import { Column, Entity, Index, ManyToOne } from 'typeorm';

import { ProductRequest } from './release-note.entity';

@Entity()
export class ProductRequestTranslation
extends VendureEntity
implements Translation<ProductRequest>, HasCustomFields
{
constructor(input?: DeepPartial<Translation<ProductRequestTranslation>>) {
super(input);
}

@Column('varchar')
languageCode: LanguageCode;

@Column('varchar')
text: string; // same name as the translatable field in the base entity
@Index()
@ManyToOne(() => ProductRequest, base => base.translations, { onDelete: 'CASCADE' })
base: ProductRequest;
}

Thus there is a one-to-many relation between ProductRequest and ProductRequestTranslation, which allows Vendure to handle multiple translations of the same entity. The ProductRequestTranslation entity also implements the Translation interface, which requires the languageCode field and a reference to the base entity.

Translations in the GraphQL schema

Since the text field is getting hydrated with the translation it should be exposed in the GraphQL Schema. Additionally, the ProductRequestTranslation type should be defined as well, to access other translations as well:

src/plugins/requests/api/types.ts
type ProductRequestTranslation {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
languageCode: LanguageCode!
text: String!
}

type ProductRequest implements Node {
id: ID!
createdAt: DateTime!
updatedAt: DateTime!
# Will be filled with the translation for the current language
text: String!
translations: [ProductRequestTranslation!]!
}

Creating translatable entities

Creating a translatable entity is usually done by using the TranslateableSaver. This injectable service provides a create and update method which can be used to save or update a translatable entity.

src/plugins/requests/service/product-request.service.ts
export class RequestService {

constructor(private translatableSaver: TranslatableSaver) {}

async create(ctx: RequestContext, input: CreateProductRequestInput): Promise<ProductRequest> {
const request = await this.translatableSaver.create({
ctx,
input,
entityType: ProductRequest,
translationType: ProductRequestTranslation,
beforeSave: async f => {
// Assign relations here
},
});
return request;
}
}

Important for the creation of translatable entities is the input object. The input object should contain a translations array with the translations for the entity. This can be done by defining the types like CreateRequestInput inside the GraphQL schema:

src/plugins/requests/api/types.ts
input ProductRequestTranslationInput {
# Only defined for update mutations
id: ID
languageCode: LanguageCode!
text: String!
}

input CreateProductRequestInput {
text: String!
translations: [ProductRequestTranslationInput!]!
}

Updating translatable entities

Updating a translatable entity is done in a similar way as creating one. The TranslateableSaver provides an update method which can be used to update a translatable entity.

src/plugins/requests/service/product-request.service.ts
export class RequestService {

constructor(private translatableSaver: TranslatableSaver) {}

async update(ctx: RequestContext, input: UpdateProductRequestInput): Promise<ProductRequest> {
const updatedEntity = await this.translatableSaver.update({
ctx,
input,
entityType: ProductRequest,
translationType: ProductRequestTranslation,
beforeSave: async f => {
// Assign relations here
},
});
return updatedEntity;
}
}

Once again it's important to provide the translations array in the input object. This array should contain the translations for the entity.

src/plugins/requests/api/types.ts

input UpdateProductRequestInput {
text: String
translations: [ProductRequestTranslationInput!]
}

Loading translatable entities

If your plugin needs to load a translatable entity, you will need to use the TranslatorService to hydrate all the LocaleString fields will the actual translated values from the correct translation.

src/plugins/requests/service/product-request.service.ts
export class RequestService {

constructor(private translator: TranslatorService) {}

findAll(
ctx: RequestContext,
options?: ListQueryOptions<ProductRequest>,
relations?: RelationPaths<ProductRequest>,
): Promise<PaginatedList<Translated<ProductRequest>>> {
return this.listQueryBuilder
.build(ProductRequest, options, {
relations,
ctx,
})
.getManyAndCount()
.then(([items, totalItems]) => {
return {
items: items.map(item => this.translator.translate(item, ctx)),
totalItems,
};
});
}

findOne(
ctx: RequestContext,
id: ID,
relations?: RelationPaths<ProductRequest>,
): Promise<Translated<ProductRequest> | null> {
return this.connection
.getRepository(ctx, ProductRequest)
.findOne({
where: { id },
relations,
})
.then(entity => entity && this.translator.translate(entity, ctx));
}
}