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 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.
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 folderlocalization
. -
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 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,
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.

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 {
assertLocalizationColumn,
assertLocalizationData,
getChangedFields,
getDeleteMessageData,
getInsertedFields,
getUpsertMessageData,
ReplicationOperationHandlers,
} 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) => {
assertReview(newData);
const fields = getInsertedFields(newData, fieldDefinitions);
return getUpsertMessageData(
config.serviceId,
entityType,
newData.id,
fields,
newData.title,
undefined, // Review has no image assignment
);
},
updateHandler: async (
newData: Dict<unknown> | undefined,
oldData: Dict<unknown> | undefined,
) => {
assertReview(newData);
assertReview(oldData);
const fields = getChangedFields(newData, oldData, fieldDefinitions);
if (isEmptyObject(fields)) {
return undefined; // Do not send a message if no localizable fields have changed
}
return getUpsertMessageData(
config.serviceId,
entityType,
newData.id,
fields,
newData.title,
undefined, // Review has no image assignment
);
},
deleteHandler: async (oldData: Dict<unknown> | undefined) => {
assertReview(oldData);
return getDeleteMessageData(config.serviceId, entityType, oldData.id);
},
};
};
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
default:
throw new MosaicError({
...InternalErrors.UnsupportedReplicationTable,
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[] = [
'movies',
'movies_images',
'movie_genres',
'tvshows',
'tvshows_images',
'tvshow_genres',
'seasons',
'seasons_images',
'episodes',
'episodes_images',
'collections',
'collections_images',
'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:


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:

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:

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:

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
-
-