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

Extend Existing Entity with Localizations

Overview

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 first 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. The service logic 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.

Pre-Requisites

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 README.md 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

Goal

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 {
  DeclareEntityDefinitionCommand,
  EntityFieldDefinition,
} 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',
    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',
    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,
    entity_type: LOCALIZATION_REVIEW_TYPE,
    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 = [
  ...movieDefinitions,
  ...tvshowDefinitions,
  ...collectionDefinitions,
  ...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. We utilize PostgreSQL triggers to detect such changes and insert the required data into the app_hidden.inbox table. The service uses a message handler to receive those inbox messages and sends then the UpsertLocalizationSourceEntityCommand to the Localization Service. The general setup is already done, so now a review-specific handling must be implemented.

First, we need to prepare dedicated database migration placeholders. Create a services\media\service\src\domains\reviews\localization\localizable-review-db-migration-placeholders.ts file with the following contents:

import { ReviewFieldDefinitions } from './get-review-localization-entity-definitions';

export const localizableReviewDbMigrationPlaceholders = {
  ':REVIEW_LOCALIZABLE_FIELDS': ReviewFieldDefinitions.map(
    (d) => d.field_name,
  ).join(','),
  ':REVIEW_LOCALIZATION_REQUIRED_FIELDS': 'id',
};

Adjust the services\media\service\src\domains\localization-db-migration-placeholders.ts:

import { localizableReviewDbMigrationPlaceholders } from './reviews/localization/localizable-review-db-migration-placeholders'; // <-- Add this line to the imports section

//...

export const localizationDbMigrationPlaceholders: Dict<string> = {
  ...localizableMovieDbMigrationPlaceholders,
  ...localizableTvshowDbMigrationPlaceholders,
  ...localizableSeasonDbMigrationPlaceholders,
  ...localizableEpisodeDbMigrationPlaceholders,
  ...localizableCollectionDbMigrationPlaceholders,
  ...localizableReviewDbMigrationPlaceholders, // <-- Add this line
};

Next, add a new database migration with the following contents:

--! Message: add-review-localization-triggers

SELECT app_hidden.create_localizable_entity_triggers(
  'id', 'reviews', 'REVIEW', ':REVIEW_LOCALIZABLE_FIELDS',':REVIEW_LOCALIZATION_REQUIRED_FIELDS');

The triggers to detect review modifications are now added, and will handle only localizable columns that are mentioned in the localizable placeholders. But we still need to make sure these changes are properly handled and corresponding messages are being sent to the Localization service. To do so - first let’s create a services\media\service\src\domains\reviews\localization\localizable-review-db-messaging-settings.ts file with the following contents:

import { MessagingSettings } from '@axinom/mosaic-message-bus-abstractions';

export class LocalizableReviewDbMessagingSettings implements MessagingSettings {
  public static LocalizableReviewCreated =
    new LocalizableReviewDbMessagingSettings(
      'LocalizableReviewCreated',
      'inbox',
      'event',
      'review',
    );
  public static LocalizableReviewUpdated =
    new LocalizableReviewDbMessagingSettings(
      'LocalizableReviewUpdated',
      'inbox',
      'event',
      'review',
    );
  public static LocalizableReviewDeleted =
    new LocalizableReviewDbMessagingSettings(
      'LocalizableReviewDeleted',
      'inbox',
      'event',
      'review',
    );

  private constructor(
    public readonly messageType: string,
    public readonly queue: string,
    public readonly action: 'command' | 'event',
    public readonly aggregateType: string,
  ) {}

  public readonly routingKey: string = '';
  public toString = (): string => {
    return this.messageType;
  };
}

We are imitating the regular RMQ messaging settings to make sure that the Transactional Inbox polling mechanism is able to handle them. But instead of coming from the RMQ, these messages are coming from the dedicated triggers we created previously.

Next, create a services\media\service\src\domains\reviews\localization\localizable-review-db-message-handlers.ts file with the following contents:

import {
  StoreOutboxMessage,
  TypedTransactionalMessage,
} from '@axinom/mosaic-transactional-inbox-outbox';
import { Config } from '../../../common';
import {
  getDeleteMessageData,
  getUpsertMessageData,
  LocalizableMediaTransactionalInboxMessageHandler,
  LocalizationMessageData,
} from '../../common';
import { LOCALIZATION_REVIEW_TYPE } from './constants';
import { LocalizableReviewDbMessagingSettings } from './localizable-review-db-messaging-settings';

export interface LocalizableReviewDbEvent {
  id: number;
  title?: string;
  description?: string;
}

export class LocalizableReviewCreatedDbMessageHandler extends LocalizableMediaTransactionalInboxMessageHandler<LocalizableReviewDbEvent> {
  constructor(storeOutboxMessage: StoreOutboxMessage, config: Config) {
    super(
      LocalizableReviewDbMessagingSettings.LocalizableReviewCreated,
      storeOutboxMessage,
      config,
    );
  }

  override async getLocalizationCommandData({
    payload: { id, ...fields },
  }: TypedTransactionalMessage<LocalizableReviewDbEvent>): Promise<
    LocalizationMessageData | undefined
  > {
    return getUpsertMessageData(
      this.config.serviceId,
      LOCALIZATION_REVIEW_TYPE,
      id,
      fields,
      fields.title,
      undefined,
    );
  }
}

export class LocalizableReviewUpdatedDbMessageHandler extends LocalizableMediaTransactionalInboxMessageHandler<LocalizableReviewDbEvent> {
  constructor(storeOutboxMessage: StoreOutboxMessage, config: Config) {
    super(
      LocalizableReviewDbMessagingSettings.LocalizableReviewUpdated,
      storeOutboxMessage,
      config,
    );
  }

  override async getLocalizationCommandData({
    payload: { id, ...fields },
  }: TypedTransactionalMessage<LocalizableReviewDbEvent>): Promise<
    LocalizationMessageData | undefined
  > {
    return getUpsertMessageData(
      this.config.serviceId,
      LOCALIZATION_REVIEW_TYPE,
      id,
      fields,
      fields.title,
      undefined,
    );
  }
}

export class LocalizableReviewDeletedDbMessageHandler extends LocalizableMediaTransactionalInboxMessageHandler<LocalizableReviewDbEvent> {
  constructor(storeOutboxMessage: StoreOutboxMessage, config: Config) {
    super(
      LocalizableReviewDbMessagingSettings.LocalizableReviewDeleted,
      storeOutboxMessage,
      config,
    );
  }

  override async getLocalizationCommandData({
    payload: { id },
  }: TypedTransactionalMessage<LocalizableReviewDbEvent>): Promise<
    LocalizationMessageData | undefined
  > {
    return getDeleteMessageData(
      this.config.serviceId,
      LOCALIZATION_REVIEW_TYPE,
      id,
    );
  }
}

We now have the three handler classes, for insert, update, and delete operations. Every time a review is modified in one of those ways, a dedicated handler is used to process the inbox message to generate a message to send to the Localization service. Finally, for these messages to actually be called - they should be registered as part of the services\media\service\src\messaging\register-messaging.ts file:

import {
  LocalizableReviewCreatedDbMessageHandler,
  LocalizableReviewDeletedDbMessageHandler,
  LocalizableReviewUpdatedDbMessageHandler,
} from '../domains/reviews/localization/localizable-review-db-message-handlers'; // <-- Add this line to the imports section

//...

  const dbMessageHandlers: TransactionalMessageHandler[] = [
    new LocalizableReviewCreatedDbMessageHandler(storeOutboxMessage, config), // <-- Add this line
    new LocalizableReviewUpdatedDbMessageHandler(storeOutboxMessage, config), // <-- Add this line
    new LocalizableReviewDeletedDbMessageHandler(storeOutboxMessage, config), // <-- Add this line
    new LocalizableCollectionUpdatedDbMessageHandler(
      storeOutboxMessage,
      config,
    ),
    // ...
  ];

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
    'review-details',
    (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',
      },
    ],
    app,
  );
  // ...
}

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(
            ':reviewId',
            reviewId.toString(),
          ),
        },
      ]
    : []),
  {
    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\_manual-repair-scripts\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:

      • Adding new configurable values to the service (e.g. 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 entity definition registration messages are sent.

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

      • Adjustments to ingest

      • Error handling