Mosaic products documentation: Concepts, API Reference, Technical articles, How-to, Downloads and tools

Extend Existing Entity with Localizations


This guide teaches how to change one of the existing or custom entities within the Mosaic Media Template to support localizations. This is done by defining an entity definition to be synchronized with the Managed Mosaic Localization Service. An entity definition describes an entity like a Movie and what localizable fields it has. It also sends all the entities (e.g. movies) with the field values for the localizable fields to the Localization Service. This is done when a new entity is created or a localizable field is changed.

This guide adjusts the Review entity that can be created by following the "Create New Entity" guide.


To follow along, you should have already completed the Mosaic Media Template setup. Instructions on how to set it up can be found in the file contained in the Mosaic Media Template package.

The following conditions must also be met:

  • The Localization Service must be enabled in the Admin Portal

  • At least one non-default locale should be configured for the Localization Service in the Admin Portal (e.g. French (Canada), fr-ca).

  • Localizations must be enabled in the Media Service by setting the following environment variable: IS_LOCALIZATION_ENABLED=TRUE


This guide describes how to enable the Review entities to be localizable for various locales. Localizations are accessible from both the Reviews Details station and the Localizations Explorer station. Localizations can be changed, reviewed, and approved. Once you’ve walked through the guide, you should have enough knowledge to enable localization for any entity.

Adding an entity definition

To be able to localize Reviews, the Localization Service must know about this Review entity type and which of its properties can be localized. Other metadata information like the entity type name and the service ID are needed to be able to distinguish different entity definitions.

The Media Service is already set up to work with localizations out of the box. So you just need to define the entity definition object and make sure it is being sent during the Media Service startup.

  • Navigate to services\media\service\src\domains\reviews. There, create a new folder localization.

  • Inside that folder, create a new file called constants.ts and fill it with the following contents:

    export const LOCALIZATION_REVIEW_TYPE = 'review';

This is a constant localization type that is used by multiple localization-related components. Next, in the same folder, create a file get-review-localization-entity-definitions.ts with the following contents:

import {
} from '@axinom/mosaic-messages';
import { LOCALIZATION_REVIEW_TYPE } from './constants';

export const ReviewFieldDefinitions: EntityFieldDefinition[] = [
    field_name: 'title',
    field_type: 'STRING',
    ui_field_type: 'TEXTBOX',
    title: 'Title field',
    description: 'The title of the review.',
    sort_index: 1,
    field_validation_rules: [
        type: 'REQUIRED',
        settings: { isRequired: true },
        message: 'Title is required.',
        severity: 'ERROR',
    field_name: 'description',
    field_type: 'STRING',
    ui_field_type: 'TEXTAREA',
    title: 'Description field',
    description: 'The description of the review.',
    sort_index: 2,
    field_validation_rules: [
        type: 'REQUIRED',
        settings: { isRequired: true },
        message: 'Description is required.',
        severity: 'ERROR',

export const getReviewLocalizationEntityDefinitions = (
  serviceId: string,
): DeclareEntityDefinitionCommand[] => [
    service_id: serviceId,
    title: 'Review',
    description: 'Localization entity definition for the review type.',
    entity_field_definitions: ReviewFieldDefinitions,

When getReviewLocalizationEntityDefinitions is called, a full entity definition for the Review entity type is returned. This definition specifies various aspects of that type:

  • Details about the entity: to which service it belongs, its type name, a title, and description that should help the translators.

  • The localizable properties. This is a subset of the properties of the Review entity type with the name of the property, its type, and a title/description to help translators know what this field is about.

  • How to visualize the properties in the Localization Service UI with the UI field type and the sort index.

  • How to validate the properties (both in the UI and the backend side)

Lastly, we need to make sure that the definition is sent during the Media Service startup to the Localization Service. Navigate to the services\media\service\src\domains\register-localization-entity-definitions.ts and add the following changes:

import { getReviewLocalizationEntityDefinitions } from './reviews/localization/get-review-localization-entity-definitions'; // <-- Add this line to the imports section


const reviewDefinitions = getReviewLocalizationEntityDefinitions(config.serviceId); // <-- Add this line
const definitions = [
  ...reviewDefinitions, // <-- Add this line

At this point, if the Media Service is launched, the new entity definition is synchronized to the Localization Service. Navigate to the Localizations Explorer for any existing locale and observe the Entity Type filter. You are able to see the Review type in this list.

entity type filter
Figure 1. Entity Type filter with the newly synchronized Review type

Implementing the localization sources synchronization

Now that the Localization Service has the Review entity definition, it is ready to start receiving source data for your review entities. Each time a new review is created, updated, or deleted - the Media Service must send a RabbitMQ message to the Localization Service with a corresponding payload. The PostgreSQL Logical Replication is utilized to detect entity changes and react to them by sending these messages. The general setup is already done, so now a review-specific handling must be implemented. First, add a reviews-replication-handlers.ts file to the services\media\service\src\domains\reviews\localization with the following contents:

import { Dict, isEmptyObject } from '@axinom/mosaic-service-common';
import { reviews } from 'zapatos/schema';
import { Config } from '../../../common';
import {
} from '../../common';
import { LOCALIZATION_REVIEW_TYPE } from './constants';
import { ReviewFieldDefinitions } from './get-review-localization-entity-definitions';

type LocalizableReview = Pick<reviews.JSONSelectable, 'id' | 'title'> & {
  [k: string]: unknown;

const assertReview: (
  data: Dict<unknown> | undefined,
) => asserts data is LocalizableReview = (
  data: Dict<unknown> | undefined,
): asserts data is LocalizableReview => {
  assertLocalizationData(data, 'reviews');
  assertLocalizationColumn(data, 'id', 'reviews');
  assertLocalizationColumn(data, 'title', 'reviews');

export const reviewsReplicationHandlers = (
  config: Config,
): ReplicationOperationHandlers => {
  const entityType = LOCALIZATION_REVIEW_TYPE;
  const fieldDefinitions = ReviewFieldDefinitions.filter((d) => !d.is_archived);
  return {
    insertHandler: async (newData: Dict<unknown> | undefined) => {

      const fields = getInsertedFields(newData, fieldDefinitions);

      return getUpsertMessageData(
        undefined, // Review has no image assignment
    updateHandler: async (
      newData: Dict<unknown> | undefined,
      oldData: Dict<unknown> | undefined,
    ) => {

      const fields = getChangedFields(newData, oldData, fieldDefinitions);
      if (isEmptyObject(fields)) {
        return undefined; // Do not send a message if no localizable fields have changed

      return getUpsertMessageData(
        undefined, // Review has no image assignment
    deleteHandler: async (oldData: Dict<unknown> | undefined) => {
      return getDeleteMessageData(config.serviceId, entityType,;

The reviewsReplicationHandlers function constructs and returns an object with three handler functions, for insert, update, and delete operations. Every time a review is modified in one of those ways, a dedicated function is called. The incoming data is then validated, and if a change was made - a specific RabbitMQ message is generated to be used by the common localizations sync function. Now this sync function services\media\service\src\domains\sync-sources-with-localization.ts must be adjusted to use the previously created function:

import { reviewsReplicationHandlers } from './reviews/localization/reviews-replication-handlers'; // <-- Add this line to the imports section


case 'collections_images':
  return collectionsImagesReplicationHandlers(config, ownerPool);
case 'reviews': // <-- Add this line
  return reviewsReplicationHandlers(config); // <-- Add this line
  throw new MosaicError({
    messageParams: [scopedMessage.tableName],

The last backend code change to be done is to add the reviews database table to the list of tables associated with the PostgreSQL Publication that is used to observe the table changes. Adjust the services\media\service\src\domains\localization-table-names.ts file by adding reviews to the exported array:

export const localizationTableNames: Table[] = [
  'reviews', // <-- Add this line

Now, if the Media Service is restarted and a new Review is created it is synchronized with the Localization Service and can be localized from the Localizations workflow:

localizations explorer
Figure 2. Localizations Explorer station
localization details
Figure 3. Localization Details station

Adding navigation from Localization Details

To facilitate a smooth navigation between the Localization Details station and the Review Details station, the Localization workflow uses the Route Resolvers feature provided by the Piral instance. You can establish a connection between these stations by registering a route resolver for the Review entity type using the @axinom/mosaic-portal library. The localization service will find a resolver for an entity using the naming convention ${entity-type}-details. So we should provide such a resolver for the Review entity type.

To achieve this, adjust the file services\media\workflows\src\Stations\Reviews\registrations.tsx with the following changes:

export function register(app: PiletApi, extensions: Extensions): void {
  // ...
  app.setRouteResolver( // <-- Add this function call
    (dynamicRouteSegments?: Record<string, string> | string) => {
      const reviewId =
        typeof dynamicRouteSegments === 'string'
          ? dynamicRouteSegments
          : dynamicRouteSegments?.reviewId;

      return reviewId ? `/reviews/${reviewId}` : undefined;
  // ...

Once the workflow registers this resolver, the Localization Details station will start to create links to the Review Details station in its inline menu:

localization details with navigation button
Figure 4. Localization Details with navigation to Review Details

Adding navigation from the Review Details

Right now the entity localization is accessible only from the Localization workflow. However there is a way to integrate the localization workflow into the Review Details station. This can be achieved by using the "Localization Generator". The localization generator is a function designed to generate a station that lists available locales and the Localization Details stations for the entity. This function registers a route that can be incorporated into customizable workflows to seamlessly link to the localization stations.

The Localization service registers and shares the localization generator function through the Piral instance data helper, using the key localization-registration. You can retrieve this function using the getDataHelper function of the Piral instance. Additionally, the @axinom/mosaic-managed-workflow-integration library provides handy functions for generating and retrieving links to localization stations. For more detailed information on how to generate and retrieve routes, please refer to the documentation provided in the @axinom/mosaic-managed-workflow-integration library’s documentation section on the localization workflow integration.

To achieve this for Review Details station, first, adjust the file services\media\workflows\src\Stations\Reviews\registrations.tsx with the following changes:

import { registerLocalizationEntryPoints } from '@axinom/mosaic-managed-workflow-integration'; // <-- Add this line to the imports section

// ...

export function register(app: PiletApi, extensions: Extensions): void {
  // ...
  registerLocalizationEntryPoints( // <-- Add this function call
        root: '/reviews/:reviewId', // the generated stations will be registered below this root
        entityIdParam: 'reviewId',
        entityType: 'review',
  // ...

And then adjust the file services\media\workflows\src\Stations\Reviews\ReviewDetails\ReviewDetails.tsx to add an action button go navigate into the generated sub-workflow:

import { getLocalizationEntryPoint } from '@axinom/mosaic-managed-workflow-integration'; // <-- Add this line to the imports section


const localizationPath = getLocalizationEntryPoint('review'); // <-- Add this line
  ...(localizationPath // <-- Add this array element to the actions array
    ? [
          label: 'Localizations',
          path: localizationPath.replace(
    : []),
    label: 'Delete',
    icon: IconName.Delete,
    confirmationMode: 'Simple',
    onActionSelected: deleteReview,

Now the navigation between from Review Details station to Locales Explorer is established:

review details
Figure 5. Review Details with navigation to Localization

Clicking on the Localization button from the Review Details leads to the Locales Explorer, where you can select any configured locale and perform the localization for it:

locales explorer
Figure 6. Locales Explorer

Next steps

The implementation achieved by following this guide should already provide you a good understanding of how to work with localizations. But you can compare this implementation with existing ones, e.g. the Movie or Collection localization folder contents in their respective domain folders. You can see that there is more that can be achieved but is not part of this guide. This includes:

  • Unit tests:

    • it’s always beneficial to make sure everything works as expected.

  • Validation and Publishing

    • The Localization Service GraphQL API exposes dedicated endpoints to validate existing localizations for a specific entity, and then prepare the metadata that can be used for publishing.

    • Appropriate integration is already implemented for other publishable entities of the media-template and exact endpoint calls can be observed in the services\media\service\src\graphql\documents folder.

  • Relations

    • Related entities can also be localized, e.g. for movies, there are Movie Genres.

    • While genres are localized separately from the Movies, changes to the related images produce Movie localization update messages. This enables localization workflows to add image thumbnails for entity localizations to achieve better visualization for translators.

  • Data Repair scripts.

    • In early stages of development, it is possible to first focus on the initial implementation without enabling localizations, and only enable them at a later stage.

    • To make sure that already existing entities are synchronized with the Localization Service - a dedicated data repair script was implemented for the existing entities: services\media\service\src\one-time-register-localization-sources.ts

    • If there is a need, it can be adjusted to support new entity types.

  • Using index.ts files

    • While going through this guide, you probably noticed that some of the import statements are a bit long compared to other already used ones.

    • This is because existing code utilizes index.ts files to export contents of folders to make import statements from such folders shorter. The same can be done for the review entity.

  • Initial localizations infrastructure setup

    • This guide operated on the Media Service which already has everything set up to enable localization of entity types. But if a brand new service is implemented - various aspects must be handled to achieve the same. This includes:

      • Making sure that PostgreSQL Server supports logical replication by adjusting its settings

      • Making sure that the Owner Pool has the REPLICATION attribute

      • Adding new configurable values to the service (e.g. replication slot name, Localization Service base URL, Localization enabled flag)

      • Adding/adjusting codegen config to include the Localization Service

      • Making sure that used service account has required localization-related permissions.

      • Adjusting the startup code to ensure that the replication slot and the related publication are created or updated, that entity definition registration messages are sent, and that the logical replication service is running.

      • Caching of auth token, because service can produce a lot of localization source upsert messages.

      • Error handling