mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Fix zh-CN sidebar translations in minimalMetadata API (#19342)
The minimalMetadata query was returning untranslated English labels for standard objects (Companies, People, Opportunities, etc.) when users requested zh-CN locale. This fix adds translation logic to MinimalMetadataService using the existing I18nService. Changes: - Add translateLabel() method to translate standard object labels - Pass locale from GraphQL context through resolver to service - Only translate non-custom objects (custom objects use user-defined labels) Co-authored-by: Kevin Yu <kevin.yu@example.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
ed912ec548
commit
265b2582ee
3 changed files with 159 additions and 13 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import { UseGuards } from '@nestjs/common';
|
||||
import { Query } from '@nestjs/graphql';
|
||||
import { Context, Query } from '@nestjs/graphql';
|
||||
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { type I18nContext } from 'src/engine/core-modules/i18n/types/i18n-context.type';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
|
|
@ -22,10 +23,12 @@ export class MinimalMetadataResolver {
|
|||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@Context() context: I18nContext,
|
||||
): Promise<MinimalMetadataDTO> {
|
||||
return this.minimalMetadataService.getMinimalMetadata(
|
||||
workspace.id,
|
||||
userWorkspaceId,
|
||||
context.req.locale,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,15 +4,18 @@ import {
|
|||
ALL_METADATA_NAME,
|
||||
type AllMetadataName,
|
||||
} from 'twenty-shared/metadata';
|
||||
import { type APP_LOCALES, SOURCE_LOCALE } from 'twenty-shared/translations';
|
||||
import { ViewVisibility } from 'twenty-shared/types';
|
||||
import { isDefined, uncapitalize } from 'twenty-shared/utils';
|
||||
|
||||
import { I18nService } from 'src/engine/core-modules/i18n/i18n.service';
|
||||
import { ALL_FLAT_ENTITY_MAPS_PROPERTIES } from 'src/engine/metadata-modules/flat-entity/constant/all-flat-entity-maps-properties.constant';
|
||||
import { WorkspaceManyOrAllFlatEntityMapsCacheService } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.service';
|
||||
import { type CollectionHashDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/collection-hash.dto';
|
||||
import { MinimalMetadataDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-metadata.dto';
|
||||
import { MinimalObjectMetadataDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-object-metadata.dto';
|
||||
import { MinimalViewDTO } from 'src/engine/metadata-modules/minimal-metadata/dtos/minimal-view.dto';
|
||||
import { resolveObjectMetadataStandardOverride } from 'src/engine/metadata-modules/object-metadata/utils/resolve-object-metadata-standard-override.util';
|
||||
import { WorkspaceCacheService } from 'src/engine/workspace-cache/services/workspace-cache.service';
|
||||
import { type WorkspaceCacheKeyName } from 'src/engine/workspace-cache/types/workspace-cache-key.type';
|
||||
|
||||
|
|
@ -33,11 +36,13 @@ export class MinimalMetadataService {
|
|||
constructor(
|
||||
private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService,
|
||||
private readonly workspaceCacheService: WorkspaceCacheService,
|
||||
private readonly i18nService: I18nService,
|
||||
) {}
|
||||
|
||||
async getMinimalMetadata(
|
||||
workspaceId: string,
|
||||
userWorkspaceId?: string,
|
||||
locale?: string,
|
||||
): Promise<MinimalMetadataDTO> {
|
||||
const [{ flatObjectMetadataMaps, flatViewMaps }, cacheHashes] =
|
||||
await Promise.all([
|
||||
|
|
@ -63,23 +68,48 @@ export class MinimalMetadataService {
|
|||
})
|
||||
.filter(isDefined);
|
||||
|
||||
const safeLocale = (locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE;
|
||||
const i18nInstance = this.i18nService.getI18nInstance(safeLocale);
|
||||
|
||||
const objectMetadataItems: MinimalObjectMetadataDTO[] = Object.values(
|
||||
flatObjectMetadataMaps.byUniversalIdentifier,
|
||||
)
|
||||
.filter(isDefined)
|
||||
.filter((flatObjectMetadata) => flatObjectMetadata.isActive === true)
|
||||
.map((flatObjectMetadata) => ({
|
||||
id: flatObjectMetadata.id,
|
||||
nameSingular: flatObjectMetadata.nameSingular,
|
||||
namePlural: flatObjectMetadata.namePlural,
|
||||
labelSingular: flatObjectMetadata.labelSingular,
|
||||
labelPlural: flatObjectMetadata.labelPlural,
|
||||
icon: flatObjectMetadata.icon ?? undefined,
|
||||
isCustom: flatObjectMetadata.isCustom,
|
||||
isActive: flatObjectMetadata.isActive,
|
||||
isSystem: flatObjectMetadata.isSystem,
|
||||
isRemote: flatObjectMetadata.isRemote,
|
||||
}));
|
||||
.map((flatObjectMetadata) => {
|
||||
const objectMetadataForOverride = {
|
||||
labelPlural: flatObjectMetadata.labelPlural,
|
||||
labelSingular: flatObjectMetadata.labelSingular,
|
||||
description: flatObjectMetadata.description ?? undefined,
|
||||
icon: flatObjectMetadata.icon ?? undefined,
|
||||
color: flatObjectMetadata.color ?? undefined,
|
||||
isCustom: flatObjectMetadata.isCustom,
|
||||
standardOverrides: flatObjectMetadata.standardOverrides ?? undefined,
|
||||
};
|
||||
|
||||
return {
|
||||
id: flatObjectMetadata.id,
|
||||
nameSingular: flatObjectMetadata.nameSingular,
|
||||
namePlural: flatObjectMetadata.namePlural,
|
||||
labelSingular: resolveObjectMetadataStandardOverride(
|
||||
objectMetadataForOverride,
|
||||
'labelSingular',
|
||||
safeLocale,
|
||||
i18nInstance,
|
||||
),
|
||||
labelPlural: resolveObjectMetadataStandardOverride(
|
||||
objectMetadataForOverride,
|
||||
'labelPlural',
|
||||
safeLocale,
|
||||
i18nInstance,
|
||||
),
|
||||
icon: flatObjectMetadata.icon ?? undefined,
|
||||
isCustom: flatObjectMetadata.isCustom,
|
||||
isActive: flatObjectMetadata.isActive,
|
||||
isSystem: flatObjectMetadata.isSystem,
|
||||
isRemote: flatObjectMetadata.isRemote,
|
||||
};
|
||||
});
|
||||
|
||||
const views: MinimalViewDTO[] = Object.values(
|
||||
flatViewMaps.byUniversalIdentifier,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
import request from 'supertest';
|
||||
|
||||
import { WORKSPACE_MEMBER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/workspace-member-data-seeds.constant';
|
||||
|
||||
const client = request(`http://localhost:${APP_PORT}`);
|
||||
|
||||
const minimalMetadataQuery = {
|
||||
query: `
|
||||
query MinimalMetadataI18n {
|
||||
minimalMetadata {
|
||||
objectMetadataItems {
|
||||
nameSingular
|
||||
labelSingular
|
||||
labelPlural
|
||||
isCustom
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
};
|
||||
|
||||
type MinimalObjectItem = {
|
||||
nameSingular: string;
|
||||
labelSingular: string;
|
||||
labelPlural: string;
|
||||
isCustom: boolean;
|
||||
};
|
||||
|
||||
const updateWorkspaceMemberLocale = async (locale: string) => {
|
||||
const response = await client
|
||||
.post('/graphql')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send({
|
||||
query: `
|
||||
mutation UpdateWorkspaceMember(
|
||||
$workspaceMemberId: UUID!
|
||||
$data: WorkspaceMemberUpdateInput!
|
||||
) {
|
||||
updateWorkspaceMember(id: $workspaceMemberId, data: $data) {
|
||||
id
|
||||
locale
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
workspaceMemberId: WORKSPACE_MEMBER_DATA_SEED_IDS.JANE,
|
||||
data: { locale },
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.updateWorkspaceMember.locale).toBe(locale);
|
||||
};
|
||||
|
||||
const queryMinimalMetadata = () =>
|
||||
client
|
||||
.post('/metadata')
|
||||
.set('Authorization', `Bearer ${APPLE_JANE_ADMIN_ACCESS_TOKEN}`)
|
||||
.send(minimalMetadataQuery);
|
||||
|
||||
const findObjectByName = (
|
||||
items: MinimalObjectItem[],
|
||||
nameSingular: string,
|
||||
): MinimalObjectItem | undefined =>
|
||||
items.find((item) => item.nameSingular === nameSingular);
|
||||
|
||||
describe('minimalMetadata i18n', () => {
|
||||
afterAll(async () => {
|
||||
await updateWorkspaceMemberLocale('en');
|
||||
});
|
||||
|
||||
it('should return English labels when user locale is en', async () => {
|
||||
await updateWorkspaceMemberLocale('en');
|
||||
|
||||
const response = await queryMinimalMetadata();
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const { objectMetadataItems } = response.body.data.minimalMetadata;
|
||||
const company = findObjectByName(objectMetadataItems, 'company');
|
||||
|
||||
expect(company).toBeDefined();
|
||||
expect(company!.labelSingular).toBe('Company');
|
||||
expect(company!.labelPlural).toBe('Companies');
|
||||
});
|
||||
|
||||
it('should return French labels when user locale is fr-FR', async () => {
|
||||
await updateWorkspaceMemberLocale('fr-FR');
|
||||
|
||||
const response = await queryMinimalMetadata();
|
||||
|
||||
expect(response.body.data).toBeDefined();
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const { objectMetadataItems } = response.body.data.minimalMetadata;
|
||||
const company = findObjectByName(objectMetadataItems, 'company');
|
||||
const person = findObjectByName(objectMetadataItems, 'person');
|
||||
const opportunity = findObjectByName(objectMetadataItems, 'opportunity');
|
||||
|
||||
expect(company).toBeDefined();
|
||||
expect(company!.labelSingular).toBe('Entreprise');
|
||||
expect(company!.labelPlural).toBe('Entreprises');
|
||||
|
||||
expect(person).toBeDefined();
|
||||
expect(person!.labelSingular).toBe('Personne');
|
||||
expect(person!.labelPlural).toBe('Personnes');
|
||||
|
||||
expect(opportunity).toBeDefined();
|
||||
expect(opportunity!.labelSingular).toBe('Opportunité');
|
||||
expect(opportunity!.labelPlural).toBe('Opportunités');
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue