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;
});
});
}