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:
@@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+6
@@ -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,
|
||||
});
|
||||
|
||||
+17
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+4
@@ -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 {
|
||||
|
||||
+3
@@ -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 (
|
||||
|
||||
+6
-15
@@ -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,
|
||||
|
||||
+6
-16
@@ -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,
|
||||
|
||||
+15
-8
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+10
-12
@@ -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
-8
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+10
-12
@@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
+14
-8
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+10
-12
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user