From 909a6bc60aaaf926653a5f802f6a503f73c2a439 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Fri, 4 Sep 2026 19:48:22 +0000 Subject: [PATCH] refactor(editor): Make the otel module descriptor import-light (no-changelog) (#37768) Co-authored-by: Claude Opus 5 --- .../otel/frontend/src/otel.module.test.ts | 39 +++++++++++++++++++ .../modules/otel/frontend/src/otel.module.ts | 8 ++-- 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/packages/modules/otel/frontend/src/otel.module.test.ts b/packages/modules/otel/frontend/src/otel.module.test.ts index 8d2114825bd..14cf784dd46 100644 --- a/packages/modules/otel/frontend/src/otel.module.test.ts +++ b/packages/modules/otel/frontend/src/otel.module.test.ts @@ -1,3 +1,4 @@ +import { i18nInstance, setLanguage } from '@n8n/i18n'; import type { Scope } from '@n8n/permissions'; import { useRBACStore } from '@n8n/stores/rbac.store'; import { createPinia, setActivePinia } from 'pinia'; @@ -51,6 +52,44 @@ describe('OtelModule', () => { }); }); + describe('label', () => { + const OTEL_LABEL_KEY = 'settings.opentelemetry'; + + afterEach(() => { + setLanguage('en'); + }); + + it('should read the label from i18n', () => { + expect(settingsPage()?.label).toBe('OpenTelemetry'); + }); + + it('should follow a language change after registration', () => { + const item = settingsPage(); + expect(item?.label).toBe('OpenTelemetry'); + + // `Object.fromEntries`, because a dotted i18n key is not a lintable + // object-literal property name. + i18nInstance.global.mergeLocaleMessage( + 'de', + Object.fromEntries([[OTEL_LABEL_KEY, 'OpenTelemetrie']]), + ); + setLanguage('de'); + + expect(item?.label).toBe('OpenTelemetrie'); + }); + + it('should read i18n lazily, not when the descriptor is imported', () => { + // The descriptor is imported by the shell manifest at boot, before `App.vue` + // calls `setLanguage`. A resolved string here would freeze the boot locale, + // and would put an i18n read at module scope — which the import-light rule + // for `*.module.ts` bans. + const descriptor = Object.getOwnPropertyDescriptor(settingsPage(), 'label'); + + expect(typeof descriptor?.get).toBe('function'); + expect(descriptor?.value).toBeUndefined(); + }); + }); + describe('route', () => { it('should keep routing to the unchanged SettingsOpenTelemetryView route name', () => { expect(OTEL_SETTINGS_VIEW).toBe('SettingsOpenTelemetryView'); diff --git a/packages/modules/otel/frontend/src/otel.module.ts b/packages/modules/otel/frontend/src/otel.module.ts index abb1221a732..2f12e956d16 100644 --- a/packages/modules/otel/frontend/src/otel.module.ts +++ b/packages/modules/otel/frontend/src/otel.module.ts @@ -4,8 +4,6 @@ import { useRBACStore } from '@n8n/stores/rbac.store'; import { OTEL_SETTINGS_VIEW } from './otel.constants'; -const i18n = useI18n(); - // typescript-eslint reads an SFC import as `any`, because only vue-tsc can type one. // `pnpm turbo typecheck` is what checks this component for real. // eslint-disable-next-line @typescript-eslint/no-unsafe-return @@ -39,11 +37,11 @@ export const OtelModule: FrontendModuleDescription = { { id: 'settings-opentelemetry', icon: 'telescope', - label: i18n.baseText('settings.opentelemetry'), + get label() { + return useI18n().baseText('settings.opentelemetry'); + }, position: 'top', route: { to: { name: OTEL_SETTINGS_VIEW } }, - // Getter, not a value: the item is registered once at init, but the - // scope check must re-run whenever the sidebar recomputes. get available() { return useRBACStore().hasScope('otel:manage'); },