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 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',
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 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 station
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 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 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
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
- 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: