From d7ab3dcdc0964af12c1918bc11a920aa584b1d6d Mon Sep 17 00:00:00 2001 From: chintankavathia Date: Fri, 17 Apr 2026 15:57:20 +0530 Subject: [PATCH] feat(dashboards-ng): add translation support for widget name, description, and heading Widget names and descriptions in the catalog now support translation keys, allowing localized widget catalog. The `heading` field in `WidgetConfig` is now typed as `TranslatableString` for consistency with downstream `si-card` components that already apply translation. --- api-goldens/dashboards-ng/index.api.md | 6 +- .../custom-widget-catalog.component.html | 4 +- .../app/widgets/charts/widget-descriptors.ts | 6 +- .../dashboards-demo/src/assets/i18n/de.json | 3 +- .../dashboards-demo/src/assets/i18n/en.json | 3 +- .../si-widget-catalog.component.html | 4 +- .../si-widget-catalog.component.spec.ts | 76 ++++++++++++++++++- .../si-widget-catalog.component.ts | 16 +++- .../dashboards-ng/src/model/widgets.model.ts | 7 +- 9 files changed, 104 insertions(+), 21 deletions(-) diff --git a/api-goldens/dashboards-ng/index.api.md b/api-goldens/dashboards-ng/index.api.md index e044751911..c02acaab51 100644 --- a/api-goldens/dashboards-ng/index.api.md +++ b/api-goldens/dashboards-ng/index.api.md @@ -286,10 +286,10 @@ export type WebComponent = CommonFactoryFields & { export interface Widget { componentFactory: WidgetComponentFactory; defaults?: Pick; - description?: string; + description?: TranslatableString; iconClass?: string; id: string; - name: string; + name: TranslatableString; payload?: any; version?: string; } @@ -306,7 +306,7 @@ export interface WidgetConfig { // (undocumented) actionBarViewType?: ViewType; expandable?: boolean; - heading?: string; + heading?: TranslatableString; height?: number; id: string; image?: WidgetImage; diff --git a/projects/dashboards-demo/src/app/components/widget-catalog/custom-widget-catalog.component.html b/projects/dashboards-demo/src/app/components/widget-catalog/custom-widget-catalog.component.html index 71c902fd39..5a3ceeb56f 100644 --- a/projects/dashboards-demo/src/app/components/widget-catalog/custom-widget-catalog.component.html +++ b/projects/dashboards-demo/src/app/components/widget-catalog/custom-widget-catalog.component.html @@ -18,8 +18,8 @@ >
- {{ myDescriptor.name }} - {{ myDescriptor.description }} + {{ myDescriptor.name | translate }} + {{ myDescriptor.description | translate }}
diff --git a/projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts b/projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts index 7a109b9954..bae7e3985e 100644 --- a/projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts +++ b/projects/dashboards-demo/src/app/widgets/charts/widget-descriptors.ts @@ -21,11 +21,9 @@ const loaderFunction = async (name: string): Promise => { }; export const LINE_CHART_DESC: Widget = { - name: 'Line Chart', + name: 'WIDGET.LINE_CHART', id: '@siemens/dashboards-demo/line-chart', - description: `A line chart is a type of chart used to show information that changes over time.\ - Line charts are created by plotting a series of several points and connecting them with a straight line.\ - Line charts are used to track changes over short and long periods.`, + description: 'WIDGET.LINE_CHART_DESC', iconClass: 'element-trend', componentFactory: { componentName: 'CartesianComponent', diff --git a/projects/dashboards-demo/src/assets/i18n/de.json b/projects/dashboards-demo/src/assets/i18n/de.json index ecbd6adfe3..a7f7280510 100644 --- a/projects/dashboards-demo/src/assets/i18n/de.json +++ b/projects/dashboards-demo/src/assets/i18n/de.json @@ -10,7 +10,8 @@ "WIDGET": { "LINE_CHART": "Liniendiagramm", "BAR_CHART": "Balkendiagramm", - "CIRCLE_CHART": "Kuchendiagramm" + "CIRCLE_CHART": "Kuchendiagramm", + "LINE_CHART_DESC": "Ein Liniendiagramm ist eine Diagrammart, die verwendet wird, um Informationen darzustellen, die sich im Laufe der Zeit ändern. Liniendiagramme werden erstellt, indem mehrere Punkte eingezeichnet und mit einer geraden Linie verbunden werden. Liniendiagramme werden verwendet, um Veränderungen über kurze und lange Zeiträume zu verfolgen." }, "TOOLBAR": { "SAVE_AS_DEFAULTS": "Als Standard speichern", diff --git a/projects/dashboards-demo/src/assets/i18n/en.json b/projects/dashboards-demo/src/assets/i18n/en.json index 8d7190f590..12e9fdd383 100644 --- a/projects/dashboards-demo/src/assets/i18n/en.json +++ b/projects/dashboards-demo/src/assets/i18n/en.json @@ -10,7 +10,8 @@ "WIDGET": { "LINE_CHART": "Line Chart", "BAR_CHART": "Bar Chart", - "CIRCLE_CHART": "Circle Chart" + "CIRCLE_CHART": "Circle Chart", + "LINE_CHART_DESC": "A line chart is a type of chart used to show information that changes over time. Line charts are created by plotting a series of several points and connecting them with a straight line. Line charts are used to track changes over short and long periods." }, "TOOLBAR": { "SAVE_AS_DEFAULTS": "Save as defaults", diff --git a/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.html b/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.html index 93efe9a910..ab4076c025 100644 --- a/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.html +++ b/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.html @@ -31,8 +31,8 @@ >
- {{ widget.name }} - {{ widget.description?.trim() }} + {{ widget.name | translate }} + {{ widget.description?.trim() | translate }}
} diff --git a/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.spec.ts b/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.spec.ts index b22375584c..97605d319d 100644 --- a/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.spec.ts +++ b/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.spec.ts @@ -8,7 +8,11 @@ import { ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { ModalRef } from '@siemens/element-ng/modal'; import { SiSearchBarComponent } from '@siemens/element-ng/search-bar'; -import { firstValueFrom } from 'rxjs'; +import { + provideMockTranslateServiceBuilder, + SiTranslateService +} from '@siemens/element-translate-ng/translate'; +import { firstValueFrom, NEVER } from 'rxjs'; import { TEST_WIDGET } from '../../../test/test-widget/test-widget'; import { createTestingWidget, TestingModule } from '../../../test/testing.module'; @@ -283,4 +287,74 @@ describe('SiWidgetCatalogComponent', () => { .tagName ).not.toBe('SI-TEST-WIDGET-EDITOR'); }); + + describe('Widget name and description translation', () => { + const translations: Record = { + 'WIDGET.NAME_KEY': 'Translated Widget Name', + 'WIDGET.DESCRIPTION_KEY': 'Translated Widget Description' + }; + + beforeEach(() => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + imports: [TestingModule, SiWidgetCatalogComponent], + providers: [ + { provide: ModalRef, useValue: new ModalRef() }, + provideMockTranslateServiceBuilder( + () => + ({ + translate: (key: string) => translations[key] ?? key, + translateSync: (key: string) => translations[key] ?? key, + translationChange: NEVER + }) as unknown as SiTranslateService + ) + ] + }); + + fixture = TestBed.createComponent(SiWidgetCatalogComponent); + component = fixture.componentInstance; + }); + + it('should display translated widget name and description', () => { + component.widgetCatalog = [ + { + ...createTestingWidget('WIDGET.NAME_KEY', 'translatable-1'), + description: 'WIDGET.DESCRIPTION_KEY' + } + ]; + fixture.detectChanges(); + + const listItems = fixture.debugElement.queryAll(By.css('.list-group-item')); + expect(listItems.length).toBe(1); + expect(listItems[0].query(By.css('.si-h5')).nativeElement.textContent).toBe( + 'Translated Widget Name' + ); + expect(listItems[0].query(By.css('.si-body')).nativeElement.textContent).toBe( + 'Translated Widget Description' + ); + }); + + it('should filter widgets by translated name', () => { + component.widgetCatalog = [ + { + ...createTestingWidget('WIDGET.NAME_KEY', 'translatable-1'), + description: 'WIDGET.DESCRIPTION_KEY' + }, + createTestingWidget('Other Widget', 'other-1') + ]; + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(By.css('.list-group-item')).length).toBe(2); + + fixture.debugElement + .query(By.css('si-search-bar')) + .triggerEventHandler('searchChange', 'Translated'); + fixture.detectChanges(); + + expect(fixture.debugElement.queryAll(By.css('.list-group-item')).length).toBe(1); + expect( + fixture.debugElement.query(By.css('.list-group-item .si-h5')).nativeElement.textContent + ).toBe('Translated Widget Name'); + }); + }); }); diff --git a/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.ts b/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.ts index a3949e4fbd..badeb5ba54 100644 --- a/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.ts +++ b/projects/dashboards-ng/src/components/widget-catalog/si-widget-catalog.component.ts @@ -23,7 +23,11 @@ import { SiActionDialogService } from '@siemens/element-ng/action-modal'; import { SiCircleStatusComponent } from '@siemens/element-ng/circle-status'; import { SiEmptyStateComponent } from '@siemens/element-ng/empty-state'; import { SiSearchBarComponent } from '@siemens/element-ng/search-bar'; -import { SiTranslatePipe, t } from '@siemens/element-translate-ng/translate'; +import { + injectSiTranslateService, + SiTranslatePipe, + t +} from '@siemens/element-translate-ng/translate'; import { Subscription } from 'rxjs'; import { @@ -108,6 +112,8 @@ export class SiWidgetCatalogComponent implements OnInit, OnDestroy { () => !!this.selected()?.componentFactory.editorComponentName ); + private readonly translateService = injectSiTranslateService(); + protected labelCancel = t(() => $localize`:@@DASHBOARD.WIDGET_LIBRARY.CANCEL:Cancel`); protected labelPrevious = t(() => $localize`:@@DASHBOARD.WIDGET_LIBRARY.PREVIOUS:Previous`); protected labelNext = t(() => $localize`:@@DASHBOARD.WIDGET_LIBRARY.NEXT:Next`); @@ -194,9 +200,11 @@ export class SiWidgetCatalogComponent implements OnInit, OnDestroy { this.filteredWidgetCatalog = this.widgetCatalog; } else { this.searchTerm = searchTerm; - this.filteredWidgetCatalog = this.widgetCatalog.filter(wd => - wd.name.toLowerCase().includes(searchTerm.trim().toLowerCase()) - ); + const term = searchTerm.trim().toLowerCase(); + this.filteredWidgetCatalog = this.widgetCatalog.filter(wd => { + const name = this.translateService.translateSync(wd.name); + return name.toLowerCase().includes(term); + }); } if (this.filteredWidgetCatalog.length > 0) { this.selectWidget(this.filteredWidgetCatalog[0]); diff --git a/projects/dashboards-ng/src/model/widgets.model.ts b/projects/dashboards-ng/src/model/widgets.model.ts index 42548aee98..4de5660412 100644 --- a/projects/dashboards-ng/src/model/widgets.model.ts +++ b/projects/dashboards-ng/src/model/widgets.model.ts @@ -6,6 +6,7 @@ import { EventEmitter, InputSignal, OutputEmitterRef, TemplateRef, Type } from ' import { AccentLineType, MenuItem as MenuItemLegacy } from '@siemens/element-ng/common'; import { ContentActionBarMainItem, ViewType } from '@siemens/element-ng/content-action-bar'; import { MenuItem } from '@siemens/element-ng/menu'; +import { TranslatableString } from '@siemens/element-translate-ng/translate'; import { Subject } from 'rxjs'; /** @@ -19,9 +20,9 @@ export interface Widget { /** An optional version string. */ version?: string; /** The name of the widget that is presented in the widget catalog. */ - name: string; + name: TranslatableString; /** An optional description that is visible in the widget catalog. */ - description?: string; + description?: TranslatableString; /** A CSS icon class that specifies the widget icon, displayed in the catalog. */ iconClass?: string; /** The factory to instantiate a widget instance component that is added to the dashboard. */ @@ -183,7 +184,7 @@ export interface WidgetConfig { /** * grid item header text. */ - heading?: string; + heading?: TranslatableString; /** Defines whether the widget instance component can be expanded and enlarged over the dashboard. */ expandable?: boolean; /** A widget specific payload object. Placeholder to pass in additional configuration. */