feat(server): resolve app translations across remaining metadata resolvers (#22237)

## Summary

**PR 3/4** of the app-metadata-translations stack. Extends runtime
translation resolution to the remaining Twenty-rendered metadata types
so coverage is complete.

- New shared `MetadataTranslationResolverService.getApplicationCatalog({
applicationId, workspaceId, locale })` — the single seam for fetching an
app's per-locale catalog.
- Wired into the **page-layout tab**, **page-layout widget**,
**view-field-group**, **command-menu navigation item**, and **view
name** resolvers, each extended with an optional `applicationCatalog`
param (backward-compatible).

Together with PR 1/4 (object + field), this covers all seven
translatable metadata surfaces.

## Stack
Stacks on #22236 (PR 2/4). Base branch:
`claude/app-translation-2-sdk-manifest`.

## Verification note
`yarn install` could not complete in the remote dev environment, so
typecheck/lint/tests were not run locally — **CI is the source of
truth**.

https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA

---
_Generated by [Claude
Code](https://claude.ai/code/session_01NiE7o3cd3zCLZarVkJa6UA)_

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22237?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Félix Malfait
2026-06-29 22:49:36 +02:00
committed by GitHub
parent fb800e7a2d
commit b1a781fbd9
15 changed files with 164 additions and 110 deletions
@@ -1,7 +1,7 @@
import type DataLoader from 'dataloader';
import {
type ApplicationRegistrationIdLoaderPayload,
type ApplicationTranslationCatalogLoaderPayload,
type FieldMetadataLoaderPayload,
type IndexFieldMetadataLoaderPayload,
type IndexMetadataLoaderPayload,
@@ -100,8 +100,8 @@ export interface IDataloaders {
string
>;
applicationRegistrationIdLoader: DataLoader<
ApplicationRegistrationIdLoaderPayload,
string | null
applicationTranslationCatalogLoader: DataLoader<
ApplicationTranslationCatalogLoaderPayload,
Record<string, string> | undefined
>;
}
@@ -124,9 +124,10 @@ export type StandardApplicationIdLoaderPayload = {
workspaceId: string;
};
export type ApplicationRegistrationIdLoaderPayload = {
workspaceId: string;
export type ApplicationTranslationCatalogLoaderPayload = {
applicationId: string;
workspaceId: string;
locale: keyof typeof APP_LOCALES;
};
@Injectable()
@@ -158,8 +159,8 @@ export class DataloaderService {
const isConfiguredLoader = this.createIsConfiguredLoader();
const standardApplicationIdLoader =
this.createStandardApplicationIdLoader();
const applicationRegistrationIdLoader =
this.createApplicationRegistrationIdLoader();
const applicationTranslationCatalogLoader =
this.createApplicationTranslationCatalogLoader();
return {
relationLoader,
@@ -177,7 +178,7 @@ export class DataloaderService {
viewFilterGroupsByViewIdLoader,
isConfiguredLoader,
standardApplicationIdLoader,
applicationRegistrationIdLoader,
applicationTranslationCatalogLoader,
};
}
@@ -831,12 +832,13 @@ export class DataloaderService {
);
}
private createApplicationRegistrationIdLoader() {
private createApplicationTranslationCatalogLoader() {
return new DataLoader<
ApplicationRegistrationIdLoaderPayload,
string | null
>(async (params: ApplicationRegistrationIdLoaderPayload[]) => {
ApplicationTranslationCatalogLoaderPayload,
Record<string, string> | undefined
>(async (params: ApplicationTranslationCatalogLoaderPayload[]) => {
const workspaceId = params[0].workspaceId;
const locale = params[0].locale;
const { flatApplicationMaps } =
await this.flatEntityMapsCacheService.getOrRecomputeManyOrAllFlatEntityMaps(
@@ -846,11 +848,29 @@ export class DataloaderService {
},
);
return params.map(
({ applicationId }) =>
flatApplicationMaps.byId[applicationId]?.applicationRegistrationId ??
null,
);
const standardApplicationId =
getTwentyStandardApplicationIdOrThrow(flatApplicationMaps);
const catalogByRegistrationId =
await this.loadApplicationCatalogByRegistrationId({
applicationIds: params.map((param) => param.applicationId),
flatApplicationMaps,
locale,
});
return params.map((param) => {
if (param.applicationId === standardApplicationId) {
return undefined;
}
const applicationRegistrationId =
flatApplicationMaps.byId[param.applicationId]
?.applicationRegistrationId;
return isDefined(applicationRegistrationId)
? catalogByRegistrationId.get(applicationRegistrationId)
: undefined;
});
});
}
@@ -52,6 +52,8 @@ export class CommandMenuItemResolver {
objectMetadataLoader: context.loaders.objectMetadataLoader,
standardApplicationIdLoader:
context.loaders.standardApplicationIdLoader,
applicationTranslationCatalogLoader:
context.loaders.applicationTranslationCatalogLoader,
workspaceId: workspace.id,
locale: context.req.locale,
})) ?? ''
@@ -69,6 +71,8 @@ export class CommandMenuItemResolver {
fieldName: 'shortLabel',
objectMetadataLoader: context.loaders.objectMetadataLoader,
standardApplicationIdLoader: context.loaders.standardApplicationIdLoader,
applicationTranslationCatalogLoader:
context.loaders.applicationTranslationCatalogLoader,
workspaceId: workspace.id,
locale: context.req.locale,
});
@@ -85,6 +89,8 @@ export class CommandMenuItemResolver {
fieldName: 'icon',
objectMetadataLoader: context.loaders.objectMetadataLoader,
standardApplicationIdLoader: context.loaders.standardApplicationIdLoader,
applicationTranslationCatalogLoader:
context.loaders.applicationTranslationCatalogLoader,
workspaceId: workspace.id,
locale: context.req.locale,
});
@@ -7,6 +7,7 @@ import { isDefined } from 'twenty-shared/utils';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import {
type ApplicationTranslationCatalogLoaderPayload,
type ObjectMetadataLoaderPayload,
type StandardApplicationIdLoaderPayload,
} from 'src/engine/dataloaders/dataloader.service';
@@ -449,6 +450,7 @@ export class CommandMenuItemService {
fieldName,
objectMetadataLoader,
standardApplicationIdLoader,
applicationTranslationCatalogLoader,
workspaceId,
locale,
}: {
@@ -462,6 +464,10 @@ export class CommandMenuItemService {
StandardApplicationIdLoaderPayload,
string
>;
applicationTranslationCatalogLoader: DataLoader<
ApplicationTranslationCatalogLoaderPayload,
Record<string, string> | undefined
>;
workspaceId: string;
locale: keyof typeof APP_LOCALES | undefined;
}): Promise<string | undefined> {
@@ -479,6 +485,16 @@ export class CommandMenuItemService {
? objectMetadata.applicationId === standardApplicationId
: false;
// The loader returns undefined for the standard app, so the standard-app
// short-circuit lives in the loader, not here.
const applicationCatalog = isDefined(objectMetadata)
? await applicationTranslationCatalogLoader.load({
applicationId: objectMetadata.applicationId,
workspaceId,
locale: locale ?? SOURCE_LOCALE,
})
: undefined;
return interpolateNavigationCommandMenuItemField({
commandMenuItem,
fieldName,
@@ -486,6 +502,7 @@ export class CommandMenuItemService {
isStandardApp,
locale,
i18nInstance: this.i18nService.getI18nInstance(locale ?? SOURCE_LOCALE),
applicationCatalog,
});
}
@@ -17,11 +17,13 @@ export const buildNavigationInterpolationContext = ({
isStandardApp,
locale,
i18nInstance,
applicationCatalog,
}: {
objectMetadata: NavigationInterpolationObjectMetadata;
isStandardApp: boolean;
locale: keyof typeof APP_LOCALES | undefined;
i18nInstance: I18n;
applicationCatalog?: Record<string, string>;
}): Record<string, unknown> => {
const overrideInput = {
labelPlural: objectMetadata.labelPlural,
@@ -37,6 +39,7 @@ export const buildNavigationInterpolationContext = ({
locale,
i18nInstance,
isStandardApp,
applicationCatalog,
);
const resolvedIcon = resolveObjectMetadataStandardOverride(
@@ -45,6 +48,7 @@ export const buildNavigationInterpolationContext = ({
locale,
i18nInstance,
isStandardApp,
applicationCatalog,
);
return {
@@ -19,6 +19,7 @@ export const interpolateNavigationCommandMenuItemField = ({
isStandardApp,
locale,
i18nInstance,
applicationCatalog,
}: {
commandMenuItem: CommandMenuItemDTO;
fieldName: 'label' | 'shortLabel' | 'icon';
@@ -26,6 +27,7 @@ export const interpolateNavigationCommandMenuItemField = ({
isStandardApp: boolean;
locale: keyof typeof APP_LOCALES | undefined;
i18nInstance: I18n;
applicationCatalog?: Record<string, string>;
}): string | undefined => {
const rawValue = commandMenuItem[fieldName];
@@ -49,6 +51,7 @@ export const interpolateNavigationCommandMenuItemField = ({
isStandardApp,
locale,
i18nInstance,
applicationCatalog,
});
return (
@@ -5,7 +5,6 @@ import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApplicationTranslationCacheService } from 'src/engine/core-modules/application/application-translation/application-translation-cache.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { ForbiddenError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@@ -45,7 +44,6 @@ export class FieldMetadataResolver {
constructor(
private readonly fieldMetadataService: FieldMetadataService,
private readonly i18nService: I18nService,
private readonly applicationTranslationCacheService: ApplicationTranslationCacheService,
) {}
@ResolveField(() => Boolean, {
@@ -71,19 +69,12 @@ export class FieldMetadataResolver {
const isStandardApp = fieldMetadata.applicationId === standardApplicationId;
const applicationRegistrationId = isStandardApp
? null
: await context.loaders.applicationRegistrationIdLoader.load({
workspaceId,
applicationId: fieldMetadata.applicationId,
});
const applicationCatalog = isDefined(applicationRegistrationId)
? await this.applicationTranslationCacheService.getCatalog({
applicationRegistrationId,
locale: context.req.locale,
})
: undefined;
const applicationCatalog =
await context.loaders.applicationTranslationCatalogLoader.load({
applicationId: fieldMetadata.applicationId,
workspaceId,
locale: context.req.locale,
});
return resolveFieldMetadataStandardOverride(
fieldMetadata,
@@ -9,10 +9,8 @@ import {
} from '@nestjs/graphql';
import { PermissionFlagType } from 'twenty-shared/constants';
import { isDefined } from 'twenty-shared/utils';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApplicationTranslationCacheService } from 'src/engine/core-modules/application/application-translation/application-translation-cache.service';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
@@ -48,7 +46,6 @@ export class ObjectMetadataResolver {
private readonly objectMetadataService: ObjectMetadataService,
private readonly objectRecordCountService: ObjectRecordCountService,
private readonly i18nService: I18nService,
private readonly applicationTranslationCacheService: ApplicationTranslationCacheService,
) {}
@ResolveField(() => Boolean, {
@@ -87,19 +84,12 @@ export class ObjectMetadataResolver {
const isStandardApp =
objectMetadata.applicationId === standardApplicationId;
const applicationRegistrationId = isStandardApp
? null
: await context.loaders.applicationRegistrationIdLoader.load({
workspaceId,
applicationId: objectMetadata.applicationId,
});
const applicationCatalog = isDefined(applicationRegistrationId)
? await this.applicationTranslationCacheService.getCatalog({
applicationRegistrationId,
locale: context.req.locale,
})
: undefined;
const applicationCatalog =
await context.loaders.applicationTranslationCatalogLoader.load({
applicationId: objectMetadata.applicationId,
workspaceId,
locale: context.req.locale,
});
return resolveObjectMetadataStandardOverride(
objectMetadata,
@@ -16,11 +16,11 @@ import {
import { PermissionFlagType } from 'twenty-shared/constants';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
@@ -42,28 +42,35 @@ export class PageLayoutTabResolver {
constructor(
private readonly pageLayoutTabService: PageLayoutTabService,
private readonly i18nService: I18nService,
private readonly applicationService: ApplicationService,
) {}
@ResolveField(() => String)
async title(
@Parent() tab: PageLayoutTabDTO,
@Context() context: I18nContext,
@Context() context: { loaders: IDataloaders } & I18nContext,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspace },
);
const standardApplicationId =
await context.loaders.standardApplicationIdLoader.load({
workspaceId: workspace.id,
});
const applicationCatalog =
await context.loaders.applicationTranslationCatalogLoader.load({
applicationId: tab.applicationId,
workspaceId: workspace.id,
locale: context.req.locale,
});
return resolvePageLayoutTabTitle({
title: tab.title,
applicationId: tab.applicationId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
twentyStandardApplicationId: standardApplicationId,
overrides: tab.overrides,
i18nInstance: i18n,
applicationCatalog,
});
}
@@ -2,7 +2,7 @@ import { type I18n } from '@lingui/core';
import { isDefined } from 'twenty-shared/utils';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { translateStandardLabel } from 'src/engine/core-modules/i18n/utils/translate-standard-label.util';
import { type PageLayoutTabOverrides } from 'src/engine/metadata-modules/page-layout-tab/entities/page-layout-tab.entity';
export const resolvePageLayoutTabTitle = ({
@@ -11,27 +11,25 @@ export const resolvePageLayoutTabTitle = ({
twentyStandardApplicationId,
overrides,
i18nInstance,
applicationCatalog,
}: {
title: string;
applicationId: string;
twentyStandardApplicationId: string;
overrides?: PageLayoutTabOverrides | null;
i18nInstance: I18n;
applicationCatalog?: Record<string, string>;
}): string => {
if (applicationId !== twentyStandardApplicationId) {
return title;
}
const isStandardApp = applicationId === twentyStandardApplicationId;
if (isDefined(overrides?.title)) {
return title;
}
const messageId = generateMessageId(title);
const translatedMessage = i18nInstance._(messageId);
if (translatedMessage === messageId) {
return title;
}
return translatedMessage;
return translateStandardLabel({
sourceValue: title,
isStandardApp,
applicationCatalog,
i18nInstance,
});
};
@@ -15,11 +15,11 @@ import {
import { PermissionFlagType } from 'twenty-shared/constants';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
import { type WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
import { type IDataloaders } from 'src/engine/dataloaders/dataloader.interface';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
@@ -43,28 +43,35 @@ export class PageLayoutWidgetResolver {
constructor(
private readonly pageLayoutWidgetService: PageLayoutWidgetService,
private readonly i18nService: I18nService,
private readonly applicationService: ApplicationService,
) {}
@ResolveField(() => String)
async title(
@Parent() widget: PageLayoutWidgetDTO,
@Context() context: I18nContext,
@Context() context: { loaders: IDataloaders } & I18nContext,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspace },
);
const standardApplicationId =
await context.loaders.standardApplicationIdLoader.load({
workspaceId: workspace.id,
});
const applicationCatalog =
await context.loaders.applicationTranslationCatalogLoader.load({
applicationId: widget.applicationId,
workspaceId: workspace.id,
locale: context.req.locale,
});
return resolvePageLayoutWidgetTitle({
title: widget.title,
applicationId: widget.applicationId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
twentyStandardApplicationId: standardApplicationId,
overrides: widget.overrides,
i18nInstance: i18n,
applicationCatalog,
});
}
@@ -2,7 +2,7 @@ import { type I18n } from '@lingui/core';
import { isDefined } from 'twenty-shared/utils';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { translateStandardLabel } from 'src/engine/core-modules/i18n/utils/translate-standard-label.util';
import { type PageLayoutWidgetOverrides } from 'src/engine/metadata-modules/page-layout-widget/entities/page-layout-widget.entity';
export const resolvePageLayoutWidgetTitle = ({
@@ -11,27 +11,25 @@ export const resolvePageLayoutWidgetTitle = ({
twentyStandardApplicationId,
overrides,
i18nInstance,
applicationCatalog,
}: {
title: string;
applicationId: string;
twentyStandardApplicationId: string;
overrides?: PageLayoutWidgetOverrides | null;
i18nInstance: I18n;
applicationCatalog?: Record<string, string>;
}): string => {
if (applicationId !== twentyStandardApplicationId) {
return title;
}
const isStandardApp = applicationId === twentyStandardApplicationId;
if (isDefined(overrides?.title)) {
return title;
}
const messageId = generateMessageId(title);
const translatedMessage = i18nInstance._(messageId);
if (translatedMessage === messageId) {
return title;
}
return translatedMessage;
return translateStandardLabel({
sourceValue: title,
isStandardApp,
applicationCatalog,
i18nInstance,
});
};
@@ -11,7 +11,6 @@ import {
import { isArray } from '@sniptt/guards';
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
import { ApplicationService } from 'src/engine/core-modules/application/application.service';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
@@ -42,28 +41,35 @@ export class ViewFieldGroupResolver {
private readonly viewFieldGroupService: ViewFieldGroupService,
private readonly fieldsWidgetUpsertService: FieldsWidgetUpsertService,
private readonly i18nService: I18nService,
private readonly applicationService: ApplicationService,
) {}
@ResolveField(() => String)
async name(
@Parent() viewFieldGroup: ViewFieldGroupDTO,
@Context() context: I18nContext,
@Context() context: { loaders: IDataloaders } & I18nContext,
@AuthWorkspace() workspace: WorkspaceEntity,
): Promise<string> {
const i18n = this.i18nService.getI18nInstance(context.req.locale);
const { twentyStandardFlatApplication } =
await this.applicationService.findWorkspaceTwentyStandardAndCustomApplicationOrThrow(
{ workspace },
);
const standardApplicationId =
await context.loaders.standardApplicationIdLoader.load({
workspaceId: workspace.id,
});
const applicationCatalog =
await context.loaders.applicationTranslationCatalogLoader.load({
applicationId: viewFieldGroup.applicationId,
workspaceId: workspace.id,
locale: context.req.locale,
});
return resolveViewFieldGroupName({
name: viewFieldGroup.name,
applicationId: viewFieldGroup.applicationId,
twentyStandardApplicationId: twentyStandardFlatApplication.id,
twentyStandardApplicationId: standardApplicationId,
overrides: viewFieldGroup.overrides,
i18nInstance: i18n,
applicationCatalog,
});
}
@@ -2,7 +2,7 @@ import { type I18n } from '@lingui/core';
import { isDefined } from 'twenty-shared/utils';
import { generateMessageId } from 'src/engine/core-modules/i18n/utils/generateMessageId';
import { translateStandardLabel } from 'src/engine/core-modules/i18n/utils/translate-standard-label.util';
import { type ViewFieldGroupOverrides } from 'src/engine/metadata-modules/view-field-group/entities/view-field-group.entity';
export const resolveViewFieldGroupName = ({
@@ -11,27 +11,25 @@ export const resolveViewFieldGroupName = ({
twentyStandardApplicationId,
overrides,
i18nInstance,
applicationCatalog,
}: {
name: string;
applicationId: string;
twentyStandardApplicationId: string;
overrides?: ViewFieldGroupOverrides | null;
i18nInstance: I18n;
applicationCatalog?: Record<string, string>;
}): string => {
if (applicationId !== twentyStandardApplicationId) {
return name;
}
const isStandardApp = applicationId === twentyStandardApplicationId;
if (isDefined(overrides?.name)) {
return name;
}
const messageId = generateMessageId(name);
const translatedMessage = i18nInstance._(messageId);
if (translatedMessage === messageId) {
return name;
}
return translatedMessage;
return translateStandardLabel({
sourceValue: name,
isStandardApp,
applicationCatalog,
i18nInstance,
});
};
@@ -72,6 +72,14 @@ export class ViewResolver {
await context.loaders.standardApplicationIdLoader.load({
workspaceId: workspace.id,
});
const isStandardApp =
objectMetadata.applicationId === standardApplicationId;
const applicationCatalog =
await context.loaders.applicationTranslationCatalogLoader.load({
applicationId: objectMetadata.applicationId,
workspaceId: workspace.id,
locale: context.req.locale,
});
const translatedObjectLabel = resolveObjectMetadataStandardOverride(
{
labelPlural: objectMetadata.labelPlural,
@@ -83,7 +91,8 @@ export class ViewResolver {
'labelPlural',
context.req.locale,
i18n,
objectMetadata.applicationId === standardApplicationId,
isStandardApp,
applicationCatalog,
);
return this.viewService.processViewNameWithTemplate(