Files
iptvnator/libs/ui/components/src/lib/content-hero/content-hero.component.html
T
4grayandClaude Opus 5.5 6e18983400 fix(i18n): translate hard-coded labels, snackbars and missing keys (#1867)
* fix(i18n): translate hard-coded labels, snackbars and missing keys

Catalog screens, snackbars and external player messages showed English
in every locale, and seven keys used in code were missing from en.json,
so ngx-translate rendered them raw.

- Catalog: "All items", item and channel counts, channel sort menus and
  tooltips, unnamed-category and empty-category labels, LIVE/PAUSED
  badges and the Xtream global search summary are translated. The Xtream
  and Stalker stores no longer bake an English name into the every-item
  sentinel; the facades return a null title and the view translates it.
- Snackbars: Xtream/Stalker request failures, the 413 upload error,
  backup export/import results and the category visibility failure use
  keys; every "Close" action uses CLOSE.
- External player: the main process sends an error code instead of an
  English sentence (player-error event, session errorCode, and a tag in
  rejected launch errors); the renderer, dock and VOD primary button
  translate it. The external player info dialog is translated.
- Adds the seven missing keys and 43 new ones, translated in all 18
  locales; seven legitimately identical values are baselined.
- tools/i18n/check-usage.mjs fails on keys used in code but missing from
  en.json; it runs in i18n:check (and so in CI through i18n:validate).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(i18n): translate catalog counts in the template and skip inline template comments

- The category subtitle hands a key to the translate pipe instead of
  caching translate.instant(), so a cold start re-renders it once the
  language file loads.
- The Xtream live root count uses the singular/plural item keys, so one
  result no longer reads "1 channels".
- check-usage.mjs strips HTML comments inside inline templates of
  TypeScript files, so a commented-out key no longer fails the check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(i18n): check keys in parenthesised translate pipe operands

`(expanded() ? 'SHOW_LESS' : 'SHOW_MORE') | translate` yields keys that
neither precede the pipe directly nor contain a dot, so the usage check
missed them. It now reads the ternary and fallback branches of a
parenthesised operand; a literal compared in the condition is not read
as a key.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(e2e): expect the translated external player failure in the dock

A launch failure without an error code now shows the translated generic
status in the playback dock, with the raw main-process detail as its
tooltip. The ClearKey DASH flow asserted the raw English text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(playback): keep the IPC error tag out of the stored session detail

A tagged launch failure reached ExternalPlayerSession.error unchanged, so
the dock tooltip showed "[iptvnator:external-player:start-failed]". The
registry now strips the tag when it stores the detail; the rejected IPC
error keeps it for the renderer to read the code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(web): expect translated portal request failure toasts

#1861's new resolved-failure specs asserted the English toast text; the
toasts now go through PORTALS.REQUEST_ERRORS keys, so the specs check
the key and its message/status params.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(i18n): read only returned branches of a grouped translate operand

A literal at the start of a condition, as in
`('ERROR' === status() ? 'CLOSE' : 'CLOSE') | translate`, was taken for
a translation key, so a valid template could fail the usage check. A
grouped literal now counts only when it ends its operand (end of group,
`:`, `||` or `??`).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 13:02:21 +02:00

208 lines
8.0 KiB
HTML

<!-- Hero Section with Backdrop -->
<!-- The skeleton keeps the stage height: whether the title has a backdrop
is unknown until it loads, and most detail pages do. -->
<section
class="hero"
[class.hero--compact]="layout() === 'compact' && !isLoading()"
>
@if (errorMessage()) {
<div class="hero__content hero__error">
<mat-icon class="error-icon">cloud_off</mat-icon>
<h2 class="error-title">{{ errorMessage() }}</h2>
<ng-content select="[hero-error-actions]"></ng-content>
</div>
} @else if (isLoading()) {
<!-- Placeholders at the loaded hero's geometry, bottom-aligned like
it: eyebrow, title, chips, two description lines, the action
row and the credits. -->
<div
class="hero__content hero__content--skeleton"
data-test-id="hero-skeleton"
aria-hidden="true"
>
<div class="poster">
<span class="skeleton skeleton--poster"></span>
</div>
<div class="details">
<span class="skeleton skeleton--kind"></span>
<span class="skeleton skeleton--title"></span>
<span class="details__tags">
<span class="skeleton skeleton--chip"></span>
<span class="skeleton skeleton--chip"></span>
<span class="skeleton skeleton--chip"></span>
</span>
<span class="skeleton-lines">
<span class="skeleton skeleton--line"></span>
<span class="skeleton skeleton--line"></span>
</span>
<span class="action-buttons">
<span class="skeleton skeleton--primary"></span>
<span class="skeleton skeleton--icon-button"></span>
<span class="skeleton skeleton--icon-button"></span>
</span>
<span class="skeleton-lines skeleton-lines--meta">
<span class="skeleton skeleton--line"></span>
<span class="skeleton skeleton--line"></span>
</span>
</div>
</div>
} @else {
<!-- Backdrop Image -->
<div
class="hero__backdrop"
[class.hero__backdrop--blurred]="usesPosterBackdrop()"
[style.backgroundImage]="
backdropImageUrl() ? null : fallbackBackdropBackground()
"
>
@if (backdropImageUrl(); as backdropImage) {
<img
class="hero__backdrop-image"
[src]="backdropImage"
(error)="onBackdropError()"
alt=""
aria-hidden="true"
/>
}
</div>
@if (trailerBackdropUrl(); as trailerUrl) {
<app-hero-trailer-backdrop
#trailer
class="hero__trailer"
[embedUrl]="trailerUrl"
[title]="formattedTitle()"
/>
<!-- Above the content layer, or the content box would take the clicks -->
@if (trailer.playing()) {
<button
type="button"
class="hero__trailer-mute"
[matTooltip]="
(trailer.muted()
? 'PORTALS.DETAIL.TRAILER_UNMUTE'
: 'PORTALS.DETAIL.TRAILER_MUTE'
) | translate
"
[attr.aria-label]="
(trailer.muted()
? 'PORTALS.DETAIL.TRAILER_UNMUTE'
: 'PORTALS.DETAIL.TRAILER_MUTE'
) | translate
"
[attr.aria-pressed]="!trailer.muted()"
data-test-id="trailer-backdrop-mute"
(click)="trailer.toggleMute()"
>
<mat-icon aria-hidden="true">{{
trailer.muted() ? 'volume_off' : 'volume_up'
}}</mat-icon>
</button>
}
}
<!-- Scrim: strong on the text side, fading into the page below -->
<div class="hero__vignette"></div>
<!-- Hero Content -->
<div class="hero__content">
<!-- Poster Card -->
<div class="poster">
@if (posterUrl() && !posterError()) {
<img
class="poster__image"
[src]="posterUrl()"
(error)="onPosterError()"
alt=""
/>
} @else {
<div
class="poster__placeholder"
[style.backgroundImage]="fallbackPosterBackground()"
>
<mat-icon class="poster__placeholder-icon"
>movie</mat-icon
>
<span class="poster__placeholder-text">{{
formattedTitle()
}}</span>
</div>
}
</div>
<div class="details">
@if (kindLabel(); as kind) {
<p class="details__kind" data-test-id="detail-kind">
{{ kind }}
</p>
}
<h1 class="details__title">{{ formattedTitle() }}</h1>
<!-- Tags Slot -->
<div class="details__tags">
<ng-content select="[hero-tags]"></ng-content>
</div>
<!-- Description -->
@if (description()) {
<p
#descriptionEl
class="details__description"
[class.details__description--expanded]="
isDescriptionExpanded()
"
>
{{ description() }}
</p>
@if (hasDescriptionOverflow() || isDescriptionExpanded()) {
<button
type="button"
class="details__description-toggle"
(click)="toggleDescription()"
[attr.aria-expanded]="isDescriptionExpanded()"
>
<span>{{
(isDescriptionExpanded()
? 'SHOW_LESS'
: 'SHOW_MORE'
) | translate
}}</span>
<mat-icon aria-hidden="true">
{{
isDescriptionExpanded()
? 'expand_less'
: 'expand_more'
}}
</mat-icon>
</button>
}
}
@if (progress() !== null) {
<span
class="details__progress"
role="progressbar"
aria-valuemin="0"
aria-valuemax="100"
[attr.aria-valuenow]="progress()"
data-test-id="detail-progress"
>
<i [style.width.%]="progress()"></i>
</span>
}
<!-- Action Buttons Slot -->
<div class="action-buttons">
<ng-content select="[hero-actions]"></ng-content>
</div>
<!-- Credits / metadata slot -->
<div class="details__meta">
<ng-content select="[hero-meta]"></ng-content>
</div>
</div>
</div>
}
</section>