feat(workspace): add i18n support for dashboard widgets and update translations

This commit is contained in:
4gray committed 2026-02-24 21:14:34 +01:00
1 parent edfcffb7d9
commit 9193f8bb80
15 files changed
+285 -46

No files matched your search

+43
View File
@@ -428,5 +428,48 @@
"DOWN_CHANNEL": "Sender abwärts",
"HEADER": "IPTVNator Fernbedienung",
"FOOTER": "IPTVNator Fernbedienung"
},
"WORKSPACE": {
"FAVORITES_ONLY": "Nur Favoriten",
"SORT_DATE_DESC": "Datum (Neueste zuerst)",
"SORT_DATE_ASC": "Datum (Älteste zuerst)",
"SORT_NAME_ASC": "Name A-Z",
"SORT_NAME_DESC": "Name Z-A",
"SORT_LABEL": "Sortierung: ",
"DASHBOARD": {
"UNSUPPORTED_WIDGET": "Nicht unterstütztes Widget",
"UNSUPPORTED_WIDGET_DESC": "Dieser Widget-Typ ist in diesem Build nicht verfügbar.",
"CONTINUE_WATCHING": "Weiter schauen",
"FOCUS": "Fokus",
"UNTITLED_SOURCE": "Unbenannte Quelle",
"OPEN_SOURCE": "Quelle öffnen",
"MANAGE_SOURCES": "Quellen verwalten",
"NO_ACTIVE_SOURCE": "Noch keine aktive Quelle ausgewählt.",
"ADD_SOURCE": "Quelle hinzufügen",
"RECENTLY_USED": "Zuletzt verwendet",
"GLOBAL_FAVORITES": "Globale Favoriten",
"LIBRARY": "Bibliothek",
"FILTER_ITEMS": "Elemente filtern",
"ALL": "Alle",
"CHANNELS": "Sender",
"VOD": "VOD",
"SERIES": "Serien",
"LIST_VIEW": "Listenansicht",
"GRID_VIEW": "Gitteransicht",
"NO_FAVORITES": "Keine Favoriten für diesen Filter gefunden.",
"RECENT_SOURCES": "Letzte Quellen",
"MANAGE_ALL": "Alle verwalten",
"NO_SOURCES": "Noch keine Quellen verfügbar.",
"OPEN_SOURCES": "Quellen öffnen",
"RECENTLY_WATCHED": "Zuletzt angesehen",
"ACTIVITY": "Aktivität",
"NO_HISTORY": "Keine Elemente für diesen Filter gefunden.",
"SOURCE_STATS": "Quellen-Statistiken",
"OVERVIEW": "Übersicht",
"TOTAL": "Gesamt",
"XTREAM": "Xtream",
"STALKER": "Stalker",
"M3U": "M3U"
}
}
}
+45
View File
@@ -142,6 +142,8 @@
},
"SETTINGS": {
"TITLE": "Settings",
"GENERAL": "General",
"GENERAL_SUBTITLE": "Change the configuration of the application",
"SUBTITLE": "Change the configuration of the application",
"DESCRIPTION": "Please adapt the options from the list below and confirm with the save changes button",
"EPG_URL_LABEL": "EPG Url (*.xml or *.xml.gz)",
@@ -490,5 +492,48 @@
"DOWN_CHANNEL": "Channel Down",
"HEADER": "IPTVNator Remote Control",
"FOOTER": "IPTVNator Remote Control"
},
"WORKSPACE": {
"FAVORITES_ONLY": "Favorites only",
"SORT_DATE_DESC": "Date Added (Latest First)",
"SORT_DATE_ASC": "Date Added (Oldest First)",
"SORT_NAME_ASC": "Name A-Z",
"SORT_NAME_DESC": "Name Z-A",
"SORT_LABEL": "Sort: ",
"DASHBOARD": {
"UNSUPPORTED_WIDGET": "Unsupported widget",
"UNSUPPORTED_WIDGET_DESC": "This widget type is not available in this build.",
"CONTINUE_WATCHING": "Continue Watching",
"FOCUS": "Focus",
"UNTITLED_SOURCE": "Untitled source",
"OPEN_SOURCE": "Open Source",
"MANAGE_SOURCES": "Manage Sources",
"NO_ACTIVE_SOURCE": "No active source selected yet.",
"ADD_SOURCE": "Add Source",
"RECENTLY_USED": "Recently used",
"GLOBAL_FAVORITES": "Global Favorites",
"LIBRARY": "Library",
"FILTER_ITEMS": "Filter items",
"ALL": "All",
"CHANNELS": "Channels",
"VOD": "VOD",
"SERIES": "Series",
"LIST_VIEW": "List view",
"GRID_VIEW": "Grid view",
"NO_FAVORITES": "No favorites found for this filter.",
"RECENT_SOURCES": "Recent Sources",
"MANAGE_ALL": "Manage all",
"NO_SOURCES": "No sources available yet.",
"OPEN_SOURCES": "Open Sources",
"RECENTLY_WATCHED": "Recently Watched",
"ACTIVITY": "Activity",
"NO_HISTORY": "No items found for this filter.",
"SOURCE_STATS": "Source Statistics",
"OVERVIEW": "Overview",
"TOTAL": "Total",
"XTREAM": "Xtream",
"STALKER": "Stalker",
"M3U": "M3U"
}
}
}
+43
View File
@@ -404,5 +404,48 @@
"DOWN_CHANNEL": "Канал вниз",
"HEADER": "IPTVNator Дистанционное управление",
"FOOTER": "IPTVNator Дистанционное управление"
},
"WORKSPACE": {
"FAVORITES_ONLY": "Только избранное",
"SORT_DATE_DESC": "Дата добавления (Сначала новые)",
"SORT_DATE_ASC": "Дата добавления (Сначала старые)",
"SORT_NAME_ASC": "Имя (А-Я)",
"SORT_NAME_DESC": "Имя (Я-А)",
"SORT_LABEL": "Сортировка: ",
"DASHBOARD": {
"UNSUPPORTED_WIDGET": "Неподдерживаемый виджет",
"UNSUPPORTED_WIDGET_DESC": "Этот тип виджета недоступен в данной сборке.",
"CONTINUE_WATCHING": "Продолжить просмотр",
"FOCUS": "Фокус",
"UNTITLED_SOURCE": "Источник без названия",
"OPEN_SOURCE": "Открыть источник",
"MANAGE_SOURCES": "Управление источниками",
"NO_ACTIVE_SOURCE": "Активный источник пока не выбран.",
"ADD_SOURCE": "Добавить источник",
"RECENTLY_USED": "Недавно использованные",
"GLOBAL_FAVORITES": "Все фавориты",
"LIBRARY": "Библиотека",
"FILTER_ITEMS": "Фильтровать",
"ALL": "Все",
"CHANNELS": "Каналы",
"VOD": "Видео",
"SERIES": "Сериалы",
"LIST_VIEW": "В виде списка",
"GRID_VIEW": "В виде сетки",
"NO_FAVORITES": "Для этого фильтра нет избранного.",
"RECENT_SOURCES": "Недавние источники",
"MANAGE_ALL": "Управлять всем",
"NO_SOURCES": "Источники пока недоступны.",
"OPEN_SOURCES": "Открыть источники",
"RECENTLY_WATCHED": "Недавно просмотренные",
"ACTIVITY": "Активность",
"NO_HISTORY": "Для этого фильтра элементы не найдены.",
"SOURCE_STATS": "Статистика источников",
"OVERVIEW": "Обзор",
"TOTAL": "Всего",
"XTREAM": "Xtream",
"STALKER": "Stalker",
"M3U": "M3U"
}
}
}
@@ -16,8 +16,10 @@
}
@default {
<section class="unsupported-widget">
<h3>Unsupported widget</h3>
<p>This widget type is not available in this build.</p>
<h3>{{ 'WORKSPACE.DASHBOARD.UNSUPPORTED_WIDGET' | translate }}</h3>
<p>
{{ 'WORKSPACE.DASHBOARD.UNSUPPORTED_WIDGET_DESC' | translate }}
</p>
</section>
}
}
@@ -1,4 +1,5 @@
import { Component, input } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import { DashboardWidgetConfig } from 'workspace-dashboard-data-access';
import { ContinueWatchingWidgetComponent } from './widgets/continue-watching-widget.component';
import { GlobalFavoritesWidgetComponent } from './widgets/global-favorites-widget.component';
@@ -14,6 +15,7 @@ import { SourceStatsWidgetComponent } from './widgets/source-stats-widget.compon
RecentSourcesWidgetComponent,
RecentlyWatchedWidgetComponent,
SourceStatsWidgetComponent,
TranslatePipe,
],
templateUrl: './dashboard-widget-host.component.html',
styleUrl: './dashboard-widget-host.component.scss',
@@ -1,42 +1,63 @@
<app-dashboard-widget-shell eyebrow="Focus" title="Continue Watching">
<app-dashboard-widget-shell
[eyebrow]="'WORKSPACE.DASHBOARD.FOCUS' | translate"
[title]="'WORKSPACE.DASHBOARD.CONTINUE_WATCHING' | translate"
>
@if (data.activePlaylist(); as active) {
<article class="focus-source">
<p class="focus-source__provider">
{{ data.getPlaylistProvider(active) }}
</p>
<h4
[attr.title]="active.title || active.filename || 'Untitled source'"
[attr.title]="
active.title ||
active.filename ||
('WORKSPACE.DASHBOARD.UNTITLED_SOURCE' | translate)
"
>
{{ active.title || active.filename || 'Untitled source' }}
{{
active.title ||
active.filename ||
('WORKSPACE.DASHBOARD.UNTITLED_SOURCE' | translate)
}}
</h4>
<div class="focus-source__actions">
<a mat-flat-button [routerLink]="data.getPlaylistLink(active)">
<mat-icon>play_arrow</mat-icon>
Open Source
{{ 'WORKSPACE.DASHBOARD.OPEN_SOURCE' | translate }}
</a>
<a mat-stroked-button routerLink="/workspace/sources">
<mat-icon>library_books</mat-icon>
Manage Sources
{{ 'WORKSPACE.DASHBOARD.MANAGE_SOURCES' | translate }}
</a>
</div>
</article>
} @else {
<article class="focus-empty">
<p>No active source selected yet.</p>
<p>{{ 'WORKSPACE.DASHBOARD.NO_ACTIVE_SOURCE' | translate }}</p>
<a mat-flat-button routerLink="/workspace/sources">
<mat-icon>add</mat-icon>
Add Source
{{ 'WORKSPACE.DASHBOARD.ADD_SOURCE' | translate }}
</a>
</article>
}
@if (data.quickRecent().length > 0) {
<div class="quick-list">
<p class="quick-list__title">Recently used</p>
<p class="quick-list__title">
{{ 'WORKSPACE.DASHBOARD.RECENTLY_USED' | translate }}
</p>
@for (item of data.quickRecent(); track item._id) {
<a class="quick-item" [routerLink]="data.getPlaylistLink(item)">
<span class="quick-item__title" [attr.title]="item.title || item.filename">
{{ item.title || item.filename || 'Untitled source' }}
<span
class="quick-item__title"
[attr.title]="item.title || item.filename"
>
{{
item.title ||
item.filename ||
('WORKSPACE.DASHBOARD.UNTITLED_SOURCE'
| translate)
}}
</span>
<span class="quick-item__meta">
{{ data.getPlaylistProvider(item) }}
@@ -2,12 +2,19 @@ import { Component, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { RouterLink } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import { DashboardDataService } from 'workspace-dashboard-data-access';
import { DashboardWidgetShellComponent } from './dashboard-widget-shell.component';
@Component({
selector: 'app-continue-watching-widget',
imports: [MatButtonModule, MatIcon, RouterLink, DashboardWidgetShellComponent],
imports: [
MatButtonModule,
MatIcon,
RouterLink,
DashboardWidgetShellComponent,
TranslatePipe,
],
templateUrl: './continue-watching-widget.component.html',
styleUrl: './continue-watching-widget.component.scss',
})
@@ -1,10 +1,13 @@
<app-dashboard-widget-shell eyebrow="Library" title="Global Favorites">
<app-dashboard-widget-shell
[eyebrow]="'WORKSPACE.DASHBOARD.LIBRARY' | translate"
[title]="'WORKSPACE.DASHBOARD.GLOBAL_FAVORITES' | translate"
>
<ng-container widget-actions>
<button
mat-icon-button
[matMenuTriggerFor]="filterMenu"
aria-label="Filter global favorites content"
matTooltip="Filter items"
[matTooltip]="'WORKSPACE.DASHBOARD.FILTER_ITEMS' | translate"
>
<mat-icon>filter_list</mat-icon>
</button>
@@ -14,7 +17,9 @@
selectedKind() === 'all' ? 'check' : ''
}}</mat-icon>
<span [class.active-filter]="selectedKind() === 'all'"
>All ({{ counts().all }})</span
>{{ 'WORKSPACE.DASHBOARD.ALL' | translate }} ({{
counts().all
}})</span
>
</button>
<button mat-menu-item (click)="onKindChange('channels')">
@@ -22,7 +27,9 @@
selectedKind() === 'channels' ? 'check' : ''
}}</mat-icon>
<span [class.active-filter]="selectedKind() === 'channels'"
>Channels ({{ counts().channels }})</span
>{{ 'WORKSPACE.DASHBOARD.CHANNELS' | translate }} ({{
counts().channels
}})</span
>
</button>
<button mat-menu-item (click)="onKindChange('vod')">
@@ -30,7 +37,9 @@
selectedKind() === 'vod' ? 'check' : ''
}}</mat-icon>
<span [class.active-filter]="selectedKind() === 'vod'"
>VOD ({{ counts().vod }})</span
>{{ 'WORKSPACE.DASHBOARD.VOD' | translate }} ({{
counts().vod
}})</span
>
</button>
<button mat-menu-item (click)="onKindChange('series')">
@@ -38,7 +47,9 @@
selectedKind() === 'series' ? 'check' : ''
}}</mat-icon>
<span [class.active-filter]="selectedKind() === 'series'"
>Series ({{ counts().series }})</span
>{{ 'WORKSPACE.DASHBOARD.SERIES' | translate }} ({{
counts().series
}})</span
>
</button>
</mat-menu>
@@ -50,10 +61,16 @@
aria-label="Select global favorites view mode"
(change)="onViewModeChange($event.value)"
>
<mat-button-toggle value="list" aria-label="List view">
<mat-button-toggle
value="list"
[aria-label]="'WORKSPACE.DASHBOARD.LIST_VIEW' | translate"
>
<mat-icon>view_list</mat-icon>
</mat-button-toggle>
<mat-button-toggle value="grid" aria-label="Grid view">
<mat-button-toggle
value="grid"
[aria-label]="'WORKSPACE.DASHBOARD.GRID_VIEW' | translate"
>
<mat-icon>grid_view</mat-icon>
</mat-button-toggle>
</mat-button-toggle-group>
@@ -61,7 +78,7 @@
<app-dashboard-activity-items
[items]="activityItems()"
[emptyLabel]="'No favorites found for this filter.'"
[emptyLabel]="'WORKSPACE.DASHBOARD.NO_FAVORITES' | translate"
[viewMode]="viewMode()"
(scrolledToEnd)="onItemsScrolledToEnd()"
/>
@@ -3,6 +3,8 @@ import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatIcon } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
import {
DashboardContentKind,
DashboardDataService,
@@ -30,8 +32,10 @@ const PAGE_SIZE = 20;
MatButtonToggleModule,
MatMenuModule,
MatIcon,
MatTooltipModule,
DashboardActivityItemsComponent,
DashboardWidgetShellComponent,
TranslatePipe,
],
templateUrl: './global-favorites-widget.component.html',
styleUrl: './global-favorites-widget.component.scss',
@@ -1,5 +1,10 @@
<app-dashboard-widget-shell eyebrow="Library" title="Recent Sources">
<a widget-actions routerLink="/workspace/sources">Manage all</a>
<app-dashboard-widget-shell
[eyebrow]="'WORKSPACE.DASHBOARD.LIBRARY' | translate"
[title]="'WORKSPACE.DASHBOARD.RECENT_SOURCES' | translate"
>
<a widget-actions routerLink="/workspace/sources">{{
'WORKSPACE.DASHBOARD.MANAGE_ALL' | translate
}}</a>
@if (data.recentPlaylists().length > 0) {
<div class="source-list">
@for (item of data.recentPlaylists(); track item._id) {
@@ -9,20 +14,44 @@
{{ data.getPlaylistProvider(item) }}
</span>
@if (item.count) {
<span class="count">{{ item.count }} channels</span>
<span class="count"
>{{ item.count }}
{{
'WORKSPACE.DASHBOARD.CHANNELS' | translate
}}</span
>
}
</div>
<h4 [attr.title]="item.title || item.filename || 'Untitled source'">
{{ item.title || item.filename || 'Untitled source' }}
<h4
[attr.title]="
item.title ||
item.filename ||
('WORKSPACE.DASHBOARD.UNTITLED_SOURCE' | translate)
"
>
{{
item.title ||
item.filename ||
('WORKSPACE.DASHBOARD.UNTITLED_SOURCE'
| translate)
}}
</h4>
<p>{{ data.formatTimestamp(item.updateDate || item.importDate) }}</p>
<p>
{{
data.formatTimestamp(
item.updateDate || item.importDate
)
}}
</p>
</a>
}
</div>
} @else {
<article class="empty-state">
<p>No sources available yet.</p>
<a routerLink="/workspace/sources">Open Sources</a>
<p>{{ 'WORKSPACE.DASHBOARD.NO_SOURCES' | translate }}</p>
<a routerLink="/workspace/sources">{{
'WORKSPACE.DASHBOARD.OPEN_SOURCES' | translate
}}</a>
</article>
}
</app-dashboard-widget-shell>
@@ -1,11 +1,12 @@
import { Component, inject } from '@angular/core';
import { RouterLink } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
import { DashboardDataService } from 'workspace-dashboard-data-access';
import { DashboardWidgetShellComponent } from './dashboard-widget-shell.component';
@Component({
selector: 'app-recent-sources-widget',
imports: [RouterLink, DashboardWidgetShellComponent],
imports: [RouterLink, DashboardWidgetShellComponent, TranslatePipe],
templateUrl: './recent-sources-widget.component.html',
styleUrl: './recent-sources-widget.component.scss',
})
@@ -1,10 +1,13 @@
<app-dashboard-widget-shell eyebrow="Activity" title="Recently Watched">
<app-dashboard-widget-shell
[eyebrow]="'WORKSPACE.DASHBOARD.ACTIVITY' | translate"
[title]="'WORKSPACE.DASHBOARD.RECENTLY_WATCHED' | translate"
>
<ng-container widget-actions>
<button
mat-icon-button
[matMenuTriggerFor]="filterMenu"
aria-label="Filter recently watched content"
matTooltip="Filter items"
[matTooltip]="'WORKSPACE.DASHBOARD.FILTER_ITEMS' | translate"
>
<mat-icon>filter_list</mat-icon>
</button>
@@ -14,7 +17,9 @@
selectedKind() === 'all' ? 'check' : ''
}}</mat-icon>
<span [class.active-filter]="selectedKind() === 'all'"
>All ({{ counts().all }})</span
>{{ 'WORKSPACE.DASHBOARD.ALL' | translate }} ({{
counts().all
}})</span
>
</button>
<button mat-menu-item (click)="onKindChange('channels')">
@@ -22,7 +27,9 @@
selectedKind() === 'channels' ? 'check' : ''
}}</mat-icon>
<span [class.active-filter]="selectedKind() === 'channels'"
>Channels ({{ counts().channels }})</span
>{{ 'WORKSPACE.DASHBOARD.CHANNELS' | translate }} ({{
counts().channels
}})</span
>
</button>
<button mat-menu-item (click)="onKindChange('vod')">
@@ -30,7 +37,9 @@
selectedKind() === 'vod' ? 'check' : ''
}}</mat-icon>
<span [class.active-filter]="selectedKind() === 'vod'"
>VOD ({{ counts().vod }})</span
>{{ 'WORKSPACE.DASHBOARD.VOD' | translate }} ({{
counts().vod
}})</span
>
</button>
<button mat-menu-item (click)="onKindChange('series')">
@@ -38,7 +47,9 @@
selectedKind() === 'series' ? 'check' : ''
}}</mat-icon>
<span [class.active-filter]="selectedKind() === 'series'"
>Series ({{ counts().series }})</span
>{{ 'WORKSPACE.DASHBOARD.SERIES' | translate }} ({{
counts().series
}})</span
>
</button>
</mat-menu>
@@ -50,10 +61,16 @@
aria-label="Select recently watched view mode"
(change)="onViewModeChange($event.value)"
>
<mat-button-toggle value="list" aria-label="List view">
<mat-button-toggle
value="list"
[aria-label]="'WORKSPACE.DASHBOARD.LIST_VIEW' | translate"
>
<mat-icon>view_list</mat-icon>
</mat-button-toggle>
<mat-button-toggle value="grid" aria-label="Grid view">
<mat-button-toggle
value="grid"
[aria-label]="'WORKSPACE.DASHBOARD.GRID_VIEW' | translate"
>
<mat-icon>grid_view</mat-icon>
</mat-button-toggle>
</mat-button-toggle-group>
@@ -61,7 +78,7 @@
<app-dashboard-activity-items
[items]="activityItems()"
[emptyLabel]="'No items found for this filter.'"
[emptyLabel]="'WORKSPACE.DASHBOARD.NO_HISTORY' | translate"
[viewMode]="viewMode()"
(scrolledToEnd)="onItemsScrolledToEnd()"
/>
@@ -3,6 +3,8 @@ import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle';
import { MatIcon } from '@angular/material/icon';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
import {
DashboardContentKind,
DashboardDataService,
@@ -30,8 +32,10 @@ const PAGE_SIZE = 20;
MatButtonToggleModule,
MatMenuModule,
MatIcon,
MatTooltipModule,
DashboardActivityItemsComponent,
DashboardWidgetShellComponent,
TranslatePipe,
],
templateUrl: './recently-watched-widget.component.html',
styleUrl: './recently-watched-widget.component.scss',
@@ -1,19 +1,22 @@
<app-dashboard-widget-shell eyebrow="Overview" title="Source Statistics">
<app-dashboard-widget-shell
[eyebrow]="'WORKSPACE.DASHBOARD.OVERVIEW' | translate"
[title]="'WORKSPACE.DASHBOARD.SOURCE_STATS' | translate"
>
<div class="stats-grid">
<article class="stat-tile">
<p class="label">Total</p>
<p class="label">{{ 'WORKSPACE.DASHBOARD.TOTAL' | translate }}</p>
<p class="value">{{ data.stats().total }}</p>
</article>
<article class="stat-tile">
<p class="label">Xtream</p>
<p class="label">{{ 'WORKSPACE.DASHBOARD.XTREAM' | translate }}</p>
<p class="value">{{ data.stats().xtream }}</p>
</article>
<article class="stat-tile">
<p class="label">Stalker</p>
<p class="label">{{ 'WORKSPACE.DASHBOARD.STALKER' | translate }}</p>
<p class="value">{{ data.stats().stalker }}</p>
</article>
<article class="stat-tile">
<p class="label">M3U</p>
<p class="label">{{ 'WORKSPACE.DASHBOARD.M3U' | translate }}</p>
<p class="value">{{ data.stats().m3u }}</p>
</article>
</div>
@@ -1,10 +1,11 @@
import { Component, inject } from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import { DashboardDataService } from 'workspace-dashboard-data-access';
import { DashboardWidgetShellComponent } from './dashboard-widget-shell.component';
@Component({
selector: 'app-source-stats-widget',
imports: [DashboardWidgetShellComponent],
imports: [DashboardWidgetShellComponent, TranslatePipe],
templateUrl: './source-stats-widget.component.html',
styleUrl: './source-stats-widget.component.scss',
})