fix(i18n): translate the remaining hard-coded labels (#1871)

* 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>

* fix(i18n): translate the remaining hard-coded labels

Follow-up to the UI-26 pass. The remaining English UI literals now come
from translation keys, translated in all 18 locales:

- Windows/Linux window controls, playlist switcher title and actions
  menu, the portal status tooltip, dashboard carousel roles and rail
  scroll buttons, the search placeholder, the card remove tooltip, the
  EPG offset unit, the REC chip, the Stalker EPG source label and the
  export file type.
- Embedded MPV failures raised in the renderer and the release-notes
  dialog without a bridge. Settings never showed its English reasons,
  so they are dropped.
- Unnamed Stalker episodes: data access leaves the title empty and the
  series view labels it after the TMDB overlay. Synthetic season names
  stay, because they key persisted episode progress.
- The phone remote-control page, which follows the first browser
  language with a translation and sets <html lang>.

Removes the dead getStatusMessage(), the unused favorites layout and the
translateWithFallback() helpers whose keys now exist.

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

* fix(remote-control): keep the remote build off shared-interfaces

The language resolver imported the Language enum, which made
remote-control-web depend on shared-interfaces. Its build-performance
chain then hit Nx's recursive-invocation guard through the existing
shared-interfaces/shared-logging build loop, failing the Electron E2E
and performance journey builds. The resolver now keeps its own list of
translation codes, and a spec checks it against the locale files the page
loads.

Also drops the deleted favorites layout from the zoneless checklist,
whose guard spec requires ticked entries to exist.

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

* perf(journeys): raise the launch DOM mutation baseline for translated attributes

Launch now sets 569 DOM mutations before the first dashboard card instead
of 557 (identical in all three CI iterations). The extra twelve come from
attributes this PR moved from static English to translated bindings on
the launch path: the window-control labels and tooltips on Linux, the
hero carousel and slide roles, and the rail scroll-button labels. A
translated attribute is a binding set after the element is attached, so
each costs a mutation. Accepted as a deliberate trade-off; it needs the
perf-baseline-increase label.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 authored and GitHub committed 2026-10-10 16:22:31 +02:00
1 parent a820194ae5
commit 2b400cb81d
85 files changed
+1822 -585

No files matched your search

@@ -0,0 +1,9 @@
---
type: fix
area: i18n
---
The phone remote control page now follows the phone's language. More labels
appear in the app's language too: the Windows and Linux window buttons, the
playlist switcher, unnamed Stalker episodes, embedded MPV errors, the dashboard
carousel and the recording chip.
+14 -3
View File
@@ -4,7 +4,11 @@ import {
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { RemoteControlComponent } from '@iptvnator/ui/remote-control';
import { DOCUMENT } from '@angular/common';
import {
RemoteControlComponent,
resolveRemoteControlLanguage,
} from '@iptvnator/ui/remote-control';
import { TranslateService } from '@ngx-translate/core';
@Component({
@@ -17,10 +21,17 @@ import { TranslateService } from '@ngx-translate/core';
})
export class App implements OnInit {
private translate = inject(TranslateService);
private document = inject(DOCUMENT);
ngOnInit() {
// Set default language
const navigator = this.document.defaultView?.navigator;
const language = resolveRemoteControlLanguage(
navigator?.languages?.length
? navigator.languages
: [navigator?.language ?? 'en']
);
this.document.documentElement.lang = language.documentLanguage;
this.translate.setDefaultLang('en');
this.translate.use('en');
this.translate.use(language.translation);
}
}
@@ -501,8 +501,8 @@ describe('ElectronService', () => {
await Promise.resolve();
expect(snackBar.open).toHaveBeenCalledWith(
'Certificate for this playlist host is invalid.',
'Trust host',
'HOME.URL_UPLOAD.ERROR_INVALID_TLS',
'HOME.URL_UPLOAD.TRUST_TLS_HOST',
{ duration: 10000 }
);
});
+14 -31
View File
@@ -509,16 +509,14 @@ export class ElectronService extends DataService {
errorMessage
)
) {
return this.translateWithFallback(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_NOT_FOUND',
'Playlist refresh failed. The local file is no longer available. Check the file path or re-import the playlist.'
return this.translateService.instant(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_NOT_FOUND'
);
}
if (/(EACCES|EPERM|permission denied)/i.test(errorMessage)) {
return this.translateWithFallback(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_ACCESS_ERROR',
'Playlist refresh failed. The app can no longer access the local file.'
return this.translateService.instant(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_ACCESS_ERROR'
);
}
@@ -542,11 +540,6 @@ export class ElectronService extends DataService {
);
}
private translateWithFallback(key: string, fallback: string): string {
const translated = this.translateService.instant(key);
return translated === key ? fallback : translated;
}
private handlePlaylistSecurityError(
error: unknown,
retry: () => void
@@ -560,14 +553,8 @@ export class ElectronService extends DataService {
}
const ref = this.snackBar.open(
this.translateWithFallback(
'HOME.URL_UPLOAD.ERROR_INVALID_TLS',
'Certificate for this playlist host is invalid.'
),
this.translateWithFallback(
'HOME.URL_UPLOAD.TRUST_TLS_HOST',
'Trust host'
),
this.translateService.instant('HOME.URL_UPLOAD.ERROR_INVALID_TLS'),
this.translateService.instant('HOME.URL_UPLOAD.TRUST_TLS_HOST'),
{ duration: 10000 }
);
@@ -583,9 +570,8 @@ export class ElectronService extends DataService {
): void {
if (!host) {
this.snackBar.open(
this.translateWithFallback(
'HOME.URL_UPLOAD.ERROR_TLS_HOST_UNKNOWN',
'Could not determine the playlist host. Please retry manually.'
this.translateService.instant(
'HOME.URL_UPLOAD.ERROR_TLS_HOST_UNKNOWN'
),
this.translateService.instant('CLOSE'),
{ duration: 5000 }
@@ -594,17 +580,14 @@ export class ElectronService extends DataService {
}
this.dialogService.openConfirmDialog({
title: this.translateWithFallback(
'HOME.URL_UPLOAD.TRUST_TLS_HOST_TITLE',
'Trust invalid certificate?'
title: this.translateService.instant(
'HOME.URL_UPLOAD.TRUST_TLS_HOST_TITLE'
),
message: this.translateWithFallback(
'HOME.URL_UPLOAD.TRUST_TLS_HOST_WARNING',
'Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.'
message: this.translateService.instant(
'HOME.URL_UPLOAD.TRUST_TLS_HOST_WARNING'
),
confirmLabel: this.translateWithFallback(
'HOME.URL_UPLOAD.TRUST_TLS_HOST',
'Trust host'
confirmLabel: this.translateService.instant(
'HOME.URL_UPLOAD.TRUST_TLS_HOST'
),
width: '420px',
onConfirm: () => {
@@ -170,6 +170,25 @@ describe('AppUpdateReleaseNotesDialogComponent', () => {
).not.toBeNull();
});
it('explains in the active language that the build has no release notes', async () => {
window.electron = {} as unknown as typeof window.electron;
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
const error = fixture.nativeElement.querySelector(
'[data-test-id="release-notes-error"]'
) as HTMLElement;
expect(error.textContent).toContain(
'SETTINGS.APP_UPDATE_RELEASE_NOTES_UNAVAILABLE'
);
expect(error.textContent).not.toContain('not available in this build');
expect(error.classList).not.toContain(
'release-notes-dialog__error--failed'
);
});
it('offers the release list, not the earlier release, when paging fails', async () => {
fixture.detectChanges();
await fixture.whenStable();
@@ -36,8 +36,12 @@ const IPC_REJECTION_PREFIX =
/^Error invoking remote method '[^']*': (?:Error: )?/;
interface ReleaseNotesLoadError {
/** `not-found`: GitHub has no release for that version; `failed`: anything else. */
kind: 'not-found' | 'failed';
/**
* `not-found`: GitHub has no release for that version; `unavailable`: this
* build has no release-notes bridge; `failed`: anything else.
*/
kind: 'not-found' | 'unavailable' | 'failed';
/** Raw detail shown under the generic failure; empty for the other kinds. */
message: string;
version?: string;
}
@@ -129,6 +133,13 @@ function decorateReleaseNotesHtml(html: string): string {
| translate: { version: loadError.version }
}}
</p>
} @else if (loadError.kind === 'unavailable') {
<p>
{{
'SETTINGS.APP_UPDATE_RELEASE_NOTES_UNAVAILABLE'
| translate
}}
</p>
} @else {
<p>
{{
@@ -322,10 +333,7 @@ export class AppUpdateReleaseNotesDialogComponent implements OnInit {
direction?: ElectronBridgeAppUpdateReleaseNotesDirection
): Promise<void> {
if (!window.electron?.getAppUpdateReleaseNotes) {
this.error.set({
kind: 'failed',
message: 'Release notes are not available in this build.',
});
this.error.set({ kind: 'unavailable', message: '' });
return;
}
@@ -43,12 +43,10 @@ export class SettingsEmbeddedMpvFacade {
* for while the page is open, so the option appears without reopening it.
*/
async load(): Promise<void> {
// Settings only reads `supported`; an unsupported answer carries no
// reason because nothing on this page displays one.
if (!this.runtime.isElectron) {
this.support.set({
supported: false,
platform: 'web',
reason: 'Embedded MPV requires the Electron desktop build.',
});
this.support.set({ supported: false, platform: 'web' });
return;
}
@@ -56,7 +54,6 @@ export class SettingsEmbeddedMpvFacade {
this.support.set({
supported: false,
platform: window.electron.platform,
reason: 'Embedded MPV support is not available in this build.',
});
return;
}
@@ -79,7 +79,9 @@
aria-labelledby="epg-offset-label"
data-test-id="epg-offset-minutes"
/>
<span class="settings-stepper__unit">min</span>
<span class="settings-stepper__unit">{{
'SETTINGS.EPG_OFFSET_UNIT' | translate
}}</span>
<button
type="button"
class="settings-stepper__button"
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "إلغاء",
"CLEAR": "مسح",
"CLOSE": "إغلاق",
"REMOVE": "إزالة",
"BACK": "رجوع",
"SHOW_MORE": "المزيد",
"SHOW_LESS": "أقل",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "لم تُضف أي قوائم تشغيل",
"SELECT_PLAYLIST": "اختر قائمة تشغيل",
"UNTITLED_PLAYLIST": "قائمة تشغيل بدون عنوان",
"PLAYLIST_ACTIONS": "إجراءات قائمة التشغيل",
"ADD_FIRST": "يرجى إضافة أول قائمة تشغيل",
"WELCOME_TITLE": "مرحبًا بك في IPTVnator",
"WELCOME_DESCRIPTION": "ابدأ رحلة البث بإضافة قائمة التشغيل الأولى. اتصل بخدمة IPTV المفضلة لديك واستمتع بالتلفزيون المباشر والأفلام والمسلسلات.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "بوابة Stalker",
"CHANNELS": "القنوات",
"CHANNELS_COUNT": "{{count}} قناة",
"PORTAL_STATUS": "حالة البوابة: {{status}}",
"PORTAL_STATUS_ACTIVE": "نشطة",
"PORTAL_STATUS_INACTIVE": "غير نشطة",
"PORTAL_STATUS_EXPIRED": "منتهية الصلاحية",
"PORTAL_STATUS_UNAVAILABLE": "غير متاحة",
"PORTAL_STATUS_CHECKING": "جارٍ التحقق",
"ADDED": "تمت الإضافة",
"SHOW_DETAILS": "عرض تفاصيل القائمة",
"REMOVE": "حذف القائمة",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "تم تصدير القائمة بنجاح.",
"PLAYLIST_EXPORT_ERROR": "خطأ: فشل تصدير القائمة.",
"EXPORT_PLAYLIST_FAILED": "فشل تصدير القائمة.",
"EXPORT_FILE_TYPE": "قائمة تشغيل",
"COPY_URL": "نسخ الرابط إلى الحافظة",
"URL_COPIED": "تم نسخ الرابط إلى الحافظة"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "اختر قائمة تشغيل",
"BROWSE": "تصفح الملفات",
"REJECTED": "تم رفض الملف، تنسيق غير مدعوم {{filename}}.",
"REJECTED_UNNAMED": "تم رفض الملف، تنسيق غير مدعوم.",
"OPEN_FAILED": "تعذر فتح \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "اضبط أوقات البرامج المعروضة دون تحديث الدليل. استخدم الدقائق من -720 إلى 720.",
"EPG_OFFSET_LABEL": "الإزاحة (بالدقائق)",
"EPG_OFFSET_ERROR": "أدخل عددًا صحيحًا من الدقائق بين -720 و720.",
"EPG_OFFSET_UNIT": "د",
"EPG_VIEW_MODE_DESCRIPTION": "اختر طريقة عرض دليل البرامج أسفل المشغل: شريط جدول زمني أفقي، أو قائمة رأسية ليوم واحد.",
"EPG_VIEW_MODE_TIMELINE": "الجدول الزمني",
"EPG_VIEW_MODE_LIST": "قائمة",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "ما الجديد",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "لم يتم العثور على ملاحظات الإصدار {{version}} على GitHub. ربما تمت إزالة الإصدار أو لم يُنشر بعد.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "تعذّر تحميل ملاحظات الإصدار.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "ملاحظات الإصدار غير متاحة في هذه النسخة من التطبيق.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "فتح صفحة الإصدارات",
"APP_UPDATE_CHANNEL": "قناة التحديث",
"APP_UPDATE_CHANNEL_DESCRIPTION": "تتبع Stable الإصدارات المنشورة. تتبع Nightly كل دمج في master وتثبّت الإصدارات التجريبية تلقائيًا. يُطبَّق عند الحفظ.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "انتهى البث المباشر.",
"CHECKING_SUPPORT": "جارٍ التحقق من دعم MPV المضمّن...",
"NOT_AVAILABLE": "MPV المضمّن غير متاح في هذه البيئة.",
"FRAME_VIEW_FAILED": "تعذّرت تهيئة عرض الفيديو في MPV المضمّن.",
"LOADING_STREAM": "جارٍ تحميل البث…",
"PLAY": "تشغيل",
"PAUSE": "إيقاف مؤقت",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "جارٍ التسجيل الآن",
"RECORDINGS": "التسجيلات",
"RECORDING_BADGE": "جارٍ التسجيل",
"RECORDING_CHIP": "تسجيل",
"STOP_RECORDING": "إيقاف التسجيل",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "إزالة التسجيل من مدير التنزيلات؟",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "تحكم في مشغل IPTV عن بعد",
"UP_CHANNEL": "القناة السابقة",
"DOWN_CHANNEL": "القناة التالية",
"NOW_PLAYING": "قيد التشغيل",
"EXECUTING": "جارٍ التنفيذ",
"SOURCE_M3U": "بث مباشر M3U",
"SOURCE_XTREAM": "بث مباشر Xtream",
"SOURCE_STALKER": "بث مباشر Stalker",
"WAITING_FOR_PLAYBACK": "بانتظار التشغيل",
"UNKNOWN_SOURCE": "غير معروف",
"NO_STREAM": "لم يتم اختيار بث مباشر",
"NO_PROGRAM": "لا يوجد برنامج حالي في EPG",
"CHANNEL_ABBR": "قناة",
"CHANNEL_NAVIGATION": "التنقل بين القنوات",
"CHANNEL_NUMBER": "رقم القناة",
"KEYPAD_DELETE": "حذف",
"KEYPAD_CLEAR": "مسح",
"VOLUME": "مستوى الصوت",
"VOLUME_ABBR": "صوت",
"MUTE": "كتم",
"UNMUTE": "إلغاء الكتم",
"VOLUME_UNAVAILABLE": "التحكم في مستوى الصوت غير متاح في وضع المشغل هذا",
"SYNCING_STATUS": "جارٍ مزامنة الحالة…",
"UPDATED_AT": "آخر تحديث {{time}}",
"WAITING_FOR_STATUS": "بانتظار الحالة…",
"ERRORS": {
"CHANNEL_UP": "فشل تبديل القناة للأعلى",
"CHANNEL_DOWN": "فشل تبديل القناة للأسفل",
"SELECT_NUMBER": "فشل التبديل حسب رقم القناة",
"VOLUME_UP": "فشل رفع مستوى الصوت",
"VOLUME_DOWN": "فشل خفض مستوى الصوت",
"TOGGLE_MUTE": "فشل تبديل كتم الصوت",
"STATUS": "فشل جلب حالة التحكم عن بعد"
},
"HEADER": "جهاز تحكم IPTVnator",
"FOOTER": "جهاز تحكم IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "ينتهي خلال {{days}} يوم",
"SOURCE_EXPIRED": "منتهي",
"HERO_LABEL": "مميز",
"HERO_ROLE_CAROUSEL": "عرض دوّار",
"HERO_ROLE_SLIDE": "شريحة",
"HERO_FAVORITE_CHANNEL": "قناة مفضلة",
"HERO_FAVORITE": "مفضل",
"HERO_RECENTLY_ADDED": "أضيف مؤخرًا",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "ستتم إزالة جميع المفضلة من {{type}} عبر جميع قوائم التشغيل. لا يمكن التراجع عن هذا الإجراء.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker Portal",
"WINDOW_MINIMIZE": "تصغير",
"WINDOW_MAXIMIZE": "تكبير",
"WINDOW_RESTORE": "استعادة",
"WINDOW_CLOSE": "إغلاق",
"CHANNELS_COUNT": "{{count}} قناة",
"NO_SOURCES_AVAILABLE": "لا توجد مصادر متاحة",
"ONE_SOURCE_AVAILABLE": "مصدر واحد متاح",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "إلغاء",
"CLEAR": "مسح",
"CLOSE": "سد",
"REMOVE": "حيد",
"BACK": "رجع",
"SHOW_MORE": "وري كثر",
"SHOW_LESS": "وري قل",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "ما تزادت حتى شي قائمة تشغيل",
"SELECT_PLAYLIST": "اختار قائمة تشغيل",
"UNTITLED_PLAYLIST": "قائمة تشغيل بلا عنوان",
"PLAYLIST_ACTIONS": "إجراءات قائمة التشغيل",
"ADD_FIRST": "عفاك زيد قائمة التشغيل الأولى ديالك",
"WELCOME_TITLE": "مرحبا بيك في IPTVnator",
"WELCOME_DESCRIPTION": "بدا رحلة البث ديالك بزيادة قائمة التشغيل الأولى ديالك. ربط مع خدمة IPTV المفضلة عندك وتمتع ببث مباشر، أفلام، وسلاسل.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "بوابة Stalker",
"CHANNELS": "القنوات",
"CHANNELS_COUNT": "{{count}} قناة",
"PORTAL_STATUS": "حالة البوابة: {{status}}",
"PORTAL_STATUS_ACTIVE": "خدامة",
"PORTAL_STATUS_INACTIVE": "ماشي خدامة",
"PORTAL_STATUS_EXPIRED": "سالات",
"PORTAL_STATUS_UNAVAILABLE": "ما متوفراش",
"PORTAL_STATUS_CHECKING": "جاري التحقق",
"ADDED": "تزادت",
"SHOW_DETAILS": "وري تفاصيل قائمة التشغيل",
"REMOVE": "حيد قائمة التشغيل",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "قائمة التشغيل تصدرات بنجاح.",
"PLAYLIST_EXPORT_ERROR": "خطأ: تصدير قائمة التشغيل فشل.",
"EXPORT_PLAYLIST_FAILED": "فشل تصدير قائمة التشغيل.",
"EXPORT_FILE_TYPE": "قائمة تشغيل",
"COPY_URL": "نسخ الرابط للحافظة",
"URL_COPIED": "الرابط تنسخ للحافظة"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "اختار قائمة تشغيل",
"BROWSE": "تصفح الملفات",
"REJECTED": "الملف ترفض، صيغة الملف غير مدعومة {{filename}}.",
"REJECTED_UNNAMED": "الملف ترفض، صيغة الملف غير مدعومة.",
"OPEN_FAILED": "ما تقدرش تحل \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "اضبط أوقات البرامج المعروضة دون تحديث الدليل. استخدم الدقائق من -720 إلى 720.",
"EPG_OFFSET_LABEL": "الإزاحة (بالدقائق)",
"EPG_OFFSET_ERROR": "دخّل عدد صحيح ديال الدقايق ما بين -720 و720.",
"EPG_OFFSET_UNIT": "د",
"EPG_VIEW_MODE_DESCRIPTION": "اختار كيفاش يبان دليل البرامج تحت المشغل: شريط زمني أفقي ولا لائحة عمودية ديال نهار واحد.",
"EPG_VIEW_MODE_TIMELINE": "الخط الزمني",
"EPG_VIEW_MODE_LIST": "لائحة",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "شنو الجديد",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "ما لقيناش ملاحظات الإصدار {{version}} فـ GitHub. يمكن الإصدار تحيد ولا مازال ما تنشرش.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "ما قدرناش نحمّلو ملاحظات الإصدار.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "ملاحظات الإصدار ما متوفراش فهاد النسخة.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "فتح صفحة الإصدارات",
"APP_UPDATE_CHANNEL": "قناة التحديث",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable كتبع الإصدارات المنشورة. Nightly كتبع كل دمج ف master وكتثبّت الإصدارات التجريبية أوتوماتيكيا. كيتطبّق ملي تحفظ.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "البث المباشر سالا.",
"CHECKING_SUPPORT": "جاري التحقق من دعم MPV المدمج...",
"NOT_AVAILABLE": "MPV المدمج ما متوفرش فهاد البيئة.",
"FRAME_VIEW_FAILED": "عرض الفيديو ديال MPV المدمج ما قدرش يبدا.",
"LOADING_STREAM": "جاري تشريج البث…",
"PLAY": "شغل",
"PAUSE": "وقّف",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "كيتسجل دابا",
"RECORDINGS": "التسجيلات",
"RECORDING_BADGE": "تسجيل",
"RECORDING_CHIP": "تسجيل",
"STOP_RECORDING": "وقّف التسجيل",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "نحيدو التسجيل من مدير التحميلات؟",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "تحكم في مشغل IPTV ديالك عن بعد",
"UP_CHANNEL": "القناة اللي قبل",
"DOWN_CHANNEL": "القناة اللي بعد",
"NOW_PLAYING": "كيتشغل دابا",
"EXECUTING": "جاري التنفيذ",
"SOURCE_M3U": "M3U مباشر",
"SOURCE_XTREAM": "Xtream مباشر",
"SOURCE_STALKER": "Stalker مباشر",
"WAITING_FOR_PLAYBACK": "كنتسناو التشغيل",
"UNKNOWN_SOURCE": "غير معروف",
"NO_STREAM": "ما كاين حتى بث مباشر متختار",
"NO_PROGRAM": "ما كاين حتى برنامج EPG دابا",
"CHANNEL_ABBR": "قناة",
"CHANNEL_NAVIGATION": "التنقل بين القنوات",
"CHANNEL_NUMBER": "رقم القناة",
"KEYPAD_DELETE": "حيد",
"KEYPAD_CLEAR": "مسح",
"VOLUME": "الصوت",
"VOLUME_ABBR": "صوت",
"MUTE": "كتم الصوت",
"UNMUTE": "رجّع الصوت",
"VOLUME_UNAVAILABLE": "التحكم فالصوت ما متوفرش فهاد الوضع ديال المشغل",
"SYNCING_STATUS": "جاري مزامنة الحالة…",
"UPDATED_AT": "آخر تحديث {{time}}",
"WAITING_FOR_STATUS": "كنتسناو الحالة…",
"ERRORS": {
"CHANNEL_UP": "ما قدرناش نبدلو للقناة اللي قبل",
"CHANNEL_DOWN": "ما قدرناش نبدلو للقناة اللي بعد",
"SELECT_NUMBER": "ما قدرناش نبدلو القناة بالرقم ديالها",
"VOLUME_UP": "ما قدرناش نعليو الصوت",
"VOLUME_DOWN": "ما قدرناش نقصو الصوت",
"TOGGLE_MUTE": "ما قدرناش نبدلو كتم الصوت",
"STATUS": "ما قدرناش نجيبو حالة التحكم عن بعد"
},
"HEADER": "التحكم عن بعد ديال IPTVnator",
"FOOTER": "التحكم عن بعد ديال IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "كتسالي ف {{days}} يوم",
"SOURCE_EXPIRED": "سالات",
"HERO_LABEL": "مختارين",
"HERO_ROLE_CAROUSEL": "عرض دوّار",
"HERO_ROLE_SLIDE": "شريحة",
"HERO_FAVORITE_CHANNEL": "قناة مفضلة",
"HERO_FAVORITE": "مفضل",
"HERO_RECENTLY_ADDED": "تزاد جديد",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "جميع مفضلة {{type}} في جميع قوائم التشغيل غادي يتحيدو. هاد العملية ما تقدرش ترجع.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker Portal",
"WINDOW_MINIMIZE": "صغّر",
"WINDOW_MAXIMIZE": "كبّر",
"WINDOW_RESTORE": "رجّع الحجم",
"WINDOW_CLOSE": "سد",
"CHANNELS_COUNT": "{{count}} قناة",
"NO_SOURCES_AVAILABLE": "ما كاينش مصادر متوفرة",
"ONE_SOURCE_AVAILABLE": "مصدر واحد متوفر",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Адмена",
"CLEAR": "Ачысціць",
"CLOSE": "Закрыць",
"REMOVE": "Выдаліць",
"BACK": "Назад",
"SHOW_MORE": "Болей",
"SHOW_LESS": "Меней",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Пакуль яшчэ не было дададзена ніводнага плэйліста",
"SELECT_PLAYLIST": "Выберыце плэйліст",
"UNTITLED_PLAYLIST": "Плэйліст без назвы",
"PLAYLIST_ACTIONS": "Дзеянні з плэйлістам",
"ADD_FIRST": "Вы можаце дадаць плэйлісты з суседніх укладак",
"WELCOME_TITLE": "Сардэчна запрашаем у IPTVnator",
"WELCOME_DESCRIPTION": "Пачніце прагляд, дадаўшы свой першы плэйліст. Падключыцеся да ўлюбёнага IPTV-сэрвісу і атрымлівайце асалоду ад прамога эфіру, фільмаў і серыялаў.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker партал",
"CHANNELS": "Каналаў",
"CHANNELS_COUNT": "{{count}} каналаў",
"PORTAL_STATUS": "Стан партала: {{status}}",
"PORTAL_STATUS_ACTIVE": "актыўны",
"PORTAL_STATUS_INACTIVE": "неактыўны",
"PORTAL_STATUS_EXPIRED": "скончыўся",
"PORTAL_STATUS_UNAVAILABLE": "недаступны",
"PORTAL_STATUS_CHECKING": "правяраецца",
"ADDED": "Дададзены",
"SHOW_DETAILS": "Паказаць інфармацыю аб плэйлісце",
"REMOVE": "Выдаліць плэйліст",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Плэйліст быў паспяхова экспартаваны.",
"PLAYLIST_EXPORT_ERROR": "Памылка: не ўдалося экспартаваць плэйліст.",
"EXPORT_PLAYLIST_FAILED": "Не ўдалося экспартаваць плэйліст.",
"EXPORT_FILE_TYPE": "Плэйліст",
"COPY_URL": "Капіяваць URL у буфер абмену",
"URL_COPIED": "URL скапіраваны ў буфер абмену"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "выберыце плэйліст",
"BROWSE": "Агляд файлаў",
"REJECTED": "На жаль, гэты фармат файлаў не падтрымліваецца {{filename}}.",
"REJECTED_UNNAMED": "Файл адхілены: фармат файла не падтрымліваецца.",
"OPEN_FAILED": "Не атрымалася адкрыць \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Змяняйце час паказу праграм без абнаўлення даведніка. Выкарыстоўвайце хвіліны ад -720 да 720.",
"EPG_OFFSET_LABEL": "Зрушэнне (хвіліны)",
"EPG_OFFSET_ERROR": "Увядзіце цэлы лік хвілін ад -720 да 720.",
"EPG_OFFSET_UNIT": "хв",
"EPG_VIEW_MODE_DESCRIPTION": "Выберыце, як паказваць праграму перадач пад прайгравальнікам: гарызантальнай стужкай часу або вертыкальным спісам на адзін дзень.",
"EPG_VIEW_MODE_TIMELINE": "Стужка часу",
"EPG_VIEW_MODE_LIST": "Спіс",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Што новага",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Заўвагі да версіі {{version}} на GitHub не знойдзены. Рэліз мог быць выдалены або яшчэ не апублікаваны.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Не ўдалося загрузіць заўвагі да версіі.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Заўвагі да версіі недаступныя ў гэтай зборцы.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Адкрыць старонку рэлізаў",
"APP_UPDATE_CHANNEL": "Канал абнаўленняў",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable ідзе за выпушчанымі рэлізамі. Nightly ідзе за кожным зліццём у master і аўтаматычна ўсталёўвае папярэднія зборкі. Ужываецца пасля захавання.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "Прамая трансляцыя скончылася.",
"CHECKING_SUPPORT": "Праверка падтрымкі ўбудаванага MPV...",
"NOT_AVAILABLE": "Убудаваны MPV недаступны ў гэтым асяроддзі.",
"FRAME_VIEW_FAILED": "Не ўдалося ініцыялізаваць вобласць відэа ўбудаванага MPV.",
"LOADING_STREAM": "Загрузка патоку…",
"PLAY": "Прайграць",
"PAUSE": "Паўза",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Зараз запісваецца",
"RECORDINGS": "Запісы",
"RECORDING_BADGE": "Ідзе запіс",
"RECORDING_CHIP": "ЗАПІС",
"STOP_RECORDING": "Спыніць запіс",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Выдаліць запіс з менеджара?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Кіруйце вашым IPTV-прайгравальнікам аддалена",
"UP_CHANNEL": "Папярэдні канал",
"DOWN_CHANNEL": "Наступны канал",
"NOW_PLAYING": "Зараз прайграецца",
"EXECUTING": "Выконваецца",
"SOURCE_M3U": "Прамы эфір M3U",
"SOURCE_XTREAM": "Прамы эфір Xtream",
"SOURCE_STALKER": "Прамы эфір Stalker",
"WAITING_FOR_PLAYBACK": "Чаканне прайгравання",
"UNKNOWN_SOURCE": "невядома",
"NO_STREAM": "Жывы паток не выбраны",
"NO_PROGRAM": "Няма бягучай перадачы ў EPG",
"CHANNEL_ABBR": "КАН",
"CHANNEL_NAVIGATION": "Навігацыя па каналах",
"CHANNEL_NUMBER": "Нумар канала",
"KEYPAD_DELETE": "ВЫД",
"KEYPAD_CLEAR": "СКІД",
"VOLUME": "Гучнасць",
"VOLUME_ABBR": "ГУЧ",
"MUTE": "ВЫКЛ. ГУК",
"UNMUTE": "УКЛ. ГУК",
"VOLUME_UNAVAILABLE": "Кіраванне гучнасцю недаступнае ў гэтым рэжыме прайгравальніка",
"SYNCING_STATUS": "Сінхранізацыя стану…",
"UPDATED_AT": "Абноўлена {{time}}",
"WAITING_FOR_STATUS": "Чаканне стану…",
"ERRORS": {
"CHANNEL_UP": "Не ўдалося пераключыцца на папярэдні канал",
"CHANNEL_DOWN": "Не ўдалося пераключыцца на наступны канал",
"SELECT_NUMBER": "Не ўдалося пераключыцца па нумары канала",
"VOLUME_UP": "Не ўдалося павялічыць гучнасць",
"VOLUME_DOWN": "Не ўдалося паменшыць гучнасць",
"TOGGLE_MUTE": "Не ўдалося ўключыць або адключыць гук",
"STATUS": "Не ўдалося атрымаць стан для аддаленага кіравання"
},
"HEADER": "Пульт IPTVnator",
"FOOTER": "Пульт IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Скончыцца праз {{days}} дз.",
"SOURCE_EXPIRED": "Скончыўся",
"HERO_LABEL": "Рэкамендаванае",
"HERO_ROLE_CAROUSEL": "карусель",
"HERO_ROLE_SLIDE": "слайд",
"HERO_FAVORITE_CHANNEL": "Абраны канал",
"HERO_FAVORITE": "Абранае",
"HERO_RECENTLY_ADDED": "Нядаўна дададзена",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Усё выбранае тыпу {{type}} з усіх плэйлістоў будзе выдалена. Гэта дзеянне нельга адмяніць.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker партал",
"WINDOW_MINIMIZE": "Згарнуць",
"WINDOW_MAXIMIZE": "Разгарнуць",
"WINDOW_RESTORE": "Аднавіць",
"WINDOW_CLOSE": "Закрыць",
"CHANNELS_COUNT": "{{count}} каналаў",
"NO_SOURCES_AVAILABLE": "Няма даступных крыніц",
"ONE_SOURCE_AVAILABLE": "1 крыніца даступная",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Abbrechen",
"CLEAR": "Leeren",
"CLOSE": "Schließen",
"REMOVE": "Entfernen",
"BACK": "Zurück",
"SHOW_MORE": "Mehr",
"SHOW_LESS": "Weniger",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Noch keine Playlisten hinzugefügt",
"SELECT_PLAYLIST": "Playlist auswählen",
"UNTITLED_PLAYLIST": "Unbenannte Playlist",
"PLAYLIST_ACTIONS": "Playlist-Aktionen",
"ADD_FIRST": "Bitte fügen Sie eine neue Playlist hinzu",
"WELCOME_TITLE": "Willkommen bei IPTVnator",
"WELCOME_DESCRIPTION": "Beginnen Sie Ihre Streaming-Reise, indem Sie Ihre erste Playlist hinzufügen. Verbinden Sie sich mit Ihrem bevorzugten IPTV-Dienst und genießen Sie Live-TV, Filme und Serien.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker-Portal",
"CHANNELS": "TV-Sender",
"CHANNELS_COUNT": "{{count}} Sender",
"PORTAL_STATUS": "Portalstatus: {{status}}",
"PORTAL_STATUS_ACTIVE": "aktiv",
"PORTAL_STATUS_INACTIVE": "inaktiv",
"PORTAL_STATUS_EXPIRED": "abgelaufen",
"PORTAL_STATUS_UNAVAILABLE": "nicht verfügbar",
"PORTAL_STATUS_CHECKING": "wird geprüft",
"ADDED": "Hinzugefügt am",
"SHOW_DETAILS": "Details anzeigen",
"REMOVE": "Playlist entfernen",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Playlist wurde erfolgreich exportiert.",
"PLAYLIST_EXPORT_ERROR": "Fehler: Playlist-Export fehlgeschlagen.",
"EXPORT_PLAYLIST_FAILED": "Playlist-Export fehlgeschlagen.",
"EXPORT_FILE_TYPE": "Wiedergabeliste",
"COPY_URL": "URL in Zwischenablage kopieren",
"URL_COPIED": "URL in die Zwischenablage kopiert"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "Playlist auswählen",
"BROWSE": "Dateien durchsuchen",
"REJECTED": "Datei wurde abgelehnt, nicht unterstütztes Dateiformat {{filename}}.",
"REJECTED_UNNAMED": "Datei wurde abgelehnt, nicht unterstütztes Dateiformat.",
"OPEN_FAILED": "\"{{filename}}\" konnte nicht geöffnet werden."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Angezeigte Sendezeiten verschieben, ohne den Programmführer zu aktualisieren. Minuten von -720 bis 720 verwenden.",
"EPG_OFFSET_LABEL": "Verschiebung (Minuten)",
"EPG_OFFSET_ERROR": "Geben Sie eine ganze Minutenzahl zwischen -720 und 720 ein.",
"EPG_OFFSET_UNIT": "Min.",
"EPG_VIEW_MODE_DESCRIPTION": "Wählen Sie, wie der Programmführer unter dem Player angezeigt wird: als horizontales Zeitleisten-Band oder als vertikale Tagesliste.",
"EPG_VIEW_MODE_TIMELINE": "Zeitleiste",
"EPG_VIEW_MODE_LIST": "Liste",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Was ist neu",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Für {{version}} wurden auf GitHub keine Release-Notes gefunden. Das Release wurde möglicherweise entfernt oder ist noch nicht veröffentlicht.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Die Release-Notes konnten nicht geladen werden.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Release-Notes sind in diesem Build nicht verfügbar.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Release-Seite öffnen",
"APP_UPDATE_CHANNEL": "Update-Kanal",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable folgt den veröffentlichten Releases. Nightly folgt jedem Merge in master und installiert Vorabversionen automatisch. Gilt nach dem Speichern.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "Der Live-Stream ist beendet.",
"CHECKING_SUPPORT": "Unterstützung für eingebettetes MPV wird geprüft...",
"NOT_AVAILABLE": "Eingebettetes MPV ist in dieser Umgebung nicht verfügbar.",
"FRAME_VIEW_FAILED": "Die Videoansicht des eingebetteten MPV konnte nicht initialisiert werden.",
"LOADING_STREAM": "Stream wird geladen…",
"PLAY": "Wiedergabe",
"PAUSE": "Pause",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Aufnahme läuft",
"RECORDINGS": "Aufnahmen",
"RECORDING_BADGE": "Aufnahme",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Aufnahme stoppen",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Aufnahme aus dem Manager entfernen?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Steuern Sie Ihren IPTV-Player aus der Ferne",
"UP_CHANNEL": "Vorheriger Kanal",
"DOWN_CHANNEL": "Nächster Kanal",
"NOW_PLAYING": "Jetzt läuft",
"EXECUTING": "Wird ausgeführt",
"SOURCE_M3U": "M3U-Live-TV",
"SOURCE_XTREAM": "Xtream-Live-TV",
"SOURCE_STALKER": "Stalker-Live-TV",
"WAITING_FOR_PLAYBACK": "Warten auf Wiedergabe",
"UNKNOWN_SOURCE": "unbekannt",
"NO_STREAM": "Kein Live-Stream ausgewählt",
"NO_PROGRAM": "Keine aktuelle EPG-Sendung",
"CHANNEL_ABBR": "Sender",
"CHANNEL_NAVIGATION": "Senderwechsel",
"CHANNEL_NUMBER": "Sendernummer",
"KEYPAD_DELETE": "ENTF",
"KEYPAD_CLEAR": "LÖSCHEN",
"VOLUME": "Lautstärke",
"VOLUME_ABBR": "Lautst.",
"MUTE": "STUMM",
"UNMUTE": "TON AN",
"VOLUME_UNAVAILABLE": "Lautstärkeregelung in diesem Player-Modus nicht verfügbar",
"SYNCING_STATUS": "Status wird synchronisiert…",
"UPDATED_AT": "Aktualisiert um {{time}}",
"WAITING_FOR_STATUS": "Warten auf Status…",
"ERRORS": {
"CHANNEL_UP": "Senderwechsel nach oben fehlgeschlagen",
"CHANNEL_DOWN": "Senderwechsel nach unten fehlgeschlagen",
"SELECT_NUMBER": "Senderwechsel per Nummer fehlgeschlagen",
"VOLUME_UP": "Lautstärke konnte nicht erhöht werden",
"VOLUME_DOWN": "Lautstärke konnte nicht verringert werden",
"TOGGLE_MUTE": "Stummschaltung konnte nicht umgeschaltet werden",
"STATUS": "Status der Fernbedienung konnte nicht abgerufen werden"
},
"HEADER": "IPTVnator-Fernbedienung",
"FOOTER": "IPTVnator-Fernbedienung"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Läuft in {{days}} T. ab",
"SOURCE_EXPIRED": "Abgelaufen",
"HERO_LABEL": "Empfohlen",
"HERO_ROLE_CAROUSEL": "Karussell",
"HERO_ROLE_SLIDE": "Folie",
"HERO_FAVORITE_CHANNEL": "Lieblingssender",
"HERO_FAVORITE": "Favorit",
"HERO_RECENTLY_ADDED": "Kürzlich hinzugefügt",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Alle {{type}}-Favoriten aus allen Playlists werden entfernt. Diese Aktion kann nicht rückgängig gemacht werden.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker-Portal",
"WINDOW_MINIMIZE": "Minimieren",
"WINDOW_MAXIMIZE": "Maximieren",
"WINDOW_RESTORE": "Verkleinern",
"WINDOW_CLOSE": "Schließen",
"CHANNELS_COUNT": "{{count}} Sender",
"NO_SOURCES_AVAILABLE": "Keine Quellen verfügbar",
"ONE_SOURCE_AVAILABLE": "1 Quelle verfügbar",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Ακύρωση",
"CLEAR": "Εκκαθάριση",
"CLOSE": "Κλείσιμο",
"REMOVE": "Αφαίρεση",
"BACK": "Πίσω",
"SHOW_MORE": "Περισσότερα",
"SHOW_LESS": "Λιγότερα",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Δεν προστέθηκε λίστα αναπαραγωγής",
"SELECT_PLAYLIST": "Επιλογή λίστας αναπαραγωγής",
"UNTITLED_PLAYLIST": "Λίστα αναπαραγωγής χωρίς τίτλο",
"PLAYLIST_ACTIONS": "Ενέργειες λίστας αναπαραγωγής",
"ADD_FIRST": "Παρακαλώ προσθέστε την πρώτη σας λίστα",
"WELCOME_TITLE": "Καλώς ήλθατε στο IPTVnator",
"WELCOME_DESCRIPTION": "Ξεκινήστε την διασκέδαση προσθέτοντας την πρώτη σας λίστα αναπαραγωγής. Συνδεθείτε στην αγαπημένη σας υπηρεσία IPTV και απολαύστε ζωντανή τηλεόραση, ταινίες και σειρές.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker portal",
"CHANNELS": "Κανάλια",
"CHANNELS_COUNT": "Κανάλια: {{count}}",
"PORTAL_STATUS": "Κατάσταση πύλης: {{status}}",
"PORTAL_STATUS_ACTIVE": "ενεργή",
"PORTAL_STATUS_INACTIVE": "ανενεργή",
"PORTAL_STATUS_EXPIRED": "έληξε",
"PORTAL_STATUS_UNAVAILABLE": "μη διαθέσιμη",
"PORTAL_STATUS_CHECKING": "γίνεται έλεγχος",
"ADDED": "Προστέθηκε",
"SHOW_DETAILS": "Λεπτομέρειες λίστας αναπαραγωγής",
"REMOVE": "Διαγραφή λίστας αναπαραγωγής",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Η εξαγωγή της λίστας αναπαραγωγής ολοκληρώθηκε με επιτυχία.",
"PLAYLIST_EXPORT_ERROR": "Σφάλμα: Η εξαγωγή της λίστας αναπαραγωγής απέτυχε.",
"EXPORT_PLAYLIST_FAILED": "Η εξαγωγή της λίστας αναπαραγωγής απέτυχε.",
"EXPORT_FILE_TYPE": "Λίστα αναπαραγωγής",
"COPY_URL": "Αντιγραφή διεύθηνσης URL στο πρόχειρο",
"URL_COPIED": "Η διεύθυνση URL αντιγράφηκε στο πρόχειρο"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "επιλέξτε μια λίστα αναπαραγωγής",
"BROWSE": "Περιήγηση αρχείων",
"REJECTED": "Το αρχείο απορρίφθηκε, η μορφή αρχείου δεν υποστηρίζεται {{filename}}.",
"REJECTED_UNNAMED": "Το αρχείο απορρίφθηκε, η μορφή αρχείου δεν υποστηρίζεται.",
"OPEN_FAILED": "Δεν ήταν δυνατό το άνοιγμα του \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Μετατοπίστε τις εμφανιζόμενες ώρες προγραμμάτων χωρίς ανανέωση του οδηγού. Χρησιμοποιήστε λεπτά από -720 έως 720.",
"EPG_OFFSET_LABEL": "Μετατόπιση (λεπτά)",
"EPG_OFFSET_ERROR": "Εισαγάγετε έναν ακέραιο αριθμό λεπτών από -720 έως 720.",
"EPG_OFFSET_UNIT": "λεπ.",
"EPG_VIEW_MODE_DESCRIPTION": "Επιλέξτε πώς εμφανίζεται ο οδηγός προγράμματος κάτω από το πρόγραμμα αναπαραγωγής: ως οριζόντια λωρίδα χρονολογίου ή ως κατακόρυφη λίστα μίας ημέρας.",
"EPG_VIEW_MODE_TIMELINE": "Χρονολόγιο",
"EPG_VIEW_MODE_LIST": "Λίστα",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Τι νέο υπάρχει",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Δεν βρέθηκαν σημειώσεις έκδοσης για την {{version}} στο GitHub. Η έκδοση μπορεί να αφαιρέθηκε ή να μην έχει δημοσιευτεί ακόμη.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Δεν ήταν δυνατή η φόρτωση των σημειώσεων έκδοσης.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Οι σημειώσεις έκδοσης δεν είναι διαθέσιμες σε αυτή τη δομή.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Άνοιγμα σελίδας εκδόσεων",
"APP_UPDATE_CHANNEL": "Κανάλι ενημερώσεων",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Το Stable ακολουθεί τις δημοσιευμένες εκδόσεις. Το Nightly ακολουθεί κάθε συγχώνευση στο master και εγκαθιστά αυτόματα προκαταρκτικές εκδόσεις. Εφαρμόζεται κατά την αποθήκευση.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "Η ζωντανή ροή τερματίστηκε.",
"CHECKING_SUPPORT": "Έλεγχος υποστήριξης ενσωματωμένου MPV...",
"NOT_AVAILABLE": "Το ενσωματωμένο MPV δεν είναι διαθέσιμο σε αυτό το περιβάλλον.",
"FRAME_VIEW_FAILED": "Η αρχικοποίηση της προβολής βίντεο του ενσωματωμένου MPV απέτυχε.",
"LOADING_STREAM": "Φόρτωση ροής…",
"PLAY": "Αναπαραγωγή",
"PAUSE": "Παύση",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Εγγραφή σε εξέλιξη",
"RECORDINGS": "Εγγραφές",
"RECORDING_BADGE": "Εγγραφή",
"RECORDING_CHIP": "ΕΓΓΡΑΦΗ",
"STOP_RECORDING": "Διακοπή εγγραφής",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Αφαίρεση εγγραφής από τη διαχείριση;",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Ελέγξτε το πρόγραμμα αναπαραγωγής IPTV σας από απόσταση",
"UP_CHANNEL": "Προηγούμενο κανάλι",
"DOWN_CHANNEL": "Επόμενο κανάλι",
"NOW_PLAYING": "Τώρα παίζει",
"EXECUTING": "Εκτέλεση",
"SOURCE_M3U": "Ζωντανά M3U",
"SOURCE_XTREAM": "Ζωντανά Xtream",
"SOURCE_STALKER": "Ζωντανά Stalker",
"WAITING_FOR_PLAYBACK": "Αναμονή για αναπαραγωγή",
"UNKNOWN_SOURCE": "άγνωστη",
"NO_STREAM": "Δεν έχει επιλεγεί ζωντανή ροή",
"NO_PROGRAM": "Δεν υπάρχει τρέχον πρόγραμμα EPG",
"CHANNEL_ABBR": "ΚΑΝ",
"CHANNEL_NAVIGATION": "Πλοήγηση καναλιών",
"CHANNEL_NUMBER": "Αριθμός καναλιού",
"KEYPAD_DELETE": "ΔΙΑΓ",
"KEYPAD_CLEAR": "ΚΑΘΑΡ",
"VOLUME": "Ένταση",
"VOLUME_ABBR": "ΕΝΤ",
"MUTE": "ΣΙΓΑΣΗ",
"UNMUTE": "ΑΡΣΗ ΣΙΓΑΣΗΣ",
"VOLUME_UNAVAILABLE": "Ο έλεγχος έντασης δεν είναι διαθέσιμος σε αυτή τη λειτουργία προγράμματος αναπαραγωγής",
"SYNCING_STATUS": "Συγχρονισμός κατάστασης…",
"UPDATED_AT": "Ενημερώθηκε {{time}}",
"WAITING_FOR_STATUS": "Αναμονή κατάστασης…",
"ERRORS": {
"CHANNEL_UP": "Αποτυχία αλλαγής καναλιού προς τα πάνω",
"CHANNEL_DOWN": "Αποτυχία αλλαγής καναλιού προς τα κάτω",
"SELECT_NUMBER": "Αποτυχία μετάβασης μέσω αριθμού καναλιού",
"VOLUME_UP": "Αποτυχία αύξησης έντασης",
"VOLUME_DOWN": "Αποτυχία μείωσης έντασης",
"TOGGLE_MUTE": "Αποτυχία εναλλαγής σίγασης",
"STATUS": "Αποτυχία λήψης κατάστασης τηλεχειρισμού"
},
"HEADER": "Τηλεχειριστήριο IPTVnator",
"FOOTER": "Τηλεχειριστήριο IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Λήγει σε {{days}} ημ.",
"SOURCE_EXPIRED": "Έληξε",
"HERO_LABEL": "Προτεινόμενα",
"HERO_ROLE_CAROUSEL": "καρουζέλ",
"HERO_ROLE_SLIDE": "διαφάνεια",
"HERO_FAVORITE_CHANNEL": "Αγαπημένο κανάλι",
"HERO_FAVORITE": "Αγαπημένο",
"HERO_RECENTLY_ADDED": "Προστέθηκε πρόσφατα",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Όλα τα αγαπημένα {{type}} σε όλες τις λίστες αναπαραγωγής θα αφαιρεθούν. Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker Portal",
"WINDOW_MINIMIZE": "Ελαχιστοποίηση",
"WINDOW_MAXIMIZE": "Μεγιστοποίηση",
"WINDOW_RESTORE": "Επαναφορά",
"WINDOW_CLOSE": "Κλείσιμο",
"CHANNELS_COUNT": "{{count}} κανάλια",
"NO_SOURCES_AVAILABLE": "Δεν υπάρχουν διαθέσιμες πηγές",
"ONE_SOURCE_AVAILABLE": "1 διαθέσιμη πηγή",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Cancel",
"CLEAR": "Clear",
"CLOSE": "Close",
"REMOVE": "Remove",
"BACK": "Back",
"SHOW_MORE": "More",
"SHOW_LESS": "Less",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "No playlists were added",
"SELECT_PLAYLIST": "Select playlist",
"UNTITLED_PLAYLIST": "Untitled playlist",
"PLAYLIST_ACTIONS": "Playlist actions",
"ADD_FIRST": "Please add your first playlist",
"WELCOME_TITLE": "Welcome to IPTVnator",
"WELCOME_DESCRIPTION": "Start your streaming journey by adding your first playlist. Connect to your favorite IPTV service and enjoy live TV, movies, and series.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker portal",
"CHANNELS": "Channels",
"CHANNELS_COUNT": "{{count}} channels",
"PORTAL_STATUS": "Portal status: {{status}}",
"PORTAL_STATUS_ACTIVE": "active",
"PORTAL_STATUS_INACTIVE": "inactive",
"PORTAL_STATUS_EXPIRED": "expired",
"PORTAL_STATUS_UNAVAILABLE": "unavailable",
"PORTAL_STATUS_CHECKING": "checking",
"ADDED": "Added",
"SHOW_DETAILS": "Show playlist details",
"REMOVE": "Remove playlist",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Playlist was exported successfully.",
"PLAYLIST_EXPORT_ERROR": "Error: Playlist export failed.",
"EXPORT_PLAYLIST_FAILED": "Playlist export failed.",
"EXPORT_FILE_TYPE": "Playlist",
"COPY_URL": "Copy URL to clipboard",
"URL_COPIED": "URL copied to clipboard"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "click to browse",
"BROWSE": "Browse files",
"REJECTED": "File was rejected, unsupported file format {{filename}}.",
"REJECTED_UNNAMED": "File was rejected, unsupported file format.",
"OPEN_FAILED": "Could not open \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Shift displayed programme times without refreshing the guide. Use minutes, from -720 to 720.",
"EPG_OFFSET_LABEL": "Offset (minutes)",
"EPG_OFFSET_ERROR": "Enter a whole number of minutes between -720 and 720.",
"EPG_OFFSET_UNIT": "min",
"EPG_VIEW_MODE_DESCRIPTION": "Choose how the programme guide under the player is shown: a horizontal timeline ribbon or a vertical, single-day list.",
"EPG_VIEW_MODE_TIMELINE": "Timeline",
"EPG_VIEW_MODE_LIST": "List",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "What's new",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "No release notes were found for {{version}} on GitHub. The release may have been removed or is not published yet.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Release notes could not be loaded.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Release notes are not available in this build.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Open releases page",
"APP_UPDATE_CHANNEL": "Update channel",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable follows the tagged releases. Nightly follows every merge into master and installs prerelease builds automatically. Applies on Save.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "The live stream ended.",
"CHECKING_SUPPORT": "Checking embedded MPV support...",
"NOT_AVAILABLE": "Embedded MPV is not available in this environment.",
"FRAME_VIEW_FAILED": "The embedded MPV video view failed to initialize.",
"LOADING_STREAM": "Loading stream…",
"PLAY": "Play",
"PAUSE": "Pause",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Recording now",
"RECORDINGS": "Recordings",
"RECORDING_BADGE": "Recording",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Stop recording",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Remove recording from manager?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Control your IPTV player remotely",
"UP_CHANNEL": "Channel Up",
"DOWN_CHANNEL": "Channel Down",
"NOW_PLAYING": "Now playing",
"EXECUTING": "Executing",
"SOURCE_M3U": "M3U live",
"SOURCE_XTREAM": "Xtream live",
"SOURCE_STALKER": "Stalker live",
"WAITING_FOR_PLAYBACK": "Waiting for playback",
"UNKNOWN_SOURCE": "unknown",
"NO_STREAM": "No live stream selected",
"NO_PROGRAM": "No current EPG program",
"CHANNEL_ABBR": "CH",
"CHANNEL_NAVIGATION": "Channel navigation",
"CHANNEL_NUMBER": "Channel number",
"KEYPAD_DELETE": "DEL",
"KEYPAD_CLEAR": "CLR",
"VOLUME": "Volume",
"VOLUME_ABBR": "VOL",
"MUTE": "MUTE",
"UNMUTE": "UNMUTE",
"VOLUME_UNAVAILABLE": "Volume control unavailable for this player mode",
"SYNCING_STATUS": "Syncing status…",
"UPDATED_AT": "Updated {{time}}",
"WAITING_FOR_STATUS": "Waiting for status…",
"ERRORS": {
"CHANNEL_UP": "Failed to change channel up",
"CHANNEL_DOWN": "Failed to change channel down",
"SELECT_NUMBER": "Failed to switch by channel number",
"VOLUME_UP": "Failed to increase volume",
"VOLUME_DOWN": "Failed to decrease volume",
"TOGGLE_MUTE": "Failed to toggle mute",
"STATUS": "Failed to fetch remote status"
},
"HEADER": "IPTVNator Remote Control",
"FOOTER": "IPTVNator Remote Control"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Expires in {{days}} d",
"SOURCE_EXPIRED": "Expired",
"HERO_LABEL": "Featured",
"HERO_ROLE_CAROUSEL": "carousel",
"HERO_ROLE_SLIDE": "slide",
"HERO_FAVORITE_CHANNEL": "Favorite channel",
"HERO_FAVORITE": "Favorite",
"HERO_RECENTLY_ADDED": "Recently added",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "All {{type}} favorites across all playlists will be removed. This action cannot be undone.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker Portal",
"WINDOW_MINIMIZE": "Minimize",
"WINDOW_MAXIMIZE": "Maximize",
"WINDOW_RESTORE": "Restore",
"WINDOW_CLOSE": "Close",
"CHANNELS_COUNT": "{{count}} channels",
"NO_SOURCES_AVAILABLE": "No sources available",
"ONE_SOURCE_AVAILABLE": "1 source available",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Cancelar",
"CLEAR": "Borrar",
"CLOSE": "Cerrar",
"REMOVE": "Quitar",
"BACK": "Atrás",
"SHOW_MORE": "Más",
"SHOW_LESS": "Menos",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "No se añadieron listas de reproducción",
"SELECT_PLAYLIST": "Seleccionar lista de reproducción",
"UNTITLED_PLAYLIST": "Lista sin título",
"PLAYLIST_ACTIONS": "Acciones de la lista",
"ADD_FIRST": "Por favor, añade tu primera lista de la otra pestaña",
"WELCOME_TITLE": "Bienvenido a IPTVnator",
"WELCOME_DESCRIPTION": "Comience su viaje de streaming agregando su primera lista de reproducción. Conéctese a su servicio IPTV favorito y disfrute de TV en vivo, películas y series.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker Portal",
"CHANNELS": "Canales",
"CHANNELS_COUNT": "Canales: {{count}}",
"PORTAL_STATUS": "Estado del portal: {{status}}",
"PORTAL_STATUS_ACTIVE": "activo",
"PORTAL_STATUS_INACTIVE": "inactivo",
"PORTAL_STATUS_EXPIRED": "caducado",
"PORTAL_STATUS_UNAVAILABLE": "no disponible",
"PORTAL_STATUS_CHECKING": "comprobando",
"ADDED": "Añadido",
"SHOW_DETAILS": "Mostrar detalles de la lista",
"REMOVE": "Quitar lista",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "La lista de reproducción se exportó correctamente.",
"PLAYLIST_EXPORT_ERROR": "Error: no se pudo exportar la lista de reproducción.",
"EXPORT_PLAYLIST_FAILED": "No se pudo exportar la lista de reproducción.",
"EXPORT_FILE_TYPE": "Lista de reproducción",
"COPY_URL": "Copiar URL al portapapeles",
"URL_COPIED": "URL copiada al portapapeles"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "Escoga una lista de reproducción",
"BROWSE": "Explorar archivos",
"REJECTED": "El archivo fue rechazado, formato de archivo no compatible {{filename}}.",
"REJECTED_UNNAMED": "El archivo fue rechazado, formato de archivo no compatible.",
"OPEN_FAILED": "No se pudo abrir \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Desplaza las horas mostradas de los programas sin actualizar la guía. Usa minutos, de -720 a 720.",
"EPG_OFFSET_LABEL": "Desfase (minutos)",
"EPG_OFFSET_ERROR": "Introduce un número entero de minutos entre -720 y 720.",
"EPG_OFFSET_UNIT": "min",
"EPG_VIEW_MODE_DESCRIPTION": "Elige cómo se muestra la guía de programas bajo el reproductor: una cinta de cronología horizontal o una lista vertical de un solo día.",
"EPG_VIEW_MODE_TIMELINE": "Cronología",
"EPG_VIEW_MODE_LIST": "Lista",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Novedades",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "No se encontraron notas de la versión {{version}} en GitHub. Es posible que la versión se haya eliminado o aún no esté publicada.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "No se pudieron cargar las notas de la versión.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Las notas de la versión no están disponibles en esta compilación.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Abrir página de versiones",
"APP_UPDATE_CHANNEL": "Canal de actualizaciones",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable sigue las versiones publicadas. Nightly sigue cada fusión en master e instala compilaciones preliminares automáticamente. Se aplica al guardar.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "La transmisión en vivo terminó.",
"CHECKING_SUPPORT": "Comprobando compatibilidad con MPV integrado...",
"NOT_AVAILABLE": "MPV integrado no está disponible en este entorno.",
"FRAME_VIEW_FAILED": "No se pudo inicializar la vista de vídeo de MPV integrado.",
"LOADING_STREAM": "Cargando stream…",
"PLAY": "Reproducir",
"PAUSE": "Pausar",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Grabando ahora",
"RECORDINGS": "Grabaciones",
"RECORDING_BADGE": "Grabando",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Detener grabación",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "¿Quitar la grabación del gestor?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Controla tu reproductor IPTV de forma remota",
"UP_CHANNEL": "Canal anterior",
"DOWN_CHANNEL": "Canal siguiente",
"NOW_PLAYING": "Reproduciendo ahora",
"EXECUTING": "Ejecutando",
"SOURCE_M3U": "M3U en vivo",
"SOURCE_XTREAM": "Xtream en vivo",
"SOURCE_STALKER": "Stalker en vivo",
"WAITING_FOR_PLAYBACK": "Esperando la reproducción",
"UNKNOWN_SOURCE": "desconocido",
"NO_STREAM": "No hay ningún stream en vivo seleccionado",
"NO_PROGRAM": "No hay ningún programa EPG en curso",
"CHANNEL_ABBR": "CAN",
"CHANNEL_NAVIGATION": "Navegación de canales",
"CHANNEL_NUMBER": "Número de canal",
"KEYPAD_DELETE": "SUPR",
"KEYPAD_CLEAR": "BORRAR",
"VOLUME": "Volumen",
"VOLUME_ABBR": "VOL",
"MUTE": "SILENCIAR",
"UNMUTE": "ACTIVAR SONIDO",
"VOLUME_UNAVAILABLE": "Control de volumen no disponible en este modo de reproductor",
"SYNCING_STATUS": "Sincronizando estado…",
"UPDATED_AT": "Actualizado a las {{time}}",
"WAITING_FOR_STATUS": "Esperando el estado…",
"ERRORS": {
"CHANNEL_UP": "No se pudo subir de canal",
"CHANNEL_DOWN": "No se pudo bajar de canal",
"SELECT_NUMBER": "No se pudo cambiar de canal por número",
"VOLUME_UP": "No se pudo subir el volumen",
"VOLUME_DOWN": "No se pudo bajar el volumen",
"TOGGLE_MUTE": "No se pudo activar o desactivar el silencio",
"STATUS": "No se pudo obtener el estado remoto"
},
"HEADER": "Control remoto de IPTVnator",
"FOOTER": "Control remoto de IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Caduca en {{days}} d",
"SOURCE_EXPIRED": "Caducada",
"HERO_LABEL": "Destacado",
"HERO_ROLE_CAROUSEL": "carrusel",
"HERO_ROLE_SLIDE": "diapositiva",
"HERO_FAVORITE_CHANNEL": "Canal favorito",
"HERO_FAVORITE": "Favorito",
"HERO_RECENTLY_ADDED": "Añadido recientemente",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Todos los favoritos {{type}} de todas las listas se eliminarán. Esta acción no se puede deshacer.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker Portal",
"WINDOW_MINIMIZE": "Minimizar",
"WINDOW_MAXIMIZE": "Maximizar",
"WINDOW_RESTORE": "Restaurar",
"WINDOW_CLOSE": "Cerrar",
"CHANNELS_COUNT": "{{count}} canales",
"NO_SOURCES_AVAILABLE": "No hay fuentes disponibles",
"ONE_SOURCE_AVAILABLE": "1 fuente disponible",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Annuler",
"CLEAR": "Effacer",
"CLOSE": "Fermer",
"REMOVE": "Retirer",
"BACK": "Retour",
"SHOW_MORE": "Plus",
"SHOW_LESS": "Moins",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Aucune liste de lecture n'a été ajoutée",
"SELECT_PLAYLIST": "Sélectionner une liste de lecture",
"UNTITLED_PLAYLIST": "Liste de lecture sans titre",
"PLAYLIST_ACTIONS": "Actions de la liste de lecture",
"ADD_FIRST": "Veuillez ajouter votre première liste de lecture depuis un autre onglet",
"WELCOME_TITLE": "Bienvenue sur IPTVnator",
"WELCOME_DESCRIPTION": "Commencez votre voyage de streaming en ajoutant votre première liste de lecture. Connectez-vous à votre service IPTV préféré et profitez de la télévision en direct, des films et des séries.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Portail Stalker",
"CHANNELS": "Chaînes",
"CHANNELS_COUNT": "{{count}} chaînes",
"PORTAL_STATUS": "État du portail : {{status}}",
"PORTAL_STATUS_ACTIVE": "actif",
"PORTAL_STATUS_INACTIVE": "inactif",
"PORTAL_STATUS_EXPIRED": "expiré",
"PORTAL_STATUS_UNAVAILABLE": "indisponible",
"PORTAL_STATUS_CHECKING": "vérification en cours",
"ADDED": "Ajouté",
"SHOW_DETAILS": "Afficher les détails de la liste de lecture",
"REMOVE": "Retirer la liste de lecture",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "La liste de lecture a été exportée avec succès.",
"PLAYLIST_EXPORT_ERROR": "Erreur : Échec de l'exportation de la liste de lecture.",
"EXPORT_PLAYLIST_FAILED": "Échec de l'exportation de la liste de lecture.",
"EXPORT_FILE_TYPE": "Liste de lecture",
"COPY_URL": "Copier l'URL dans le presse-papiers",
"URL_COPIED": "URL copiée dans le presse-papiers"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "Choisir une liste de lecture",
"BROWSE": "Parcourir les fichiers",
"REJECTED": "Le fichier a été rejeté, format de fichier non pris en charge {{filename}}.",
"REJECTED_UNNAMED": "Le fichier a été rejeté, format de fichier non pris en charge.",
"OPEN_FAILED": "Impossible d’ouvrir \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Décalez les horaires affichés des programmes sans actualiser le guide. Utilisez des minutes de -720 à 720.",
"EPG_OFFSET_LABEL": "Décalage (minutes)",
"EPG_OFFSET_ERROR": "Saisissez un nombre entier de minutes compris entre -720 et 720.",
"EPG_OFFSET_UNIT": "min",
"EPG_VIEW_MODE_DESCRIPTION": "Choisissez comment le guide des programmes sous le lecteur est affiché : une frise chronologique horizontale ou une liste verticale d'une seule journée.",
"EPG_VIEW_MODE_TIMELINE": "Chronologie",
"EPG_VIEW_MODE_LIST": "Liste",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Nouveautés",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Aucune note de version n'a été trouvée pour {{version}} sur GitHub. La version a peut-être été supprimée ou n'est pas encore publiée.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Impossible de charger les notes de version.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Les notes de version ne sont pas disponibles dans cette compilation.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Ouvrir la page des versions",
"APP_UPDATE_CHANNEL": "Canal de mise à jour",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable suit les versions publiées. Nightly suit chaque fusion dans master et installe automatiquement les préversions. Appliqué à l’enregistrement.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "La diffusion en direct est terminée.",
"CHECKING_SUPPORT": "Vérification de la prise en charge de MPV intégré...",
"NOT_AVAILABLE": "MPV intégré n'est pas disponible dans cet environnement.",
"FRAME_VIEW_FAILED": "La vue vidéo de MPV intégré n'a pas pu être initialisée.",
"LOADING_STREAM": "Chargement du flux…",
"PLAY": "Lecture",
"PAUSE": "Pause",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Enregistrement en cours",
"RECORDINGS": "Enregistrements",
"RECORDING_BADGE": "Enregistrement",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Arrêter l'enregistrement",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Retirer l'enregistrement du gestionnaire ?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Contrôlez votre lecteur IPTV à distance",
"UP_CHANNEL": "Chaîne précédente",
"DOWN_CHANNEL": "Chaîne suivante",
"NOW_PLAYING": "En cours de lecture",
"EXECUTING": "Exécution en cours",
"SOURCE_M3U": "M3U en direct",
"SOURCE_XTREAM": "Xtream en direct",
"SOURCE_STALKER": "Stalker en direct",
"WAITING_FOR_PLAYBACK": "En attente de lecture",
"UNKNOWN_SOURCE": "source inconnue",
"NO_STREAM": "Aucun flux en direct sélectionné",
"NO_PROGRAM": "Aucun programme EPG en cours",
"CHANNEL_ABBR": "CH",
"CHANNEL_NAVIGATION": "Navigation entre les chaînes",
"CHANNEL_NUMBER": "Numéro de chaîne",
"KEYPAD_DELETE": "SUPPR",
"KEYPAD_CLEAR": "EFF",
"VOLUME": "Volume",
"VOLUME_ABBR": "VOL",
"MUTE": "MUET",
"UNMUTE": "ACTIVER LE SON",
"VOLUME_UNAVAILABLE": "Réglage du volume indisponible pour ce mode de lecteur",
"SYNCING_STATUS": "Synchronisation de l'état…",
"UPDATED_AT": "Mis à jour à {{time}}",
"WAITING_FOR_STATUS": "En attente de l'état…",
"ERRORS": {
"CHANNEL_UP": "Impossible de passer à la chaîne précédente",
"CHANNEL_DOWN": "Impossible de passer à la chaîne suivante",
"SELECT_NUMBER": "Impossible de changer de chaîne par numéro",
"VOLUME_UP": "Impossible d'augmenter le volume",
"VOLUME_DOWN": "Impossible de baisser le volume",
"TOGGLE_MUTE": "Impossible de couper ou d'activer le son",
"STATUS": "Impossible de récupérer l'état distant"
},
"HEADER": "Télécommande IPTVnator",
"FOOTER": "Télécommande IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Expire dans {{days}} j",
"SOURCE_EXPIRED": "Expiré",
"HERO_LABEL": "À la une",
"HERO_ROLE_CAROUSEL": "diaporama",
"HERO_ROLE_SLIDE": "diapositive",
"HERO_FAVORITE_CHANNEL": "Chaîne favorite",
"HERO_FAVORITE": "Favori",
"HERO_RECENTLY_ADDED": "Ajouté récemment",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Tous les favoris {{type}} de toutes les listes de lecture seront retirés. Cette action est irréversible.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Portail Stalker",
"WINDOW_MINIMIZE": "Réduire",
"WINDOW_MAXIMIZE": "Agrandir",
"WINDOW_RESTORE": "Restaurer",
"WINDOW_CLOSE": "Fermer",
"CHANNELS_COUNT": "{{count}} chaînes",
"NO_SOURCES_AVAILABLE": "Aucune source disponible",
"ONE_SOURCE_AVAILABLE": "1 source disponible",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Mégse",
"CLEAR": "Törlés",
"CLOSE": "Bezárás",
"REMOVE": "Eltávolítás",
"BACK": "Vissza",
"SHOW_MORE": "Több mutatása",
"SHOW_LESS": "Kevesebb mutatása",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Még nincs hozzáadott lejátszási lista",
"SELECT_PLAYLIST": "Lejátszási lista kiválasztása",
"UNTITLED_PLAYLIST": "Névtelen lejátszási lista",
"PLAYLIST_ACTIONS": "Lejátszási lista műveletei",
"ADD_FIRST": "Adja hozzá az első lejátszási listát",
"WELCOME_TITLE": "Üdvözli Önt az IPTVnator!",
"WELCOME_DESCRIPTION": "Első lépésként adja hozzá lejátszási listáját. Kapcsolódjon kedvenc IPTV-szolgáltatásához, és élvezze az élő tévéadásokat, filmeket és sorozatokat.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker-portál",
"CHANNELS": "Csatornák",
"CHANNELS_COUNT": "{{count}} csatorna",
"PORTAL_STATUS": "Portál állapota: {{status}}",
"PORTAL_STATUS_ACTIVE": "aktív",
"PORTAL_STATUS_INACTIVE": "inaktív",
"PORTAL_STATUS_EXPIRED": "lejárt",
"PORTAL_STATUS_UNAVAILABLE": "nem érhető el",
"PORTAL_STATUS_CHECKING": "ellenőrzés alatt",
"ADDED": "Hozzáadva",
"SHOW_DETAILS": "Lejátszási lista részleteinek megjelenítése",
"REMOVE": "Lejátszási lista eltávolítása",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "A lejátszási lista exportálása sikerült.",
"PLAYLIST_EXPORT_ERROR": "Hiba: nem sikerült exportálni a lejátszási listát.",
"EXPORT_PLAYLIST_FAILED": "Nem sikerült exportálni a lejátszási listát.",
"EXPORT_FILE_TYPE": "Lejátszási lista",
"COPY_URL": "URL-cím másolása a vágólapra",
"URL_COPIED": "Az URL-cím a vágólapra került"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "kattintson a tallózáshoz",
"BROWSE": "Fájlok tallózása",
"REJECTED": "Elutasított fájl: {{filename}} – nem támogatott fájlformátum.",
"REJECTED_UNNAMED": "Elutasított fájl – nem támogatott fájlformátum.",
"OPEN_FAILED": "A(z) \"{{filename}}\" nem nyitható meg."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "A műsorok megjelenített idejének eltolása a műsorújság frissítése nélkül. Percben adja meg, -720 és 720 között.",
"EPG_OFFSET_LABEL": "Eltolás (perc)",
"EPG_OFFSET_ERROR": "Adjon meg egy egész percértéket -720 és 720 között.",
"EPG_OFFSET_UNIT": "perc",
"EPG_VIEW_MODE_DESCRIPTION": "Válassza ki, hogyan jelenjen meg a lejátszó alatti műsorújság: vízszintes idősávként vagy függőleges, egynapos listaként.",
"EPG_VIEW_MODE_TIMELINE": "Idővonal",
"EPG_VIEW_MODE_LIST": "Lista",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Újdonságok",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "A(z) {{version}} verzióhoz nem található kiadási jegyzet a GitHubon. A kiadást talán eltávolították, vagy még nem tették közzé.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "A kiadási jegyzetek betöltése nem sikerült.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Ebben a buildben nem érhetők el a kiadási jegyzetek.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Kiadások oldalának megnyitása",
"APP_UPDATE_CHANNEL": "Frissítési csatorna",
"APP_UPDATE_CHANNEL_DESCRIPTION": "A Stable a kiadott verziókat követi. A Nightly a master minden beolvasztását követi, és automatikusan telepíti az előzetes buildeket. Mentéskor lép érvénybe.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "Az élő adás véget ért.",
"CHECKING_SUPPORT": "A beágyazott MPV támogatásának ellenőrzése…",
"NOT_AVAILABLE": "A beágyazott MPV ebben a környezetben nem érhető el.",
"FRAME_VIEW_FAILED": "Nem sikerült inicializálni a beágyazott MPV videónézetét.",
"LOADING_STREAM": "Adatfolyam betöltése…",
"PLAY": "Lejátszás",
"PAUSE": "Szünet",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Folyamatban lévő felvételek",
"RECORDINGS": "Felvételek",
"RECORDING_BADGE": "Felvétel",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Felvétel leállítása",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Eltávolítja a felvételt a kezelőből?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Az IPTV-lejátszó távvezérlése",
"UP_CHANNEL": "Előző csatorna",
"DOWN_CHANNEL": "Következő csatorna",
"NOW_PLAYING": "Lejátszás alatt",
"EXECUTING": "Végrehajtás folyamatban",
"SOURCE_M3U": "M3U élő adás",
"SOURCE_XTREAM": "Xtream élő adás",
"SOURCE_STALKER": "Stalker élő adás",
"WAITING_FOR_PLAYBACK": "Várakozás a lejátszásra",
"UNKNOWN_SOURCE": "ismeretlen",
"NO_STREAM": "Nincs kiválasztott élő adás",
"NO_PROGRAM": "Nincs aktuális EPG-műsor",
"CHANNEL_ABBR": "CSAT",
"CHANNEL_NAVIGATION": "Csatornaváltás",
"CHANNEL_NUMBER": "Csatornaszám",
"KEYPAD_DELETE": "TÖRÖL",
"KEYPAD_CLEAR": "ÜRÍT",
"VOLUME": "Hangerő",
"VOLUME_ABBR": "HANG",
"MUTE": "NÉMÍTÁS",
"UNMUTE": "HANG BE",
"VOLUME_UNAVAILABLE": "Ebben a lejátszómódban a hangerő nem szabályozható",
"SYNCING_STATUS": "Állapot szinkronizálása…",
"UPDATED_AT": "Frissítve: {{time}}",
"WAITING_FOR_STATUS": "Várakozás az állapotadatokra…",
"ERRORS": {
"CHANNEL_UP": "Nem sikerült az előző csatornára váltani",
"CHANNEL_DOWN": "Nem sikerült a következő csatornára váltani",
"SELECT_NUMBER": "Nem sikerült csatornaszám alapján váltani",
"VOLUME_UP": "Nem sikerült növelni a hangerőt",
"VOLUME_DOWN": "Nem sikerült csökkenteni a hangerőt",
"TOGGLE_MUTE": "Nem sikerült be- vagy kikapcsolni a némítást",
"STATUS": "Nem sikerült lekérni a távvezérlés állapotát"
},
"HEADER": "IPTVnator távvezérlő",
"FOOTER": "IPTVnator távvezérlő"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "{{days}} nap múlva lejár",
"SOURCE_EXPIRED": "Lejárt",
"HERO_LABEL": "Kiemelt",
"HERO_ROLE_CAROUSEL": "diavetítés",
"HERO_ROLE_SLIDE": "dia",
"HERO_FAVORITE_CHANNEL": "Kedvenc csatorna",
"HERO_FAVORITE": "Kedvenc",
"HERO_RECENTLY_ADDED": "Nemrég hozzáadva",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Az összes lejátszási lista következő tartalomtípusba tartozó valamennyi kedvence törlődik: {{type}}. A művelet nem vonható vissza.",
"XTREAM_CODE": "Xtream Codes",
"STALKER_PORTAL": "Stalker-portál",
"WINDOW_MINIMIZE": "Kis méret",
"WINDOW_MAXIMIZE": "Teljes méret",
"WINDOW_RESTORE": "Előző méret",
"WINDOW_CLOSE": "Bezárás",
"CHANNELS_COUNT": "{{count}} csatorna",
"NO_SOURCES_AVAILABLE": "Nincs elérhető forrás",
"ONE_SOURCE_AVAILABLE": "1 forrás érhető el",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Annulla",
"CLEAR": "Cancella",
"CLOSE": "Chiudi",
"REMOVE": "Rimuovi",
"BACK": "Indietro",
"SHOW_MORE": "Altro",
"SHOW_LESS": "Meno",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Nessuna playlist è stata aggiunta",
"SELECT_PLAYLIST": "Seleziona playlist",
"UNTITLED_PLAYLIST": "Playlist senza titolo",
"PLAYLIST_ACTIONS": "Azioni playlist",
"ADD_FIRST": "Aggiungi la tua prima playlist dall'altra scheda",
"WELCOME_TITLE": "Benvenuto in IPTVnator",
"WELCOME_DESCRIPTION": "Inizia il tuo viaggio nello streaming aggiungendo la tua prima playlist. Connettiti al tuo servizio IPTV preferito e goditi TV in diretta, film e serie.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Portale Stalker",
"CHANNELS": "Canali",
"CHANNELS_COUNT": "Canali: {{count}}",
"PORTAL_STATUS": "Stato del portale: {{status}}",
"PORTAL_STATUS_ACTIVE": "attivo",
"PORTAL_STATUS_INACTIVE": "inattivo",
"PORTAL_STATUS_EXPIRED": "scaduto",
"PORTAL_STATUS_UNAVAILABLE": "non disponibile",
"PORTAL_STATUS_CHECKING": "verifica in corso",
"ADDED": "Aggiunto",
"SHOW_DETAILS": "Visualizza i dettagli della playlist",
"REMOVE": "Rimuovi playlist",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Playlist esportata correttamente.",
"PLAYLIST_EXPORT_ERROR": "Errore: esportazione della playlist fallita.",
"EXPORT_PLAYLIST_FAILED": "Esportazione della playlist non riuscita.",
"EXPORT_FILE_TYPE": "File di playlist",
"COPY_URL": "Copia URL negli appunti",
"URL_COPIED": "URL copiato negli appunti"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "scegli una playlist",
"BROWSE": "Sfoglia file",
"REJECTED": "Il file è stato rifiutato, formato di file non supportato {{filename}}.",
"REJECTED_UNNAMED": "Il file è stato rifiutato, formato di file non supportato.",
"OPEN_FAILED": "Impossibile aprire \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Sposta gli orari dei programmi visualizzati senza aggiornare la guida. Usa minuti da -720 a 720.",
"EPG_OFFSET_LABEL": "Scostamento (minuti)",
"EPG_OFFSET_ERROR": "Inserisci un numero intero di minuti compreso tra -720 e 720.",
"EPG_OFFSET_UNIT": "min",
"EPG_VIEW_MODE_DESCRIPTION": "Scegli come mostrare la guida programmi sotto il lettore: una fascia con timeline orizzontale o un elenco verticale di un singolo giorno.",
"EPG_VIEW_MODE_TIMELINE": "Timeline",
"EPG_VIEW_MODE_LIST": "Elenco",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Novità",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Nessuna nota di rilascio trovata per {{version}} su GitHub. La versione potrebbe essere stata rimossa o non è ancora pubblicata.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Impossibile caricare le note di rilascio.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Le note di rilascio non sono disponibili in questa build.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Apri la pagina delle versioni",
"APP_UPDATE_CHANNEL": "Canale di aggiornamento",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable segue le versioni pubblicate. Nightly segue ogni merge in master e installa automaticamente le build preliminari. Si applica al salvataggio.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "Lo stream in diretta è terminato.",
"CHECKING_SUPPORT": "Verifica del supporto per MPV integrato...",
"NOT_AVAILABLE": "MPV integrato non è disponibile in questo ambiente.",
"FRAME_VIEW_FAILED": "Impossibile inizializzare la vista video di MPV integrato.",
"LOADING_STREAM": "Caricamento stream…",
"PLAY": "Riproduci",
"PAUSE": "Pausa",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Registrazione in corso",
"RECORDINGS": "Registrazioni",
"RECORDING_BADGE": "In registrazione",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Interrompi registrazione",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Rimuovere la registrazione dal gestore?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Controlla il tuo lettore IPTV da remoto",
"UP_CHANNEL": "Canale precedente",
"DOWN_CHANNEL": "Canale successivo",
"NOW_PLAYING": "In riproduzione",
"EXECUTING": "Esecuzione in corso",
"SOURCE_M3U": "Diretta M3U",
"SOURCE_XTREAM": "Diretta Xtream",
"SOURCE_STALKER": "Diretta Stalker",
"WAITING_FOR_PLAYBACK": "In attesa della riproduzione",
"UNKNOWN_SOURCE": "sconosciuta",
"NO_STREAM": "Nessuno stream in diretta selezionato",
"NO_PROGRAM": "Nessun programma EPG in corso",
"CHANNEL_ABBR": "CAN",
"CHANNEL_NAVIGATION": "Navigazione canali",
"CHANNEL_NUMBER": "Numero canale",
"KEYPAD_DELETE": "CANC",
"KEYPAD_CLEAR": "AZZERA",
"VOLUME": "Volume",
"VOLUME_ABBR": "VOL",
"MUTE": "DISATTIVA AUDIO",
"UNMUTE": "ATTIVA AUDIO",
"VOLUME_UNAVAILABLE": "Controllo del volume non disponibile in questa modalità del lettore",
"SYNCING_STATUS": "Sincronizzazione dello stato…",
"UPDATED_AT": "Aggiornato alle {{time}}",
"WAITING_FOR_STATUS": "In attesa dello stato…",
"ERRORS": {
"CHANNEL_UP": "Impossibile passare al canale precedente",
"CHANNEL_DOWN": "Impossibile passare al canale successivo",
"SELECT_NUMBER": "Impossibile cambiare canale tramite il numero",
"VOLUME_UP": "Impossibile aumentare il volume",
"VOLUME_DOWN": "Impossibile diminuire il volume",
"TOGGLE_MUTE": "Impossibile attivare/disattivare l'audio",
"STATUS": "Impossibile recuperare lo stato remoto"
},
"HEADER": "Telecomando IPTVnator",
"FOOTER": "Telecomando IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Scade tra {{days}} g",
"SOURCE_EXPIRED": "Scaduto",
"HERO_LABEL": "In evidenza",
"HERO_ROLE_CAROUSEL": "carosello",
"HERO_ROLE_SLIDE": "diapositiva",
"HERO_FAVORITE_CHANNEL": "Canale preferito",
"HERO_FAVORITE": "Preferito",
"HERO_RECENTLY_ADDED": "Aggiunto di recente",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Tutti i preferiti {{type}} di tutte le playlist verranno rimossi. L'azione non può essere annullata.",
"XTREAM_CODE": "Codice Xtream",
"STALKER_PORTAL": "Portale Stalker",
"WINDOW_MINIMIZE": "Riduci a icona",
"WINDOW_MAXIMIZE": "Ingrandisci",
"WINDOW_RESTORE": "Ripristina",
"WINDOW_CLOSE": "Chiudi",
"CHANNELS_COUNT": "{{count}} canali",
"NO_SOURCES_AVAILABLE": "Nessuna sorgente disponibile",
"ONE_SOURCE_AVAILABLE": "1 sorgente disponibile",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "キャンセル",
"CLEAR": "クリア",
"CLOSE": "閉じる",
"REMOVE": "削除",
"BACK": "戻る",
"SHOW_MORE": "もっと見る",
"SHOW_LESS": "折りたたむ",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "プレイリストが追加されていません",
"SELECT_PLAYLIST": "プレイリストを選択",
"UNTITLED_PLAYLIST": "無題のプレイリスト",
"PLAYLIST_ACTIONS": "プレイリストの操作",
"ADD_FIRST": "最初のプレイリストを追加してください",
"WELCOME_TITLE": "IPTVnatorへようこそ",
"WELCOME_DESCRIPTION": "最初のプレイリストを追加してストリーミングを始めましょう。お好みのIPTVサービスに接続して、ライブTV、映画、シリーズをお楽しみください。",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalkerポータル",
"CHANNELS": "チャンネル",
"CHANNELS_COUNT": "{{count}}チャンネル",
"PORTAL_STATUS": "ポータルのステータス:{{status}}",
"PORTAL_STATUS_ACTIVE": "有効",
"PORTAL_STATUS_INACTIVE": "無効",
"PORTAL_STATUS_EXPIRED": "期限切れ",
"PORTAL_STATUS_UNAVAILABLE": "利用不可",
"PORTAL_STATUS_CHECKING": "確認中",
"ADDED": "追加済み",
"SHOW_DETAILS": "プレイリストの詳細を表示",
"REMOVE": "プレイリストを削除",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "プレイリストを正常にエクスポートしました。",
"PLAYLIST_EXPORT_ERROR": "エラー:プレイリストのエクスポートに失敗しました。",
"EXPORT_PLAYLIST_FAILED": "プレイリストのエクスポートに失敗しました。",
"EXPORT_FILE_TYPE": "プレイリスト",
"COPY_URL": "URLをクリップボードにコピー",
"URL_COPIED": "URLをクリップボードにコピーしました"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "プレイリストを選択",
"BROWSE": "ファイルを参照",
"REJECTED": "ファイルが拒否されました。サポートされていないファイル形式です:{{filename}}",
"REJECTED_UNNAMED": "ファイルが拒否されました。サポートされていないファイル形式です。",
"OPEN_FAILED": "\"{{filename}}\" を開けませんでした。"
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "番組表を更新せずに表示時刻をずらします。-720 から 720 までの分単位で指定してください。",
"EPG_OFFSET_LABEL": "補正(分)",
"EPG_OFFSET_ERROR": "-720 から 720 までの整数(分)を入力してください。",
"EPG_OFFSET_UNIT": "分",
"EPG_VIEW_MODE_DESCRIPTION": "プレーヤー下部の番組表の表示方法を選択します。横方向のタイムライン形式か、縦方向の1日分のリスト形式かを選べます。",
"EPG_VIEW_MODE_TIMELINE": "タイムライン",
"EPG_VIEW_MODE_LIST": "リスト",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "更新内容",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "{{version}} のリリースノートが GitHub に見つかりません。リリースが削除されたか、まだ公開されていない可能性があります。",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "リリースノートを読み込めませんでした。",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "このビルドではリリースノートを利用できません。",
"APP_UPDATE_OPEN_RELEASES_PAGE": "リリースページを開く",
"APP_UPDATE_CHANNEL": "更新チャンネル",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable は公開されたリリースに追従します。Nightly は master への各マージに追従し、プレリリースビルドを自動的にインストールします。保存時に適用されます。",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "ライブストリームが終了しました。",
"CHECKING_SUPPORT": "組み込みMPVのサポートを確認中...",
"NOT_AVAILABLE": "この環境では組み込みMPVは利用できません。",
"FRAME_VIEW_FAILED": "組み込みMPVのビデオビューを初期化できませんでした。",
"LOADING_STREAM": "ストリームを読み込み中…",
"PLAY": "再生",
"PAUSE": "一時停止",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "現在録画中",
"RECORDINGS": "録画",
"RECORDING_BADGE": "録画",
"RECORDING_CHIP": "録画",
"STOP_RECORDING": "録画を停止",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "録画をマネージャーから削除しますか?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "IPTVプレーヤーをリモートで操作",
"UP_CHANNEL": "前のチャンネル",
"DOWN_CHANNEL": "次のチャンネル",
"NOW_PLAYING": "再生中",
"EXECUTING": "実行中",
"SOURCE_M3U": "M3U ライブ",
"SOURCE_XTREAM": "Xtream ライブ",
"SOURCE_STALKER": "Stalker ライブ",
"WAITING_FOR_PLAYBACK": "再生を待機中",
"UNKNOWN_SOURCE": "不明",
"NO_STREAM": "ライブストリームが選択されていません",
"NO_PROGRAM": "現在の EPG 番組はありません",
"CHANNEL_ABBR": "チャンネル",
"CHANNEL_NAVIGATION": "チャンネル切り替え",
"CHANNEL_NUMBER": "チャンネル番号",
"KEYPAD_DELETE": "削除",
"KEYPAD_CLEAR": "クリア",
"VOLUME": "音量",
"VOLUME_ABBR": "音量",
"MUTE": "ミュート",
"UNMUTE": "ミュート解除",
"VOLUME_UNAVAILABLE": "このプレーヤーモードでは音量を調整できません",
"SYNCING_STATUS": "ステータスを同期中…",
"UPDATED_AT": "最終更新:{{time}}",
"WAITING_FOR_STATUS": "ステータスを待機中…",
"ERRORS": {
"CHANNEL_UP": "前のチャンネルに切り替えられませんでした",
"CHANNEL_DOWN": "次のチャンネルに切り替えられませんでした",
"SELECT_NUMBER": "チャンネル番号で切り替えられませんでした",
"VOLUME_UP": "音量を上げられませんでした",
"VOLUME_DOWN": "音量を下げられませんでした",
"TOGGLE_MUTE": "ミュートを切り替えられませんでした",
"STATUS": "リモートのステータスを取得できませんでした"
},
"HEADER": "IPTVnator リモコン",
"FOOTER": "IPTVnator リモコン"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "あと{{days}}日で期限切れ",
"SOURCE_EXPIRED": "期限切れ",
"HERO_LABEL": "おすすめ",
"HERO_ROLE_CAROUSEL": "カルーセル",
"HERO_ROLE_SLIDE": "スライド",
"HERO_FAVORITE_CHANNEL": "お気に入りチャンネル",
"HERO_FAVORITE": "お気に入り",
"HERO_RECENTLY_ADDED": "最近追加",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "すべてのプレイリストにわたる{{type}}のお気に入りがすべて削除されます。この操作は取り消せません。",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalkerポータル",
"WINDOW_MINIMIZE": "最小化",
"WINDOW_MAXIMIZE": "最大化",
"WINDOW_RESTORE": "元に戻す (縮小)",
"WINDOW_CLOSE": "閉じる",
"CHANNELS_COUNT": "{{count}}チャンネル",
"NO_SOURCES_AVAILABLE": "利用可能なソースがありません",
"ONE_SOURCE_AVAILABLE": "1件のソースが利用可能",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "취소",
"CLEAR": "지우기",
"CLOSE": "닫기",
"REMOVE": "제거",
"BACK": "뒤로",
"SHOW_MORE": "더 보기",
"SHOW_LESS": "간략히 보기",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "재생 목록이 추가되지 않았습니다",
"SELECT_PLAYLIST": "재생목록 선택",
"UNTITLED_PLAYLIST": "제목 없는 재생목록",
"PLAYLIST_ACTIONS": "재생목록 작업",
"ADD_FIRST": "다른 탭에서 첫 번째 재생 목록을 추가하십시오",
"WELCOME_TITLE": "IPTVnator에 오신 것을 환영합니다",
"WELCOME_DESCRIPTION": "첫 번째 재생목록을 추가하여 스트리밍을 시작하세요. 즐겨 사용하는 IPTV 서비스에 연결하여 라이브 TV, 영화, 시리즈를 즐겨보세요.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker 포털",
"CHANNELS": "채널",
"CHANNELS_COUNT": "{{count}}개 채널",
"PORTAL_STATUS": "포털 상태: {{status}}",
"PORTAL_STATUS_ACTIVE": "활성",
"PORTAL_STATUS_INACTIVE": "비활성",
"PORTAL_STATUS_EXPIRED": "만료됨",
"PORTAL_STATUS_UNAVAILABLE": "사용 불가",
"PORTAL_STATUS_CHECKING": "확인 중",
"ADDED": "추가되었습니다",
"SHOW_DETAILS": "재생 목록 세부 정보 표시",
"REMOVE": "재생 목록 제거",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "재생목록을 성공적으로 내보냈습니다.",
"PLAYLIST_EXPORT_ERROR": "오류: 재생목록 내보내기에 실패했습니다.",
"EXPORT_PLAYLIST_FAILED": "재생목록 내보내기에 실패했습니다.",
"EXPORT_FILE_TYPE": "재생목록",
"COPY_URL": "URL을 클립보드에 복사",
"URL_COPIED": "URL이 클립보드에 복사되었습니다"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "재생 목록을 선택",
"BROWSE": "파일 찾아보기",
"REJECTED": "파일이 거부되었습니다. 지원되지 않는 파일 형식 {{filename}}.",
"REJECTED_UNNAMED": "파일이 거부되었습니다. 지원되지 않는 파일 형식입니다.",
"OPEN_FAILED": "\"{{filename}}\" 파일을 열 수 없습니다."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "가이드를 새로 고치지 않고 표시되는 프로그램 시간을 조정합니다. -720에서 720 사이의 분 단위로 입력하세요.",
"EPG_OFFSET_LABEL": "오프셋(분)",
"EPG_OFFSET_ERROR": "-720에서 720 사이의 정수(분)를 입력하세요.",
"EPG_OFFSET_UNIT": "분",
"EPG_VIEW_MODE_DESCRIPTION": "플레이어 아래 프로그램 가이드를 표시하는 방식을 선택합니다: 가로 타임라인 리본 또는 하루 단위의 세로 목록입니다.",
"EPG_VIEW_MODE_TIMELINE": "타임라인",
"EPG_VIEW_MODE_LIST": "목록",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "새로운 내용",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "GitHub에서 {{version}}의 릴리스 노트를 찾을 수 없습니다. 릴리스가 삭제되었거나 아직 게시되지 않았을 수 있습니다.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "릴리스 노트를 불러올 수 없습니다.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "이 빌드에서는 릴리스 노트를 사용할 수 없습니다.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "릴리스 페이지 열기",
"APP_UPDATE_CHANNEL": "업데이트 채널",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable은 정식 릴리스를 따릅니다. Nightly는 master에 병합될 때마다 따라가며 프리릴리스 빌드를 자동으로 설치합니다. 저장 시 적용됩니다.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "라이브 스트림이 종료되었습니다.",
"CHECKING_SUPPORT": "내장 MPV 지원을 확인하는 중...",
"NOT_AVAILABLE": "이 환경에서는 내장 MPV를 사용할 수 없습니다.",
"FRAME_VIEW_FAILED": "내장 MPV 비디오 화면을 초기화하지 못했습니다.",
"LOADING_STREAM": "스트림을 불러오는 중…",
"PLAY": "재생",
"PAUSE": "일시정지",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "지금 녹화 중",
"RECORDINGS": "녹화",
"RECORDING_BADGE": "녹화 중",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "녹화 중지",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "관리 목록에서 녹화를 제거하시겠습니까?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "IPTV 플레이어를 원격으로 제어하세요",
"UP_CHANNEL": "이전 채널",
"DOWN_CHANNEL": "다음 채널",
"NOW_PLAYING": "지금 재생 중",
"EXECUTING": "실행 중",
"SOURCE_M3U": "M3U 라이브",
"SOURCE_XTREAM": "Xtream 라이브",
"SOURCE_STALKER": "Stalker 라이브",
"WAITING_FOR_PLAYBACK": "재생 대기 중",
"UNKNOWN_SOURCE": "알 수 없음",
"NO_STREAM": "선택된 라이브 스트림이 없습니다",
"NO_PROGRAM": "현재 EPG 프로그램이 없습니다",
"CHANNEL_ABBR": "채널",
"CHANNEL_NAVIGATION": "채널 이동",
"CHANNEL_NUMBER": "채널 번호",
"KEYPAD_DELETE": "삭제",
"KEYPAD_CLEAR": "초기화",
"VOLUME": "볼륨",
"VOLUME_ABBR": "볼륨",
"MUTE": "음소거",
"UNMUTE": "음소거 해제",
"VOLUME_UNAVAILABLE": "이 플레이어 모드에서는 볼륨을 조절할 수 없습니다",
"SYNCING_STATUS": "상태 동기화 중…",
"UPDATED_AT": "{{time}}에 업데이트됨",
"WAITING_FOR_STATUS": "상태를 기다리는 중…",
"ERRORS": {
"CHANNEL_UP": "이전 채널로 전환하지 못했습니다",
"CHANNEL_DOWN": "다음 채널로 전환하지 못했습니다",
"SELECT_NUMBER": "채널 번호로 전환하지 못했습니다",
"VOLUME_UP": "볼륨을 높이지 못했습니다",
"VOLUME_DOWN": "볼륨을 낮추지 못했습니다",
"TOGGLE_MUTE": "음소거를 전환하지 못했습니다",
"STATUS": "원격 상태를 가져오지 못했습니다"
},
"HEADER": "IPTVnator 리모컨",
"FOOTER": "IPTVnator 리모컨"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "{{days}}일 후 만료",
"SOURCE_EXPIRED": "만료됨",
"HERO_LABEL": "추천",
"HERO_ROLE_CAROUSEL": "캐러셀",
"HERO_ROLE_SLIDE": "슬라이드",
"HERO_FAVORITE_CHANNEL": "즐겨찾는 채널",
"HERO_FAVORITE": "즐겨찾기",
"HERO_RECENTLY_ADDED": "최근 추가됨",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "모든 재생목록에 걸친 모든 {{type}} 즐겨찾기가 제거됩니다. 이 작업은 되돌릴 수 없습니다.",
"XTREAM_CODE": "Xtream 코드",
"STALKER_PORTAL": "Stalker 포털",
"WINDOW_MINIMIZE": "최소화",
"WINDOW_MAXIMIZE": "최대화",
"WINDOW_RESTORE": "이전 크기로 복원",
"WINDOW_CLOSE": "닫기",
"CHANNELS_COUNT": "{{count}}개 채널",
"NO_SOURCES_AVAILABLE": "사용 가능한 소스가 없습니다",
"ONE_SOURCE_AVAILABLE": "1개 소스 사용 가능",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Annuleren",
"CLEAR": "Wissen",
"CLOSE": "Afsluiten",
"REMOVE": "Verwijderen",
"BACK": "Terug",
"SHOW_MORE": "Meer",
"SHOW_LESS": "Minder",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Geen afspeellijst toegevoegd",
"SELECT_PLAYLIST": "Afspeellijst selecteren",
"UNTITLED_PLAYLIST": "Naamloze afspeellijst",
"PLAYLIST_ACTIONS": "Afspeellijstacties",
"ADD_FIRST": "Voeg je afspeellijst toe",
"WELCOME_TITLE": "Welkom bij IPTVnator",
"WELCOME_DESCRIPTION": "Begin je streamingavontuur door je eerste afspeellijst toe te voegen. Maak verbinding met je favoriete IPTV-dienst en geniet van live tv, films en series.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker portal",
"CHANNELS": "Kanalen",
"CHANNELS_COUNT": "Kanalen: {{count}}",
"PORTAL_STATUS": "Portaalstatus: {{status}}",
"PORTAL_STATUS_ACTIVE": "actief",
"PORTAL_STATUS_INACTIVE": "inactief",
"PORTAL_STATUS_EXPIRED": "verlopen",
"PORTAL_STATUS_UNAVAILABLE": "niet beschikbaar",
"PORTAL_STATUS_CHECKING": "wordt gecontroleerd",
"ADDED": "Toegevoegd",
"SHOW_DETAILS": "Toon afspeellijstdetails",
"REMOVE": "Verwijder afspeellijst",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Afspeellijst succesvol geëxporteerd.",
"PLAYLIST_EXPORT_ERROR": "Fout: exporteren van afspeellijst mislukt.",
"EXPORT_PLAYLIST_FAILED": "Exporteren van afspeellijst mislukt.",
"EXPORT_FILE_TYPE": "Afspeellijst",
"COPY_URL": "URL naar klembord kopiëren",
"URL_COPIED": "URL naar klembord gekopieerd"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "kies een afspeellijst",
"BROWSE": "Bestanden bladeren",
"REJECTED": "Bestand geweigerd, niet-ondersteund bestandsformaat: {{filename}}.",
"REJECTED_UNNAMED": "Bestand geweigerd, niet-ondersteund bestandsformaat.",
"OPEN_FAILED": "Kan \"{{filename}}\" niet openen."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Verschuif weergegeven programmatijden zonder de gids te vernieuwen. Gebruik minuten van -720 tot 720.",
"EPG_OFFSET_LABEL": "Verschuiving (minuten)",
"EPG_OFFSET_ERROR": "Voer een geheel aantal minuten in tussen -720 en 720.",
"EPG_OFFSET_UNIT": "min",
"EPG_VIEW_MODE_DESCRIPTION": "Kies hoe de programmagids onder de speler wordt getoond: een horizontale tijdlijn of een verticale lijst voor één dag.",
"EPG_VIEW_MODE_TIMELINE": "Tijdlijn",
"EPG_VIEW_MODE_LIST": "Lijst",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Wat is nieuw",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Er zijn geen releasenotes voor {{version}} gevonden op GitHub. De release is mogelijk verwijderd of nog niet gepubliceerd.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "De releasenotes konden niet worden geladen.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Releasenotes zijn niet beschikbaar in deze build.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Releasepagina openen",
"APP_UPDATE_CHANNEL": "Updatekanaal",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable volgt de uitgebrachte releases. Nightly volgt elke merge in master en installeert voorlopige builds automatisch. Wordt toegepast bij opslaan.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "De live stream is beëindigd.",
"CHECKING_SUPPORT": "Ondersteuning voor Embedded MPV controleren...",
"NOT_AVAILABLE": "Embedded MPV is niet beschikbaar in deze omgeving.",
"FRAME_VIEW_FAILED": "De videoweergave van Embedded MPV kon niet worden geïnitialiseerd.",
"LOADING_STREAM": "Stream laden…",
"PLAY": "Afspelen",
"PAUSE": "Pauzeren",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Nu aan het opnemen",
"RECORDINGS": "Opnames",
"RECORDING_BADGE": "Opname",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Opname stoppen",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Opname uit downloadbeheer verwijderen?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Bedien je IPTV-speler op afstand",
"UP_CHANNEL": "Vorig kanaal",
"DOWN_CHANNEL": "Volgend kanaal",
"NOW_PLAYING": "Nu bezig",
"EXECUTING": "Wordt uitgevoerd",
"SOURCE_M3U": "Live-tv via M3U",
"SOURCE_XTREAM": "Live-tv via Xtream",
"SOURCE_STALKER": "Live-tv via Stalker",
"WAITING_FOR_PLAYBACK": "Wachten op afspelen",
"UNKNOWN_SOURCE": "onbekend",
"NO_STREAM": "Geen live stream geselecteerd",
"NO_PROGRAM": "Geen actueel EPG-programma",
"CHANNEL_ABBR": "KAN",
"CHANNEL_NAVIGATION": "Kanaalnavigatie",
"CHANNEL_NUMBER": "Kanaalnummer",
"KEYPAD_DELETE": "TERUG",
"KEYPAD_CLEAR": "WIS",
"VOLUME": "Volume",
"VOLUME_ABBR": "VOL",
"MUTE": "DEMPEN",
"UNMUTE": "DEMPEN UIT",
"VOLUME_UNAVAILABLE": "Volumeregeling niet beschikbaar in deze spelermodus",
"SYNCING_STATUS": "Status synchroniseren…",
"UPDATED_AT": "Bijgewerkt om {{time}}",
"WAITING_FOR_STATUS": "Wachten op status…",
"ERRORS": {
"CHANNEL_UP": "Kanaal omhoog schakelen mislukt",
"CHANNEL_DOWN": "Kanaal omlaag schakelen mislukt",
"SELECT_NUMBER": "Schakelen via kanaalnummer mislukt",
"VOLUME_UP": "Verhogen van volume mislukt",
"VOLUME_DOWN": "Verlagen van volume mislukt",
"TOGGLE_MUTE": "In- of uitschakelen van dempen mislukt",
"STATUS": "Ophalen van status op afstand mislukt"
},
"HEADER": "IPTVnator-afstandsbediening",
"FOOTER": "IPTVnator-afstandsbediening"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Verloopt over {{days}} d",
"SOURCE_EXPIRED": "Verlopen",
"HERO_LABEL": "Uitgelicht",
"HERO_ROLE_CAROUSEL": "carrousel",
"HERO_ROLE_SLIDE": "dia",
"HERO_FAVORITE_CHANNEL": "Favoriete zender",
"HERO_FAVORITE": "Favoriet",
"HERO_RECENTLY_ADDED": "Recent toegevoegd",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Alle {{type}}-favorieten in alle afspeellijsten worden verwijderd. Deze actie kan niet ongedaan worden gemaakt.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker Portal",
"WINDOW_MINIMIZE": "Minimaliseren",
"WINDOW_MAXIMIZE": "Maximaliseren",
"WINDOW_RESTORE": "Vorige grootte",
"WINDOW_CLOSE": "Sluiten",
"CHANNELS_COUNT": "{{count}} kanalen",
"NO_SOURCES_AVAILABLE": "Geen bronnen beschikbaar",
"ONE_SOURCE_AVAILABLE": "1 bron beschikbaar",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Anuluj",
"CLEAR": "Wyczyść",
"CLOSE": "Zamknij",
"REMOVE": "Usuń",
"BACK": "Wstecz",
"SHOW_MORE": "Więcej",
"SHOW_LESS": "Mniej",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Nie dodano żadnych list odtwarzania",
"SELECT_PLAYLIST": "Wybierz listę odtwarzania",
"UNTITLED_PLAYLIST": "Lista odtwarzania bez tytułu",
"PLAYLIST_ACTIONS": "Akcje listy odtwarzania",
"ADD_FIRST": "Dodaj swoją pierwszą listę odtwarzania",
"WELCOME_TITLE": "Witaj w IPTVnator",
"WELCOME_DESCRIPTION": "Rozpocznij streamingową przygodę, dodając pierwszą listę odtwarzania. Połącz się z ulubionym dostawcą IPTV i ciesz się telewizją na żywo, filmami i serialami.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Portal Stalker",
"CHANNELS": "Kanały",
"CHANNELS_COUNT": "{{count}} kanałów",
"PORTAL_STATUS": "Status portalu: {{status}}",
"PORTAL_STATUS_ACTIVE": "aktywny",
"PORTAL_STATUS_INACTIVE": "nieaktywny",
"PORTAL_STATUS_EXPIRED": "wygasły",
"PORTAL_STATUS_UNAVAILABLE": "niedostępny",
"PORTAL_STATUS_CHECKING": "sprawdzanie",
"ADDED": "Dodano",
"SHOW_DETAILS": "Pokaż szczegóły listy odtwarzania",
"REMOVE": "Usuń listę odtwarzania",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Lista odtwarzania została pomyślnie wyeksportowana.",
"PLAYLIST_EXPORT_ERROR": "Błąd: Eksport listy odtwarzania nie powiódł się.",
"EXPORT_PLAYLIST_FAILED": "Eksport listy odtwarzania nie powiódł się.",
"EXPORT_FILE_TYPE": "Lista odtwarzania",
"COPY_URL": "Kopiuj URL do schowka",
"URL_COPIED": "URL skopiowany do schowka"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "wybierz listę odtwarzania",
"BROWSE": "Przeglądaj pliki",
"REJECTED": "Plik został odrzucony, nieobsługiwany format pliku {{filename}}.",
"REJECTED_UNNAMED": "Plik został odrzucony, nieobsługiwany format pliku.",
"OPEN_FAILED": "Nie udało się otworzyć \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Przesuń wyświetlane godziny programów bez odświeżania przewodnika. Użyj minut od -720 do 720.",
"EPG_OFFSET_LABEL": "Przesunięcie (minuty)",
"EPG_OFFSET_ERROR": "Wpisz całkowitą liczbę minut z zakresu od -720 do 720.",
"EPG_OFFSET_UNIT": "min",
"EPG_VIEW_MODE_DESCRIPTION": "Wybierz sposób wyświetlania przewodnika po programach pod odtwarzaczem: pozioma oś czasu lub pionowa lista dla jednego dnia.",
"EPG_VIEW_MODE_TIMELINE": "Oś czasu",
"EPG_VIEW_MODE_LIST": "Lista",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Co nowego",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Nie znaleziono informacji o wydaniu {{version}} w GitHub. Wydanie mogło zostać usunięte lub nie zostało jeszcze opublikowane.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Nie udało się wczytać informacji o wydaniu.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Informacje o wydaniu nie są dostępne w tej kompilacji.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Otwórz stronę wydań",
"APP_UPDATE_CHANNEL": "Kanał aktualizacji",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable podąża za opublikowanymi wydaniami. Nightly podąża za każdym scaleniem do master i automatycznie instaluje wersje wstępne. Obowiązuje po zapisaniu.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "Transmisja na żywo zakończyła się.",
"CHECKING_SUPPORT": "Sprawdzanie obsługi wbudowanego MPV...",
"NOT_AVAILABLE": "Wbudowane MPV nie jest dostępne w tym środowisku.",
"FRAME_VIEW_FAILED": "Nie udało się zainicjować widoku wideo wbudowanego MPV.",
"LOADING_STREAM": "Ładowanie strumienia…",
"PLAY": "Odtwórz",
"PAUSE": "Wstrzymaj",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Trwa nagrywanie",
"RECORDINGS": "Nagrania",
"RECORDING_BADGE": "Nagrywanie",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Zatrzymaj nagrywanie",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Usunąć nagranie z menedżera?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Steruj odtwarzaczem IPTV zdalnie",
"UP_CHANNEL": "Poprzedni kanał",
"DOWN_CHANNEL": "Następny kanał",
"NOW_PLAYING": "Teraz odtwarzane",
"EXECUTING": "Wykonywanie",
"SOURCE_M3U": "M3U na żywo",
"SOURCE_XTREAM": "Xtream na żywo",
"SOURCE_STALKER": "Stalker na żywo",
"WAITING_FOR_PLAYBACK": "Oczekiwanie na odtwarzanie",
"UNKNOWN_SOURCE": "nieznane",
"NO_STREAM": "Nie wybrano transmisji na żywo",
"NO_PROGRAM": "Brak bieżącego programu EPG",
"CHANNEL_ABBR": "CH",
"CHANNEL_NAVIGATION": "Nawigacja po kanałach",
"CHANNEL_NUMBER": "Numer kanału",
"KEYPAD_DELETE": "USUŃ",
"KEYPAD_CLEAR": "WYCZYŚĆ",
"VOLUME": "Głośność",
"VOLUME_ABBR": "VOL",
"MUTE": "WYCISZ",
"UNMUTE": "WYŁĄCZ WYCISZENIE",
"VOLUME_UNAVAILABLE": "Sterowanie głośnością jest niedostępne w tym trybie odtwarzacza",
"SYNCING_STATUS": "Synchronizacja statusu…",
"UPDATED_AT": "Zaktualizowano {{time}}",
"WAITING_FOR_STATUS": "Oczekiwanie na status…",
"ERRORS": {
"CHANNEL_UP": "Nie udało się przełączyć kanału w górę",
"CHANNEL_DOWN": "Nie udało się przełączyć kanału w dół",
"SELECT_NUMBER": "Nie udało się przełączyć kanału według numeru",
"VOLUME_UP": "Nie udało się zwiększyć głośności",
"VOLUME_DOWN": "Nie udało się zmniejszyć głośności",
"TOGGLE_MUTE": "Nie udało się przełączyć wyciszenia",
"STATUS": "Nie udało się pobrać statusu zdalnego sterowania"
},
"HEADER": "Pilot IPTVnator",
"FOOTER": "Pilot IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Wygasa za {{days}} dn.",
"SOURCE_EXPIRED": "Wygasło",
"HERO_LABEL": "Polecane",
"HERO_ROLE_CAROUSEL": "karuzela",
"HERO_ROLE_SLIDE": "slajd",
"HERO_FAVORITE_CHANNEL": "Ulubiony kanał",
"HERO_FAVORITE": "Ulubione",
"HERO_RECENTLY_ADDED": "Ostatnio dodane",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Wszystkie ulubione {{type}} ze wszystkich list odtwarzania zostaną usunięte. Tej akcji nie można cofnąć.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Portal Stalker",
"WINDOW_MINIMIZE": "Minimalizuj",
"WINDOW_MAXIMIZE": "Maksymalizuj",
"WINDOW_RESTORE": "Przywróć w dół",
"WINDOW_CLOSE": "Zamknij",
"CHANNELS_COUNT": "{{count}} kanałów",
"NO_SOURCES_AVAILABLE": "Brak dostępnych źródeł",
"ONE_SOURCE_AVAILABLE": "1 źródło dostępne",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Cancelar",
"CLEAR": "Limpar",
"CLOSE": "Fechar",
"REMOVE": "Remover",
"BACK": "Voltar",
"SHOW_MORE": "Mais",
"SHOW_LESS": "Menos",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Nenhuma playlist foi adicionada",
"SELECT_PLAYLIST": "Selecionar playlist",
"UNTITLED_PLAYLIST": "Playlist sem título",
"PLAYLIST_ACTIONS": "Ações da playlist",
"ADD_FIRST": "Por favor, adicione sua primeira playlist",
"WELCOME_TITLE": "Bem-vindo ao IPTVnator",
"WELCOME_DESCRIPTION": "Comece sua jornada de streaming adicionando sua primeira playlist. Conecte-se ao seu serviço de IPTV preferido e aproveite TV ao vivo, filmes e séries.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Portal Stalker",
"CHANNELS": "Canais",
"CHANNELS_COUNT": "Canais: {{count}}",
"PORTAL_STATUS": "Status do portal: {{status}}",
"PORTAL_STATUS_ACTIVE": "ativo",
"PORTAL_STATUS_INACTIVE": "inativo",
"PORTAL_STATUS_EXPIRED": "expirado",
"PORTAL_STATUS_UNAVAILABLE": "indisponível",
"PORTAL_STATUS_CHECKING": "verificando",
"ADDED": "Adicionado",
"SHOW_DETAILS": "Mostrar detalhes da playlist",
"REMOVE": "Remover playlist",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Playlist foi exportada com sucesso.",
"PLAYLIST_EXPORT_ERROR": "Erro: Falha ao exportar a playlist.",
"EXPORT_PLAYLIST_FAILED": "Falha ao exportar a playlist.",
"EXPORT_FILE_TYPE": "Arquivo de playlist",
"COPY_URL": "Copiar URL para a área de transferência",
"URL_COPIED": "URL copiada para a área de transferência"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "escolha uma playlist",
"BROWSE": "Procurar arquivos",
"REJECTED": "O arquivo foi rejeitado, formato de arquivo não suportado {{filename}}.",
"REJECTED_UNNAMED": "O arquivo foi rejeitado, formato de arquivo não suportado.",
"OPEN_FAILED": "Não foi possível abrir \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Desloque os horários dos programas exibidos sem atualizar o guia. Use minutos de -720 a 720.",
"EPG_OFFSET_LABEL": "Deslocamento (minutos)",
"EPG_OFFSET_ERROR": "Insira um número inteiro de minutos entre -720 e 720.",
"EPG_OFFSET_UNIT": "min",
"EPG_VIEW_MODE_DESCRIPTION": "Escolha como o guia de programação abaixo do reprodutor é exibido: uma faixa horizontal em linha do tempo ou uma lista vertical de um único dia.",
"EPG_VIEW_MODE_TIMELINE": "Linha do tempo",
"EPG_VIEW_MODE_LIST": "Lista",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Novidades",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Nenhuma nota de versão foi encontrada para {{version}} no GitHub. A versão pode ter sido removida ou ainda não foi publicada.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Não foi possível carregar as notas da versão.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "As notas da versão não estão disponíveis nesta compilação.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Abrir página de versões",
"APP_UPDATE_CHANNEL": "Canal de atualização",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable segue as versões publicadas. Nightly segue cada merge em master e instala compilações preliminares automaticamente. Aplica-se ao salvar.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "A transmissão ao vivo terminou.",
"CHECKING_SUPPORT": "Verificando suporte ao MPV integrado...",
"NOT_AVAILABLE": "O MPV integrado não está disponível neste ambiente.",
"FRAME_VIEW_FAILED": "Falha ao inicializar a visualização de vídeo do MPV integrado.",
"LOADING_STREAM": "Carregando stream…",
"PLAY": "Reproduzir",
"PAUSE": "Pausar",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Gravando agora",
"RECORDINGS": "Gravações",
"RECORDING_BADGE": "Gravação",
"RECORDING_CHIP": "REC",
"STOP_RECORDING": "Parar gravação",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Remover gravação do gerenciador?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Controle seu reprodutor IPTV remotamente",
"UP_CHANNEL": "Canal anterior",
"DOWN_CHANNEL": "Próximo canal",
"NOW_PLAYING": "Reproduzindo agora",
"EXECUTING": "Executando",
"SOURCE_M3U": "M3U ao vivo",
"SOURCE_XTREAM": "Xtream ao vivo",
"SOURCE_STALKER": "Stalker ao vivo",
"WAITING_FOR_PLAYBACK": "Aguardando reprodução",
"UNKNOWN_SOURCE": "desconhecido",
"NO_STREAM": "Nenhuma transmissão ao vivo selecionada",
"NO_PROGRAM": "Nenhum programa atual no EPG",
"CHANNEL_ABBR": "CH",
"CHANNEL_NAVIGATION": "Navegação de canais",
"CHANNEL_NUMBER": "Número do canal",
"KEYPAD_DELETE": "APAGAR",
"KEYPAD_CLEAR": "LIMPAR",
"VOLUME": "Volume",
"VOLUME_ABBR": "VOL",
"MUTE": "SILENCIAR",
"UNMUTE": "ATIVAR SOM",
"VOLUME_UNAVAILABLE": "Controle de volume indisponível neste modo de reprodutor",
"SYNCING_STATUS": "Sincronizando status…",
"UPDATED_AT": "Atualizado às {{time}}",
"WAITING_FOR_STATUS": "Aguardando status…",
"ERRORS": {
"CHANNEL_UP": "Falha ao mudar para o canal anterior",
"CHANNEL_DOWN": "Falha ao mudar para o próximo canal",
"SELECT_NUMBER": "Falha ao mudar de canal pelo número",
"VOLUME_UP": "Falha ao aumentar o volume",
"VOLUME_DOWN": "Falha ao diminuir o volume",
"TOGGLE_MUTE": "Falha ao ativar ou desativar o som",
"STATUS": "Falha ao obter o status do controle remoto"
},
"HEADER": "Controle remoto do IPTVnator",
"FOOTER": "Controle remoto do IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Expira em {{days}} d",
"SOURCE_EXPIRED": "Expirada",
"HERO_LABEL": "Em destaque",
"HERO_ROLE_CAROUSEL": "carrossel",
"HERO_ROLE_SLIDE": "slide",
"HERO_FAVORITE_CHANNEL": "Canal favorito",
"HERO_FAVORITE": "Favorito",
"HERO_RECENTLY_ADDED": "Adicionado recentemente",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Todos os favoritos de {{type}} de todas as playlists serão removidos. Esta ação não pode ser desfeita.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Portal Stalker",
"WINDOW_MINIMIZE": "Minimizar",
"WINDOW_MAXIMIZE": "Maximizar",
"WINDOW_RESTORE": "Restaurar",
"WINDOW_CLOSE": "Fechar",
"CHANNELS_COUNT": "{{count}} canais",
"NO_SOURCES_AVAILABLE": "Nenhuma fonte disponível",
"ONE_SOURCE_AVAILABLE": "1 fonte disponível",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "Отмена",
"CLEAR": "Очистить",
"CLOSE": "Закрыть",
"REMOVE": "Удалить",
"BACK": "Назад",
"SHOW_MORE": "Больше",
"SHOW_LESS": "Меньше",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Пока еще не было добавлено ни одного плейлиста",
"SELECT_PLAYLIST": "Выберите плейлист",
"UNTITLED_PLAYLIST": "Плейлист без названия",
"PLAYLIST_ACTIONS": "Действия с плейлистом",
"ADD_FIRST": "Вы можете добавить плейлисты из соседних вкладок",
"WELCOME_TITLE": "Добро пожаловать в IPTVnator",
"WELCOME_DESCRIPTION": "Начните свое путешествие в мир стриминга, добавив свой первый плейлист. Подключитесь к вашему любимому IPTV-сервису и наслаждайтесь прямыми трансляциями, фильмами и сериалами.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker портал",
"CHANNELS": "Каналов",
"CHANNELS_COUNT": "Каналов: {{count}}",
"PORTAL_STATUS": "Статус портала: {{status}}",
"PORTAL_STATUS_ACTIVE": "активен",
"PORTAL_STATUS_INACTIVE": "неактивен",
"PORTAL_STATUS_EXPIRED": "истёк",
"PORTAL_STATUS_UNAVAILABLE": "недоступен",
"PORTAL_STATUS_CHECKING": "проверяется",
"ADDED": "Добавлен",
"SHOW_DETAILS": "Показать информацию о плейлисте",
"REMOVE": "Удалить плейлист",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Плейлист был успешно экспортирован.",
"PLAYLIST_EXPORT_ERROR": "Ошибка: не удалось экспортировать плейлист.",
"EXPORT_PLAYLIST_FAILED": "Не удалось экспортировать плейлист.",
"EXPORT_FILE_TYPE": "Плейлист",
"COPY_URL": "Скопировать URL в буфер обмена",
"URL_COPIED": "URL скопирован в буфер обмена"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "выберите плейлист",
"BROWSE": "Выбрать файлы",
"REJECTED": "К сожалению, данный формат файлов не поддерживается {{filename}}.",
"REJECTED_UNNAMED": "К сожалению, данный формат файлов не поддерживается.",
"OPEN_FAILED": "Не удалось открыть \"{{filename}}\"."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Смещайте время отображения программ без обновления телепрограммы. Укажите минуты от -720 до 720.",
"EPG_OFFSET_LABEL": "Смещение (минуты)",
"EPG_OFFSET_ERROR": "Введите целое число минут от -720 до 720.",
"EPG_OFFSET_UNIT": "мин",
"EPG_VIEW_MODE_DESCRIPTION": "Выберите, как отображается программа передач под плеером: горизонтальная лента-таймлайн или вертикальный список на один день.",
"EPG_VIEW_MODE_TIMELINE": "Таймлайн",
"EPG_VIEW_MODE_LIST": "Список",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Что нового",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "Заметки к версии {{version}} на GitHub не найдены. Релиз мог быть удалён или ещё не опубликован.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Не удалось загрузить заметки к версии.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Заметки к версии недоступны в этой сборке.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Открыть страницу релизов",
"APP_UPDATE_CHANNEL": "Канал обновлений",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable следует за выпущенными релизами. Nightly следует за каждым слиянием в master и автоматически устанавливает предварительные сборки. Применяется после сохранения.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "Прямой эфир завершился.",
"CHECKING_SUPPORT": "Проверка поддержки встроенного MPV...",
"NOT_AVAILABLE": "Встроенный MPV недоступен в этой среде.",
"FRAME_VIEW_FAILED": "Не удалось инициализировать область видео встроенного MPV.",
"LOADING_STREAM": "Загрузка потока…",
"PLAY": "Воспроизвести",
"PAUSE": "Пауза",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Записываются сейчас",
"RECORDINGS": "Записи",
"RECORDING_BADGE": "Запись",
"RECORDING_CHIP": "ЗАПИСЬ",
"STOP_RECORDING": "Остановить запись",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Убрать запись из менеджера?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "Управляйте IPTV-плеером удалённо",
"UP_CHANNEL": "Предыдущий канал",
"DOWN_CHANNEL": "Следующий канал",
"NOW_PLAYING": "Сейчас воспроизводится",
"EXECUTING": "Выполняется",
"SOURCE_M3U": "Прямой эфир M3U",
"SOURCE_XTREAM": "Прямой эфир Xtream",
"SOURCE_STALKER": "Прямой эфир Stalker",
"WAITING_FOR_PLAYBACK": "Ожидание воспроизведения",
"UNKNOWN_SOURCE": "неизвестно",
"NO_STREAM": "Канал не выбран",
"NO_PROGRAM": "Нет текущей передачи в EPG",
"CHANNEL_ABBR": "КАНАЛ",
"CHANNEL_NAVIGATION": "Переключение каналов",
"CHANNEL_NUMBER": "Номер канала",
"KEYPAD_DELETE": "СТЕРЕТЬ",
"KEYPAD_CLEAR": "СБРОС",
"VOLUME": "Громкость",
"VOLUME_ABBR": "ЗВУК",
"MUTE": "ВЫКЛ. ЗВУК",
"UNMUTE": "ВКЛ. ЗВУК",
"VOLUME_UNAVAILABLE": "Регулировка громкости недоступна в этом режиме плеера",
"SYNCING_STATUS": "Синхронизация статуса…",
"UPDATED_AT": "Обновлено в {{time}}",
"WAITING_FOR_STATUS": "Ожидание статуса…",
"ERRORS": {
"CHANNEL_UP": "Не удалось переключиться на предыдущий канал",
"CHANNEL_DOWN": "Не удалось переключиться на следующий канал",
"SELECT_NUMBER": "Не удалось переключить канал по номеру",
"VOLUME_UP": "Не удалось увеличить громкость",
"VOLUME_DOWN": "Не удалось уменьшить громкость",
"TOGGLE_MUTE": "Не удалось включить или отключить звук",
"STATUS": "Не удалось получить статус дистанционного управления"
},
"HEADER": "Пульт IPTVnator",
"FOOTER": "Пульт IPTVnator"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "Истекает через {{days}} дн.",
"SOURCE_EXPIRED": "Истёк",
"HERO_LABEL": "Рекомендуемое",
"HERO_ROLE_CAROUSEL": "карусель",
"HERO_ROLE_SLIDE": "слайд",
"HERO_FAVORITE_CHANNEL": "Избранный канал",
"HERO_FAVORITE": "Избранное",
"HERO_RECENTLY_ADDED": "Недавно добавлено",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Все избранное «{{type}}» из всех плейлистов будет удалено. Это действие нельзя отменить.",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Портал Stalker",
"WINDOW_MINIMIZE": "Свернуть",
"WINDOW_MAXIMIZE": "Развернуть",
"WINDOW_RESTORE": "Восстановить",
"WINDOW_CLOSE": "Закрыть",
"CHANNELS_COUNT": "{{count}} каналов",
"NO_SOURCES_AVAILABLE": "Нет доступных источников",
"ONE_SOURCE_AVAILABLE": "Доступен 1 источник",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "İptal",
"CLEAR": "Temizle",
"CLOSE": "Kapat",
"REMOVE": "Kaldır",
"BACK": "Geri",
"SHOW_MORE": "Daha fazla",
"SHOW_LESS": "Daha az",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "Oynatma listesi eklenmedi",
"SELECT_PLAYLIST": "Oynatma listesi seçin",
"UNTITLED_PLAYLIST": "Adsız oynatma listesi",
"PLAYLIST_ACTIONS": "Oynatma listesi eylemleri",
"ADD_FIRST": "Lütfen ilk oynatma listenizi ekleyin",
"WELCOME_TITLE": "IPTVnator'a Hoş Geldiniz",
"WELCOME_DESCRIPTION": "İlk oynatma listenizi ekleyerek yayın yolculuğunuza başlayın. Favori IPTV hizmetinize bağlanın ve canlı TV, film ve dizilerin keyfini çıkarın.",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker portalı",
"CHANNELS": "Kanallar",
"CHANNELS_COUNT": "{{count}} kanal",
"PORTAL_STATUS": "Portal durumu: {{status}}",
"PORTAL_STATUS_ACTIVE": "etkin",
"PORTAL_STATUS_INACTIVE": "devre dışı",
"PORTAL_STATUS_EXPIRED": "süresi doldu",
"PORTAL_STATUS_UNAVAILABLE": "kullanılamıyor",
"PORTAL_STATUS_CHECKING": "kontrol ediliyor",
"ADDED": "Eklendi",
"SHOW_DETAILS": "Oynatma listesi ayrıntılarını göster",
"REMOVE": "Oynatma listesini kaldır",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "Oynatma listesi başarıyla dışa aktarıldı.",
"PLAYLIST_EXPORT_ERROR": "Hata: Oynatma listesi dışa aktarılamadı.",
"EXPORT_PLAYLIST_FAILED": "Oynatma listesi dışa aktarılamadı.",
"EXPORT_FILE_TYPE": "Oynatma listesi",
"COPY_URL": "URL'yi panoya kopyala",
"URL_COPIED": "URL panoya kopyalandı"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "oynatma listesi seçin",
"BROWSE": "Dosyalara göz at",
"REJECTED": "Dosya reddedildi, desteklenmeyen dosya formatı {{filename}}.",
"REJECTED_UNNAMED": "Dosya reddedildi, desteklenmeyen dosya formatı.",
"OPEN_FAILED": "\"{{filename}}\" açılamadı."
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "Rehberi yenilemeden görüntülenen program saatlerini kaydırın. -720 ile 720 arasında dakika kullanın.",
"EPG_OFFSET_LABEL": "Kayma (dakika)",
"EPG_OFFSET_ERROR": "-720 ile 720 arasında tam sayı olarak dakika girin.",
"EPG_OFFSET_UNIT": "dk",
"EPG_VIEW_MODE_DESCRIPTION": "Oynatıcının altındaki program rehberinin nasıl gösterileceğini seçin: yatay bir zaman çizelgesi şeridi veya dikey, tek günlük bir liste.",
"EPG_VIEW_MODE_TIMELINE": "Zaman çizelgesi",
"EPG_VIEW_MODE_LIST": "Liste",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "Yenilikler",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "GitHub'da {{version}} için sürüm notu bulunamadı. Sürüm kaldırılmış ya da henüz yayınlanmamış olabilir.",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "Sürüm notları yüklenemedi.",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "Sürüm notları bu derlemede kullanılamıyor.",
"APP_UPDATE_OPEN_RELEASES_PAGE": "Sürümler sayfasını aç",
"APP_UPDATE_CHANNEL": "Güncelleme kanalı",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable, yayımlanan sürümleri izler. Nightly, master’a yapılan her birleştirmeyi izler ve ön sürümleri otomatik olarak kurar. Kaydedildiğinde uygulanır.",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "Canlı yayın sona erdi.",
"CHECKING_SUPPORT": "Gömülü MPV desteği denetleniyor...",
"NOT_AVAILABLE": "Gömülü MPV bu ortamda kullanılamıyor.",
"FRAME_VIEW_FAILED": "Gömülü MPV video görünümü başlatılamadı.",
"LOADING_STREAM": "Yayın yükleniyor…",
"PLAY": "Oynat",
"PAUSE": "Duraklat",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "Şu anda kaydediliyor",
"RECORDINGS": "Kayıtlar",
"RECORDING_BADGE": "Kaydediliyor",
"RECORDING_CHIP": "KAYIT",
"STOP_RECORDING": "Kaydı durdur",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "Kayıt yöneticiden kaldırılsın mı?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "IPTV oynatıcınızı uzaktan kontrol edin",
"UP_CHANNEL": "Önceki kanal",
"DOWN_CHANNEL": "Sonraki kanal",
"NOW_PLAYING": "Şimdi oynatılıyor",
"EXECUTING": "Yürütülüyor",
"SOURCE_M3U": "M3U canlı",
"SOURCE_XTREAM": "Xtream canlı",
"SOURCE_STALKER": "Stalker canlı",
"WAITING_FOR_PLAYBACK": "Oynatma bekleniyor",
"UNKNOWN_SOURCE": "bilinmiyor",
"NO_STREAM": "Canlı yayın seçilmedi",
"NO_PROGRAM": "Geçerli EPG programı yok",
"CHANNEL_ABBR": "KANAL",
"CHANNEL_NAVIGATION": "Kanal değiştirme",
"CHANNEL_NUMBER": "Kanal numarası",
"KEYPAD_DELETE": "SİL",
"KEYPAD_CLEAR": "TEMİZLE",
"VOLUME": "Ses seviyesi",
"VOLUME_ABBR": "SES",
"MUTE": "SESİ KAPAT",
"UNMUTE": "SESİ AÇ",
"VOLUME_UNAVAILABLE": "Bu oynatıcı modunda ses kontrolü kullanılamıyor",
"SYNCING_STATUS": "Durum eşitleniyor…",
"UPDATED_AT": "Son güncelleme: {{time}}",
"WAITING_FOR_STATUS": "Durum bekleniyor…",
"ERRORS": {
"CHANNEL_UP": "Önceki kanala geçilemedi",
"CHANNEL_DOWN": "Sonraki kanala geçilemedi",
"SELECT_NUMBER": "Kanal numarasıyla geçiş yapılamadı",
"VOLUME_UP": "Ses seviyesi artırılamadı",
"VOLUME_DOWN": "Ses seviyesi azaltılamadı",
"TOGGLE_MUTE": "Ses açılamadı veya kapatılamadı",
"STATUS": "Uzaktan kumanda durumu alınamadı"
},
"HEADER": "IPTVnator Uzaktan Kumanda",
"FOOTER": "IPTVnator Uzaktan Kumanda"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "{{days}} gün içinde sona erer",
"SOURCE_EXPIRED": "Süresi doldu",
"HERO_LABEL": "Öne çıkanlar",
"HERO_ROLE_CAROUSEL": "slayt gösterisi",
"HERO_ROLE_SLIDE": "slayt",
"HERO_FAVORITE_CHANNEL": "Favori kanal",
"HERO_FAVORITE": "Favori",
"HERO_RECENTLY_ADDED": "Yeni eklendi",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "Tüm oynatma listelerindeki tüm {{type}} favoriler kaldırılacaktır. Bu işlem geri alınamaz.",
"XTREAM_CODE": "Xtream Kodu",
"STALKER_PORTAL": "Stalker Portalı",
"WINDOW_MINIMIZE": "Simge durumuna küçült",
"WINDOW_MAXIMIZE": "Ekranı kapla",
"WINDOW_RESTORE": "Geri yükle",
"WINDOW_CLOSE": "Kapat",
"CHANNELS_COUNT": "{{count}} kanal",
"NO_SOURCES_AVAILABLE": "Kullanılabilir kaynak yok",
"ONE_SOURCE_AVAILABLE": "1 kaynak kullanılabilir",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "取消",
"CLEAR": "清除",
"CLOSE": "关闭",
"REMOVE": "移除",
"BACK": "返回",
"SHOW_MORE": "更多",
"SHOW_LESS": "收起",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "未添加播放列表",
"SELECT_PLAYLIST": "选择播放列表",
"UNTITLED_PLAYLIST": "未命名播放列表",
"PLAYLIST_ACTIONS": "播放列表操作",
"ADD_FIRST": "从其他标签页添加你的第一个播放列表",
"WELCOME_TITLE": "欢迎使用 IPTVnator",
"WELCOME_DESCRIPTION": "添加您的第一个播放列表,开启流媒体之旅。连接到您喜爱的 IPTV 服务,享受直播、电影和剧集。",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker 门户",
"CHANNELS": "频道",
"CHANNELS_COUNT": "{{count}} 个频道",
"PORTAL_STATUS": "门户状态:{{status}}",
"PORTAL_STATUS_ACTIVE": "有效",
"PORTAL_STATUS_INACTIVE": "未激活",
"PORTAL_STATUS_EXPIRED": "已过期",
"PORTAL_STATUS_UNAVAILABLE": "不可用",
"PORTAL_STATUS_CHECKING": "检查中",
"ADDED": "已添加的",
"SHOW_DETAILS": "显示播放列表详情",
"REMOVE": "移除播放列表",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "播放列表已成功导出。",
"PLAYLIST_EXPORT_ERROR": "错误:播放列表导出失败。",
"EXPORT_PLAYLIST_FAILED": "播放列表导出失败。",
"EXPORT_FILE_TYPE": "播放列表",
"COPY_URL": "复制 URL 到剪贴板",
"URL_COPIED": "URL 已复制到剪贴板"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "选择一个播放列表",
"BROWSE": "浏览文件",
"REJECTED": "文件已被拒绝,不支持的文件格式 {{filename}}。",
"REJECTED_UNNAMED": "文件已被拒绝,不支持的文件格式。",
"OPEN_FAILED": "无法打开 \"{{filename}}\"。"
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "无需刷新节目指南即可调整显示的节目时间。请输入 -720 到 720 之间的分钟数。",
"EPG_OFFSET_LABEL": "偏移(分钟)",
"EPG_OFFSET_ERROR": "请输入 -720 到 720 之间的整数分钟数。",
"EPG_OFFSET_UNIT": "分钟",
"EPG_VIEW_MODE_DESCRIPTION": "选择播放器下方的节目指南如何显示:横向时间轴条,或纵向的单日列表。",
"EPG_VIEW_MODE_TIMELINE": "时间轴",
"EPG_VIEW_MODE_LIST": "列表",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "更新内容",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "在 GitHub 上未找到 {{version}} 的更新说明。该版本可能已被移除或尚未发布。",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "无法加载更新说明。",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "此构建中没有可用的更新说明。",
"APP_UPDATE_OPEN_RELEASES_PAGE": "打开版本发布页面",
"APP_UPDATE_CHANNEL": "更新频道",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable 跟随正式发布的版本。Nightly 跟随每次合并到 master 的更改,并自动安装预发布构建。保存后生效。",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "直播已结束。",
"CHECKING_SUPPORT": "正在检查嵌入式 MPV 支持…",
"NOT_AVAILABLE": "嵌入式 MPV 在当前环境中不可用。",
"FRAME_VIEW_FAILED": "嵌入式 MPV 视频视图初始化失败。",
"LOADING_STREAM": "正在加载流…",
"PLAY": "播放",
"PAUSE": "暂停",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "正在录制",
"RECORDINGS": "录制内容",
"RECORDING_BADGE": "录制中",
"RECORDING_CHIP": "录制",
"STOP_RECORDING": "停止录制",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "从管理器中移除录制?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "远程控制您的 IPTV 播放器",
"UP_CHANNEL": "上一个频道",
"DOWN_CHANNEL": "下一个频道",
"NOW_PLAYING": "正在播放",
"EXECUTING": "执行中",
"SOURCE_M3U": "M3U 直播",
"SOURCE_XTREAM": "Xtream 直播",
"SOURCE_STALKER": "Stalker 直播",
"WAITING_FOR_PLAYBACK": "等待播放",
"UNKNOWN_SOURCE": "未知",
"NO_STREAM": "未选择直播流",
"NO_PROGRAM": "当前无 EPG 节目",
"CHANNEL_ABBR": "频道",
"CHANNEL_NAVIGATION": "频道导航",
"CHANNEL_NUMBER": "频道号",
"KEYPAD_DELETE": "删除",
"KEYPAD_CLEAR": "清除",
"VOLUME": "音量",
"VOLUME_ABBR": "音量",
"MUTE": "静音",
"UNMUTE": "取消静音",
"VOLUME_UNAVAILABLE": "当前播放器模式不支持音量控制",
"SYNCING_STATUS": "正在同步状态…",
"UPDATED_AT": "更新于 {{time}}",
"WAITING_FOR_STATUS": "正在等待状态…",
"ERRORS": {
"CHANNEL_UP": "切换到上一个频道失败",
"CHANNEL_DOWN": "切换到下一个频道失败",
"SELECT_NUMBER": "按频道号切换失败",
"VOLUME_UP": "调高音量失败",
"VOLUME_DOWN": "调低音量失败",
"TOGGLE_MUTE": "切换静音失败",
"STATUS": "获取远程状态失败"
},
"HEADER": "IPTVnator 遥控器",
"FOOTER": "IPTVnator 遥控器"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "{{days}} 天后到期",
"SOURCE_EXPIRED": "已过期",
"HERO_LABEL": "精选",
"HERO_ROLE_CAROUSEL": "轮播",
"HERO_ROLE_SLIDE": "幻灯片",
"HERO_FAVORITE_CHANNEL": "收藏频道",
"HERO_FAVORITE": "收藏",
"HERO_RECENTLY_ADDED": "最近添加",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "所有播放列表中所有的{{type}}收藏将被移除。此操作无法撤销。",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker 门户",
"WINDOW_MINIMIZE": "最小化",
"WINDOW_MAXIMIZE": "最大化",
"WINDOW_RESTORE": "向下还原",
"WINDOW_CLOSE": "关闭",
"CHANNELS_COUNT": "{{count}} 个频道",
"NO_SOURCES_AVAILABLE": "没有可用的源",
"ONE_SOURCE_AVAILABLE": "1 个可用的源",
+53
View File
@@ -2,6 +2,7 @@
"CANCEL": "取消",
"CLEAR": "清除",
"CLOSE": "關閉",
"REMOVE": "移除",
"BACK": "返回",
"SHOW_MORE": "更多",
"SHOW_LESS": "收起",
@@ -19,6 +20,9 @@
},
"PLAYLISTS": {
"NO_PLAYLISTS": "尚未新增播放清單",
"SELECT_PLAYLIST": "選擇播放清單",
"UNTITLED_PLAYLIST": "未命名的播放清單",
"PLAYLIST_ACTIONS": "播放清單操作",
"ADD_FIRST": "請從其他分頁新增您的第一個播放清單",
"WELCOME_TITLE": "歡迎使用 IPTVnator",
"WELCOME_DESCRIPTION": "透過新增您的第一個播放清單開始串流之旅。連接您喜愛的 IPTV 服務,享受直播電視、電影和影集。",
@@ -64,6 +68,12 @@
"STALKER_PORTAL": "Stalker 入口網站",
"CHANNELS": "頻道",
"CHANNELS_COUNT": "{{count}} 個頻道",
"PORTAL_STATUS": "入口網站狀態:{{status}}",
"PORTAL_STATUS_ACTIVE": "啟用中",
"PORTAL_STATUS_INACTIVE": "未啟用",
"PORTAL_STATUS_EXPIRED": "已過期",
"PORTAL_STATUS_UNAVAILABLE": "無法使用",
"PORTAL_STATUS_CHECKING": "檢查中",
"ADDED": "已新增",
"SHOW_DETAILS": "顯示播放清單詳細資訊",
"REMOVE": "移除播放清單",
@@ -91,6 +101,7 @@
"PLAYLIST_EXPORT_SUCCESS": "播放清單已成功匯出。",
"PLAYLIST_EXPORT_ERROR": "錯誤:播放清單匯出失敗。",
"EXPORT_PLAYLIST_FAILED": "播放清單匯出失敗。",
"EXPORT_FILE_TYPE": "播放清單",
"COPY_URL": "複製網址至剪貼簿",
"URL_COPIED": "已將網址複製至剪貼簿"
},
@@ -162,6 +173,7 @@
"CHOOSE_PLAYLIST": "選擇播放清單",
"BROWSE": "瀏覽檔案",
"REJECTED": "檔案被拒絕,不支援的檔案格式 {{filename}}。",
"REJECTED_UNNAMED": "檔案被拒絕,不支援的檔案格式。",
"OPEN_FAILED": "無法開啟 \"{{filename}}\"。"
},
"URL_UPLOAD": {
@@ -436,6 +448,7 @@
"EPG_OFFSET_HINT": "無需重新整理節目表即可調整顯示的節目時間。請輸入 -720 到 720 之間的分鐘數。",
"EPG_OFFSET_LABEL": "偏移(分鐘)",
"EPG_OFFSET_ERROR": "請輸入 -720 到 720 之間的整數分鐘數。",
"EPG_OFFSET_UNIT": "分鐘",
"EPG_VIEW_MODE_DESCRIPTION": "選擇播放器下方節目表的顯示方式:橫向時間軸條,或縱向的單日清單。",
"EPG_VIEW_MODE_TIMELINE": "時間軸",
"EPG_VIEW_MODE_LIST": "清單",
@@ -574,6 +587,7 @@
"APP_UPDATE_RELEASE_NOTES": "更新內容",
"APP_UPDATE_RELEASE_NOTES_NOT_FOUND": "在 GitHub 上找不到 {{version}} 的更新說明。該版本可能已被移除或尚未發布。",
"APP_UPDATE_RELEASE_NOTES_LOAD_FAILED": "無法載入更新說明。",
"APP_UPDATE_RELEASE_NOTES_UNAVAILABLE": "此版本未提供更新說明。",
"APP_UPDATE_OPEN_RELEASES_PAGE": "開啟版本發布頁面",
"APP_UPDATE_CHANNEL": "更新頻道",
"APP_UPDATE_CHANNEL_DESCRIPTION": "Stable 跟隨正式發布的版本。Nightly 跟隨每次合併到 master 的變更,並自動安裝預先發布的建置版本。儲存後生效。",
@@ -648,6 +662,7 @@
"STREAM_ENDED": "現場直播已結束。",
"CHECKING_SUPPORT": "正在檢查內嵌 MPV 支援…",
"NOT_AVAILABLE": "內嵌 MPV 在此環境中無法使用。",
"FRAME_VIEW_FAILED": "內嵌 MPV 影片畫面初始化失敗。",
"LOADING_STREAM": "正在載入串流…",
"PLAY": "播放",
"PAUSE": "暫停",
@@ -1539,6 +1554,7 @@
"RECORDING_NOW": "正在錄製",
"RECORDINGS": "錄製檔",
"RECORDING_BADGE": "錄製中",
"RECORDING_CHIP": "錄製",
"STOP_RECORDING": "停止錄製",
"REMOVE_RECORDING_DIALOG": {
"TITLE": "從管理員中移除錄製檔?",
@@ -1610,6 +1626,37 @@
"SUBTITLE": "遠端控制您的 IPTV 播放器",
"UP_CHANNEL": "上一頻道",
"DOWN_CHANNEL": "下一頻道",
"NOW_PLAYING": "現正播放",
"EXECUTING": "執行中",
"SOURCE_M3U": "M3U 直播",
"SOURCE_XTREAM": "Xtream 直播",
"SOURCE_STALKER": "Stalker 直播",
"WAITING_FOR_PLAYBACK": "正在等待播放",
"UNKNOWN_SOURCE": "未知",
"NO_STREAM": "未選擇直播串流",
"NO_PROGRAM": "目前沒有 EPG 節目",
"CHANNEL_ABBR": "頻道",
"CHANNEL_NAVIGATION": "頻道切換",
"CHANNEL_NUMBER": "頻道號碼",
"KEYPAD_DELETE": "刪除",
"KEYPAD_CLEAR": "清除",
"VOLUME": "音量",
"VOLUME_ABBR": "音量",
"MUTE": "靜音",
"UNMUTE": "取消靜音",
"VOLUME_UNAVAILABLE": "此播放器模式無法控制音量",
"SYNCING_STATUS": "正在同步狀態…",
"UPDATED_AT": "更新於 {{time}}",
"WAITING_FOR_STATUS": "正在等待狀態…",
"ERRORS": {
"CHANNEL_UP": "無法切換到上一頻道",
"CHANNEL_DOWN": "無法切換到下一頻道",
"SELECT_NUMBER": "無法依頻道號碼切換",
"VOLUME_UP": "無法調高音量",
"VOLUME_DOWN": "無法調低音量",
"TOGGLE_MUTE": "無法切換靜音",
"STATUS": "無法擷取遠端狀態"
},
"HEADER": "IPTVnator 遠端控制",
"FOOTER": "IPTVnator 遠端控制"
},
@@ -1671,6 +1718,8 @@
"SOURCE_EXPIRES_IN_DAYS": "{{days}} 天後到期",
"SOURCE_EXPIRED": "已過期",
"HERO_LABEL": "精選",
"HERO_ROLE_CAROUSEL": "輪播",
"HERO_ROLE_SLIDE": "投影片",
"HERO_FAVORITE_CHANNEL": "收藏頻道",
"HERO_FAVORITE": "收藏",
"HERO_RECENTLY_ADDED": "最近新增",
@@ -1748,6 +1797,10 @@
"CLEAR_FAVORITES_DIALOG_MESSAGE_ALL": "所有播放清單中所有的{{type}}最愛項目都將被移除。此操作無法復原。",
"XTREAM_CODE": "Xtream Code",
"STALKER_PORTAL": "Stalker Portal",
"WINDOW_MINIMIZE": "最小化",
"WINDOW_MAXIMIZE": "最大化",
"WINDOW_RESTORE": "向下還原",
"WINDOW_CLOSE": "關閉",
"CHANNELS_COUNT": "{{count}} 個頻道",
"NO_SOURCES_AVAILABLE": "沒有可用的來源",
"ONE_SOURCE_AVAILABLE": "1 個可用來源",
@@ -304,7 +304,7 @@ Implication:
Current code paths:
- `libs/portal/xtream/feature/src/lib/xtream-collection-detail.component.ts` (favorites + recent, with shared UI from `libs/portal/shared/ui/src/lib/components/favorites-layout/`)
- `libs/portal/xtream/feature/src/lib/xtream-collection-detail.component.ts` (favorites + recent)
- `libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts`
- `libs/portal/catalog/feature/src/lib/category-content-view/category-content-view.component.ts`
+5
View File
@@ -318,6 +318,11 @@ Implemented UI behavior:
- Numeric keypad (`0-9`, `DEL`, `CLR`, `OK`)
- Volume controls (`VOL-`, `MUTE/UNMUTE`, `VOL+`)
- Status card (portal, channel name/number, current program)
- Labels and errors come from the shared `REMOTE_CONTROL.*` translation keys.
The phone never sees the desktop language setting, so the app shell picks
the first browser language with a translation
(`resolveRemoteControlLanguage`, English otherwise) and sets `<html lang>`
to it.
- Polls `/status` every 2s
- Uses action wrapper to refresh status after command execution
+2 -2
View File
@@ -184,8 +184,8 @@ coverage do not decrease.
`apps/website` is an Astro marketing site. Its useful signal is a successful
static build plus targeted output checks, not a merged code coverage percentage.
Projects with a test target but no specs, such as `remote-control-web` and
`remote-control` today, should not be in Tier A until focused specs exist.
Projects with a test target but no specs, such as `remote-control-web`
today, should not be in Tier A until focused specs exist.
For local coverage inspection:
+2 -3
View File
@@ -60,7 +60,7 @@ must be ticked here.
## Eager components
66 production files, 67 components (`epg-progress-panel.component.ts` holds
65 production files, 66 components (`epg-progress-panel.component.ts` holds
two). Tick an entry by deleting `changeDetection: ChangeDetectionStrategy.Eager`
(or setting OnPush) once its template state is signals, signal inputs or
explicitly marked. The guard spec compares the unticked entries with the
@@ -146,9 +146,8 @@ picker), so each marks itself on the form's `events` through
`libs/playlist/m3u/feature-player` (2) goes with the playback PR.
### libs/portal (9)
### libs/portal (8)
- [x] `libs/portal/shared/ui/src/lib/components/favorites-layout/favorites-layout.component.ts`
- [x] `libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts`
- [x] `libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts`
- [x] `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts`
@@ -260,9 +260,13 @@ export class AddPlaylistDialogComponent {
rejectFile(filename: string): void {
this.snackBar.open(
this.translateService.instant('HOME.FILE_UPLOAD.REJECTED', {
filename,
})
filename
? this.translateService.instant('HOME.FILE_UPLOAD.REJECTED', {
filename,
})
: this.translateService.instant(
'HOME.FILE_UPLOAD.REJECTED_UNNAMED'
)
);
}
@@ -118,7 +118,8 @@ export class FileUploadComponent {
}
if (result.reason !== 'cancelled') {
this.fileRejected.emit('selected playlist');
// The native dialog does not report a file name.
this.fileRejected.emit('');
}
}
}
@@ -414,8 +414,8 @@ describe('PlaylistRefreshActionService', () => {
await Promise.resolve();
expect(snackBar.open).toHaveBeenCalledWith(
'Certificate for this playlist host is invalid.',
'Trust host',
'HOME.URL_UPLOAD.ERROR_INVALID_TLS',
'HOME.URL_UPLOAD.TRUST_TLS_HOST',
{ duration: 10000 }
);
expect(store.dispatch).toHaveBeenCalledWith(
@@ -244,14 +244,8 @@ export class PlaylistRefreshActionService {
}
const ref = this.snackBar.open(
this.translateWithFallback(
'HOME.URL_UPLOAD.ERROR_INVALID_TLS',
'Certificate for this playlist host is invalid.'
),
this.translateWithFallback(
'HOME.URL_UPLOAD.TRUST_TLS_HOST',
'Trust host'
),
this.translate.instant('HOME.URL_UPLOAD.ERROR_INVALID_TLS'),
this.translate.instant('HOME.URL_UPLOAD.TRUST_TLS_HOST'),
{ duration: 10000 }
);
ref.onAction().subscribe(() => {
@@ -266,9 +260,8 @@ export class PlaylistRefreshActionService {
): void {
if (!host) {
this.snackBar.open(
this.translateWithFallback(
'HOME.URL_UPLOAD.ERROR_TLS_HOST_UNKNOWN',
'Could not determine the playlist host. Please retry manually.'
this.translate.instant(
'HOME.URL_UPLOAD.ERROR_TLS_HOST_UNKNOWN'
),
this.translate.instant('CLOSE'),
{ duration: 5000 }
@@ -277,17 +270,14 @@ export class PlaylistRefreshActionService {
}
this.dialogService.openConfirmDialog({
title: this.translateWithFallback(
'HOME.URL_UPLOAD.TRUST_TLS_HOST_TITLE',
'Trust invalid certificate?'
title: this.translate.instant(
'HOME.URL_UPLOAD.TRUST_TLS_HOST_TITLE'
),
message: this.translateWithFallback(
'HOME.URL_UPLOAD.TRUST_TLS_HOST_WARNING',
'Only continue if you trust this playlist host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.'
message: this.translate.instant(
'HOME.URL_UPLOAD.TRUST_TLS_HOST_WARNING'
),
confirmLabel: this.translateWithFallback(
'HOME.URL_UPLOAD.TRUST_TLS_HOST',
'Trust host'
confirmLabel: this.translate.instant(
'HOME.URL_UPLOAD.TRUST_TLS_HOST'
),
width: '420px',
onConfirm: () => {
@@ -310,11 +300,6 @@ export class PlaylistRefreshActionService {
});
}
private translateWithFallback(key: string, fallback: string): string {
const translated = this.translate.instant(key);
return translated === key ? fallback : translated;
}
private updateRefreshPreparationFromEvent(
playlistId: string,
operationId: string,
@@ -192,7 +192,9 @@
[matMenuTriggerFor]="itemMenu"
[matMenuTriggerData]="{ playlist: playlist }"
(click)="$event.stopPropagation()"
aria-label="Playlist actions"
[attr.aria-label]="
'HOME.PLAYLISTS.PLAYLIST_ACTIONS' | translate
"
>
<mat-icon>more_vert</mat-icon>
</button>
@@ -418,11 +418,16 @@ describe('PlaylistSwitcherComponent', () => {
expect(component.displayTitle()).toBe('Cinema Xtream');
const translate = TestBed.inject(TranslateService);
translate.setTranslation('en', {
HOME: { PLAYLISTS: { SELECT_PLAYLIST: 'Pick a source' } },
});
translate.use('en');
resolvedPlaylistIdSignal.set(null);
activePlaylistSignal.set(null);
fixture.detectChanges();
expect(component.displayTitle()).toBe('Select playlist');
expect(component.displayTitle()).toBe('Pick a source');
});
it('delegates playlist deletion and removes the source after confirmation', async () => {
@@ -161,8 +161,9 @@ export class PlaylistSwitcherComponent {
);
});
readonly displayTitle = computed(() => {
this.languageTick();
if (!this.activePlaylistId()) {
return 'Select playlist';
return this.translate.instant('HOME.PLAYLISTS.SELECT_PLAYLIST');
}
return (
@@ -171,7 +172,7 @@ export class PlaylistSwitcherComponent {
this.activePlaylist()?.filename ||
this.activePlaylist()?.url ||
this.activePlaylist()?.portalUrl ||
'Untitled playlist'
this.translate.instant('HOME.PLAYLISTS.UNTITLED_PLAYLIST')
);
});
@@ -19,6 +19,8 @@ import {
STALKER_PLAYLIST_CONNECTION_EDITOR_STATUS,
} from './stalker-playlist-connection-editor.token';
const EXPORT_FILE_TYPE = 'HOME.PLAYLISTS.INFO_DIALOG.EXPORT_FILE_TYPE';
describe('PlaylistInfoComponent', () => {
let component: PlaylistInfoComponent;
let fixture: ComponentFixture<PlaylistInfoComponent>;
@@ -326,7 +328,7 @@ describe('PlaylistInfoComponent', () => {
expect(window.electron.saveFileDialog).toHaveBeenCalledWith(
'My Playlist.m3u8',
[{ name: 'Playlist', extensions: ['m3u8', 'm3u'] }]
[{ name: EXPORT_FILE_TYPE, extensions: ['m3u8', 'm3u'] }]
);
expect(window.electron.writeFile).toHaveBeenCalledWith(
'/tmp/export.m3u8',
@@ -820,7 +820,9 @@ export class PlaylistInfoComponent {
`${this.playlist.title || 'exported'}.m3u8`,
[
{
name: 'Playlist',
name: this.translate.instant(
'HOME.PLAYLISTS.INFO_DIALOG.EXPORT_FILE_TYPE'
),
extensions: ['m3u8', 'm3u'],
},
]
@@ -61,11 +61,15 @@
}
}
@if (showsPortalStatusDot) {
@let statusLabel =
'HOME.PLAYLISTS.PORTAL_STATUS'
| translate
: { status: (portalStatusLabelKey() | translate) };
<span
class="status-dot"
[class]="getStatusClass()"
[matTooltip]="'Portal status: ' + portalStatus()"
[attr.aria-label]="'Portal status: ' + portalStatus()"
[matTooltip]="statusLabel"
[attr.aria-label]="statusLabel"
></span>
} @else if (showsAutoRefresh) {
<mat-icon
@@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { MatIconModule } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { MockModule } from 'ng-mocks';
import {
PortalStatusService,
@@ -209,6 +209,17 @@ describe('PlaylistItemComponent', () => {
username: 'demo',
password: 'secret',
};
const translate = TestBed.inject(TranslateService);
// Not English text, so the label must come from the keys.
translate.setTranslation('en', {
HOME: {
PLAYLISTS: {
PORTAL_STATUS: 'Portalstatus: {{status}}',
PORTAL_STATUS_ACTIVE: 'aktiv',
},
},
});
translate.use('en');
// The fixture renders on its own: a forced detectChanges() after the
// await would hide a status that does not schedule a render.
fixture.autoDetectChanges();
@@ -219,7 +230,7 @@ describe('PlaylistItemComponent', () => {
'.status-dot'
);
expect(statusDot?.getAttribute('aria-label')).toBe(
'Portal status: active'
'Portalstatus: aktiv'
);
});
@@ -36,6 +36,15 @@ import {
} from '@iptvnator/services';
import type { PlaylistMeta } from '@iptvnator/shared/interfaces';
/** Translation keys for the status word in the portal status tooltip. */
const PORTAL_STATUS_LABEL_KEYS: Record<PortalStatus, string> = {
active: 'HOME.PLAYLISTS.PORTAL_STATUS_ACTIVE',
inactive: 'HOME.PLAYLISTS.PORTAL_STATUS_INACTIVE',
expired: 'HOME.PLAYLISTS.PORTAL_STATUS_EXPIRED',
unavailable: 'HOME.PLAYLISTS.PORTAL_STATUS_UNAVAILABLE',
checking: 'HOME.PLAYLISTS.PORTAL_STATUS_CHECKING',
};
@Component({
selector: 'app-playlist-item',
templateUrl: './playlist-item.component.html',
@@ -72,6 +81,9 @@ export class PlaylistItemComponent implements OnInit {
readonly cancelBusyActionClicked = output<void>();
readonly portalStatus = signal<PortalStatus>('unavailable');
readonly portalStatusLabelKey = computed(
() => PORTAL_STATUS_LABEL_KEYS[this.portalStatus()]
);
private readonly portalStatusService = inject(PortalStatusService);
readonly runtime = inject(RuntimeCapabilitiesService);
readonly sourceIcons = SOURCE_TYPE_ICONS;
@@ -652,25 +652,17 @@ export class RecentPlaylistsComponent {
private translatePlaylistRefreshPhase(phase?: string): string {
switch (phase) {
case 'fetching':
return this.translateWithFallback(
'HOME.PLAYLISTS.REFRESH_FETCHING',
'Fetching playlist...'
return this.translate.instant(
'HOME.PLAYLISTS.REFRESH_FETCHING'
);
case 'reading-file':
return this.translateWithFallback(
'HOME.PLAYLISTS.REFRESH_READING_FILE',
'Reading playlist file...'
return this.translate.instant(
'HOME.PLAYLISTS.REFRESH_READING_FILE'
);
case 'parsing':
return this.translateWithFallback(
'HOME.PLAYLISTS.REFRESH_PARSING',
'Parsing playlist...'
);
return this.translate.instant('HOME.PLAYLISTS.REFRESH_PARSING');
case 'saving':
return this.translateWithFallback(
'HOME.PLAYLISTS.REFRESH_SAVING',
'Saving playlist...'
);
return this.translate.instant('HOME.PLAYLISTS.REFRESH_SAVING');
default:
return this.translate.instant('HOME.PLAYLISTS.REFRESH');
}
@@ -686,25 +678,18 @@ export class RecentPlaylistsComponent {
);
if (/(ENOENT|no such file or directory|not found)/i.test(message)) {
return this.translateWithFallback(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_NOT_FOUND',
'Playlist refresh failed. The local file is no longer available. Check the file path or re-import the playlist.'
return this.translate.instant(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_NOT_FOUND'
);
}
if (/(EACCES|EPERM|permission denied)/i.test(message)) {
return this.translateWithFallback(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_ACCESS_ERROR',
'Playlist refresh failed. The app can no longer access the local file.'
return this.translate.instant(
'HOME.PLAYLISTS.PLAYLIST_UPDATE_FILE_ACCESS_ERROR'
);
}
}
return this.translate.instant('HOME.PLAYLISTS.PLAYLIST_UPDATE_ERROR');
}
private translateWithFallback(key: string, fallback: string): string {
const translated = this.translate.instant(key);
return translated === key ? fallback : translated;
}
}
@@ -72,7 +72,9 @@
class="recording-queue__rec-dot"
aria-hidden="true"
></span>
REC&nbsp;{{ elapsedLabel(row.item) }}
{{
'DOWNLOADS.RECORDING_CHIP' | translate
}}&nbsp;{{ elapsedLabel(row.item) }}
</span>
}
<button
@@ -10,6 +10,7 @@ import { RecordingQueueComponent } from './recording-queue.component';
const TEST_TRANSLATIONS = {
DOWNLOADS: {
RECORDING_NOW: 'Recording now',
RECORDING_CHIP: 'REC',
NEEDS_ATTENTION: 'Needs attention',
STOP_RECORDING: 'Stop recording',
REMOVE_FROM_MANAGER: 'Remove from manager',
-1
View File
@@ -3,7 +3,6 @@ export * from './lib/directives/infinite-scroll.directive';
export * from './lib/components/content-card/content-card.component';
export * from './lib/cover-titles/cover-titles.service';
export * from './lib/components/content-rail-shell/content-rail-shell.component';
export * from './lib/components/favorites-layout/favorites-layout.component';
export * from './lib/components/global-favorites-list/global-favorites-list.component';
export * from './lib/components/grid-list/grid-list.component';
export * from './lib/components/playlist-error-view/playlist-error-view.component';
@@ -74,8 +74,8 @@
mat-icon-button
type="button"
class="remove-button"
[matTooltip]="removeTooltip()"
[attr.aria-label]="removeTooltip()"
[matTooltip]="removeTooltip() || ('REMOVE' | translate)"
[attr.aria-label]="removeTooltip() || ('REMOVE' | translate)"
(click)="onRemoveClick($event)"
>
<mat-icon>close</mat-icon>
@@ -13,7 +13,7 @@ import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { normalizeDateLocale } from '@iptvnator/pipes';
import { TranslateService } from '@ngx-translate/core';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { startWith } from 'rxjs';
import { CoverTitlesService } from '../../cover-titles/cover-titles.service';
@@ -23,7 +23,7 @@ const LABELLED_CONTENT_TYPES: ReadonlySet<string> = new Set(['live', 'radio']);
@Component({
selector: 'app-content-card',
standalone: true,
imports: [DatePipe, MatIcon, MatIconButton, MatTooltip],
imports: [DatePipe, MatIcon, MatIconButton, MatTooltip, TranslatePipe],
templateUrl: './content-card.component.html',
styleUrl: './content-card.component.scss',
host: { '[class.content-card--posters-only]': 'postersOnly()' },
@@ -52,8 +52,8 @@ export class ContentCardComponent {
/** Whether to show the remove button */
readonly showRemoveButton = input<boolean>(false);
/** Tooltip text for the remove button */
readonly removeTooltip = input<string>('Remove');
/** Tooltip text for the remove button; empty means the generic "Remove" */
readonly removeTooltip = input<string>('');
/** Whether to show placeholder when no poster */
readonly showPlaceholder = input<boolean>(true);
@@ -1,77 +0,0 @@
@if (!isWorkspaceLayout) {
<div
class="sidebar"
appResizable
[minWidth]="250"
[maxWidth]="600"
[defaultWidth]="400"
storageKey="sidebar-width"
>
<div class="sidebar-header">
<app-playlist-switcher
class="switcher"
[currentTitle]="playlistTitle()"
[subtitle]="playlistSubtitle()"
/>
</div>
<app-category-view
[items]="categories()"
[selectedCategoryId]="$any(selectedCategoryId())"
(categoryClicked)="setCategoryId($event.category_id)"
/>
</div>
}
<div class="main-content">
<div class="content-header">
<div class="category-meta">
<h2 class="category-title">
{{ titleTranslationString() | translate }}
</h2>
@if (selectedCategoryId()) {
<div class="category-subtitle">{{ selectedCategoryId() }}</div>
}
</div>
@if (showHeaderAction() && !isWorkspaceLayout) {
<button
type="button"
mat-icon-button
[matTooltip]="headerActionTooltip()"
(click)="onHeaderActionClick()"
>
<mat-icon>{{ headerActionIcon() }}</mat-icon>
</button>
}
</div>
<div class="scrollable-content">
@if (favorites().length) {
<div class="items-grid">
@for (item of favorites(); track $index) {
<app-content-card
[posterUrl]="item.poster_url || item.cover"
[title]="
item?.title ||
item?.details?.info?.name ||
'Unknown'
"
[type]="item.type"
[showTypeBadge]="false"
[date]="item.added_at"
[showRemoveButton]="true"
[removeTooltip]="
removeTooltip() ||
('CHANNELS.REMOVE_FAVORITE' | translate)
"
(cardClick)="openFavorite(item)"
(remove)="removeFromFavorites(item)"
/>
}
</div>
} @else {
<div class="no-items">
<mat-icon>{{ emptyIcon() }}</mat-icon>
<p>No items in the selected category</p>
</div>
}
</div>
</div>
@@ -1,120 +0,0 @@
@use '@angular/material' as mat;
@use '../../styles/content-grid' as grid;
:host {
display: flex;
flex-direction: row;
overflow: hidden;
height: 100%;
width: 100%;
}
.main-content {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0; // Important for proper flex behavior
}
.content-header {
padding: 8px 12px;
min-height: 48px;
background: var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
border-bottom: 1px solid var(--app-separator);
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.category-meta {
min-width: 0;
flex: 1;
display: flex;
align-items: baseline;
gap: 8px;
}
.category-title {
margin: 0;
font-size: 0.94rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--mat-sys-on-surface);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Informational badge — primary-tinted pill with definition ring
.category-subtitle {
white-space: nowrap;
flex-shrink: 0;
font-size: 0.70rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: capitalize;
line-height: 1;
color: var(--mat-sys-primary);
background: color-mix(in srgb, var(--mat-sys-primary) 10%, var(--mat-sys-surface-container-high));
padding: 4px 10px;
border-radius: 999px;
box-shadow:
inset 0 0 0 1px color-mix(in srgb, var(--mat-sys-primary) 30%, transparent),
0 1px 3px rgba(0, 0, 0, 0.18);
}
.scrollable-content {
flex: 1;
overflow-y: auto;
}
.favorites-container {
padding: 20px;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.items-grid {
@include grid.content-grid;
padding: 16px;
width: 100%;
box-sizing: border-box;
margin: 0;
}
.no-items {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 2rem;
mat-icon {
font-size: 48px;
width: 48px;
height: 48px;
margin-bottom: 1rem;
}
p {
font-size: 0.85rem;
margin: 0;
opacity: 0.65;
}
}
.sidebar-header {
display: flex;
flex-direction: row;
align-items: center;
button {
@include mat.form-field-density(-5);
}
}
@@ -1,110 +0,0 @@
import {
Component,
inject,
input,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
import { ActivatedRoute } from '@angular/router';
import { PlaylistSwitcherComponent } from '@iptvnator/playlist/shared/ui';
import { isWorkspaceLayoutRoute } from '@iptvnator/portal/shared/util';
import { TranslatePipe } from '@ngx-translate/core';
import { ResizableDirective } from '@iptvnator/ui/components';
import {
StalkerPortalItem,
XtreamCategory,
} from '@iptvnator/shared/interfaces';
import { CategoryViewComponent } from '../category-view/category-view.component';
import { ContentCardComponent } from '../content-card/content-card.component';
interface FavoriteLayoutSelection {
readonly category_id?: string | number;
}
export interface FavoriteLayoutItem {
readonly added_at?: string | number;
readonly content_id?: number;
readonly category_id?: string | number;
readonly cover?: string;
readonly details?: {
readonly info?: {
readonly name?: string;
};
};
readonly id?: string | number;
readonly name?: string;
readonly o_name?: string;
readonly playlist_id?: string;
readonly playlist_name?: string;
readonly poster_url?: string;
readonly source?: 'xtream' | 'stalker' | 'm3u';
readonly stalker_item?: StalkerPortalItem;
readonly stream_icon?: string;
readonly title?: string;
readonly type?: string;
readonly viewed_at?: string;
readonly xtream_id?: string | number;
}
@Component({
selector: 'app-favorites-layout',
templateUrl: './favorites-layout.component.html',
styleUrls: [
'./favorites-layout.component.scss',
'../../styles/portal-sidebar.scss',
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
CategoryViewComponent,
ContentCardComponent,
MatIcon,
MatIconButton,
PlaylistSwitcherComponent,
ResizableDirective,
TranslatePipe,
MatTooltip,
],
})
export class FavoritesLayoutComponent {
readonly isWorkspaceLayout = isWorkspaceLayoutRoute(inject(ActivatedRoute));
readonly categories = input<XtreamCategory[]>([]);
readonly favorites = input<FavoriteLayoutItem[]>([]);
readonly playlistSubtitle = input<string>('');
readonly playlistTitle = input<string>('Playlist');
readonly selectedCategoryId = input<string>('movie');
readonly titleTranslationString = input<string>('CHANNELS.FAVORITES');
readonly removeTooltip = input<string>('');
readonly emptyIcon = input<string>('favorite_border');
readonly showHeaderAction = input<boolean>(false);
readonly headerActionIcon = input<string>('delete_sweep');
readonly headerActionTooltip = input<string>('');
readonly categoryClicked = output<FavoriteLayoutSelection>();
readonly removeFavorite = output<FavoriteLayoutItem>();
readonly openItem = output<FavoriteLayoutItem>();
readonly headerActionClicked = output<void>();
setCategoryId(categoryId: string | number | undefined) {
if (categoryId === undefined) {
return;
}
this.categoryClicked.emit({ category_id: categoryId });
}
removeFromFavorites(item: FavoriteLayoutItem) {
this.removeFavorite.emit(item);
}
openFavorite(item: FavoriteLayoutItem) {
this.openItem.emit(item);
}
onHeaderActionClick() {
this.headerActionClicked.emit();
}
}
@@ -6,7 +6,7 @@
type="search"
spellcheck="false"
autocomplete="off"
[placeholder]="placeholder"
[placeholder]="placeholder || ('PORTALS.SEARCH' | translate)"
[ngModel]="searchTerm()"
(ngModelChange)="onSearchTermChange()"
/>
@@ -89,7 +89,8 @@ export interface SearchFilter {
})
export class SearchFormComponent {
@ViewChild('searchInput') searchInput!: ElementRef;
@Input() placeholder = 'Search';
/** Empty means the generic "Search" label */
@Input() placeholder = '';
@Input() filters: SearchFilters = {};
@Input() filterConfig: SearchFilter[] = [];
@Input() singleSelection = false; // Add this new input
@@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { TranslatePipe } from '@ngx-translate/core';
import { TranslateModule, TranslatePipe } from '@ngx-translate/core';
import { MockPipe } from 'ng-mocks';
import { WorkspaceBackNavigationService } from '@iptvnator/portal/shared/data-access';
import { SearchLayoutComponent } from './search-layout.component';
@@ -10,7 +10,8 @@ describe('SearchLayoutComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [SearchLayoutComponent],
// The search form inside renders the real pipe.
imports: [SearchLayoutComponent, TranslateModule.forRoot()],
})
.overrideComponent(SearchLayoutComponent, {
remove: { imports: [TranslatePipe] },
@@ -237,7 +237,8 @@ export function mapVodSeriesEpisodes(
...createBaseEpisode(
trackingId,
episodeNum,
episode.name || `Episode ${episodeNum}`,
// No English stand-in: the view labels unnamed episodes.
episode.name || '',
'mpg',
'vod-series',
seasonNum,
@@ -286,7 +287,9 @@ export function mapRegularSeriesEpisodes(
...createBaseEpisode(
trackingId,
episodeNum,
`Episode ${episodeNum}`,
// Regular series episodes carry no names; the view
// labels them.
'',
'',
'regular-series',
Number(seasonKey),
@@ -202,7 +202,7 @@
controlledChannel()?.name ?? activePlaybackTitle()
"
[channelLogo]="activePlaybackArtwork()"
[sourceLabel]="'Stalker Portal'"
[sourceLabel]="'WORKSPACE.SHELL.STALKER_PORTAL' | translate"
[archivePlaybackAvailable]="false"
[archiveDays]="0"
[loading]="isLoadingEpg()"
@@ -225,7 +225,7 @@
controlledChannel()?.name ?? activePlaybackTitle()
"
[channelLogo]="activePlaybackArtwork()"
[sourceLabel]="'Stalker Portal'"
[sourceLabel]="'WORKSPACE.SHELL.STALKER_PORTAL' | translate"
[archivePlaybackAvailable]="false"
[archiveDays]="0"
[loading]="isLoadingEpg()"
@@ -0,0 +1,43 @@
import { XtreamSerieEpisode } from '@iptvnator/shared/interfaces';
import { withEpisodeTitleFallback } from './stalker-series-episode-titles';
const episode = (
episodeNum: number,
title: string
): XtreamSerieEpisode & { originalCmd: string } => ({
id: String(episodeNum),
episode_num: episodeNum,
title,
container_extension: '',
info: {},
custom_sid: 'regular-series',
added: '',
season: 1,
direct_source: '',
originalCmd: `cmd-${episodeNum}`,
});
describe('withEpisodeTitleFallback', () => {
const label = (episodeNumber: number) => `Folge ${episodeNumber}`;
it('labels unnamed episodes and keeps the provider fields', () => {
const result = withEpisodeTitleFallback(
{ '1': [episode(1, ''), episode(2, ' ')] },
label
);
expect(result['1'].map((item) => item.title)).toEqual([
'Folge 1',
'Folge 2',
]);
expect(result['1'][0]).toMatchObject({ originalCmd: 'cmd-1' });
});
it('leaves named episodes untouched', () => {
const named = episode(3, 'A real name');
const result = withEpisodeTitleFallback({ '2': [named] }, label);
expect(result['2'][0]).toBe(named);
});
});
@@ -0,0 +1,26 @@
import { XtreamSerieEpisode } from '@iptvnator/shared/interfaces';
/**
* Portals often send episodes without a name. Data access leaves those titles
* empty because it cannot translate, TMDB fills the ones it knows, and the
* rest get the label for "Episode N" here. Run it after the TMDB overlay: the
* overlay only replaces empty or generic English titles, so a translated
* label would keep the real TMDB name out.
*/
export function withEpisodeTitleFallback(
seasons: Record<string, XtreamSerieEpisode[]>,
episodeLabel: (episodeNumber: number) => string
): Record<string, XtreamSerieEpisode[]> {
const labelled: Record<string, XtreamSerieEpisode[]> = {};
for (const [seasonKey, episodes] of Object.entries(seasons)) {
labelled[seasonKey] = episodes.map((episode) =>
episode.title?.trim()
? episode
: {
...episode,
title: episodeLabel(Number(episode.episode_num)),
}
);
}
return labelled;
}
@@ -57,7 +57,11 @@ describe('StalkerSeriesViewComponent', () => {
expect(quickStartButton).not.toBeNull();
expect(quickStartButton?.textContent).toContain('XTREAM.PLAY');
expect(quickStartButton?.textContent).toContain('S01E01 · Episode 1');
// Regular series episodes have no names: the view labels them
// (the harness translates a key to itself).
expect(quickStartButton?.textContent).toContain(
'S01E01 · PORTALS.DETAIL.EPISODE_NUMBER'
);
quickStartButton?.click();
await fixture.whenStable();
@@ -12,9 +12,12 @@ import {
viewChild,
} from '@angular/core';
import { Location } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Router } from '@angular/router';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { startWith } from 'rxjs';
import { withEpisodeTitleFallback } from './stalker-series-episode-titles';
import { FavoritesButtonComponent } from '../stalker-favorites-button/stalker-favorites-button.component';
import {
findStalkerResumeLazySeason,
@@ -183,6 +186,10 @@ export class StalkerSeriesViewComponent implements OnDestroy {
);
private readonly snackBar = inject(MatSnackBar);
private readonly translateService = inject(TranslateService);
private readonly languageTick = toSignal(
this.translateService.onLangChange.pipe(startWith(null)),
{ initialValue: null }
);
readonly backClicked = output<void>();
private readonly logger = createLogger('StalkerSeriesView');
readonly inlinePlayback = signal<ResolvedPortalPlayback | null>(null);
@@ -719,7 +726,14 @@ export class StalkerSeriesViewComponent implements OnDestroy {
// Overlay lazily fetched TMDB episode data (real names,
// overviews, stills) — a no-op while nothing is fetched
return this.tmdbSeasons.overlay(base, displayItem?.info?.tmdb_id);
const overlaid = this.tmdbSeasons.overlay(
base,
displayItem?.info?.tmdb_id
);
this.languageTick();
return withEpisodeTitleFallback(overlaid, (episode) =>
this.episodeLabel(episode)
);
}
);
@@ -961,7 +975,7 @@ export class StalkerSeriesViewComponent implements OnDestroy {
? `/media/file_${mappedEpisode.originalId ?? ''}.mpg`
: mappedEpisode.originalCmd;
const title = isLazyVod
? `${item.info.name} - ${mappedEpisode.title || `Episode ${episodeState.episodeNumber}`}`
? `${item.info.name} - ${mappedEpisode.title || this.episodeLabel(episodeState.episodeNumber)}`
: item.info.name;
const trackingId = Number(mappedEpisode.id);
const startTime = fromStart
@@ -1072,6 +1086,12 @@ export class StalkerSeriesViewComponent implements OnDestroy {
return toStalkerSeriesId(id);
}
private episodeLabel(episode: number): string {
return this.translateService.instant('PORTALS.DETAIL.EPISODE_NUMBER', {
episode,
});
}
closeInlinePlayer(): void {
this.seriesPlaybackRequestGeneration += 1;
this.inlinePlayback.set(null);
@@ -342,29 +342,6 @@ export class PortalStatusService {
return { status: 'unavailable', expiresAtSeconds: null };
}
/**
* Gets a user-friendly message based on the portal status
*
* @param status The portal status
* @returns A message describing the status
*/
getStatusMessage(status: PortalStatus | null): string {
switch (status) {
case 'active':
return 'Connection successful! Portal is active.';
case 'inactive':
return 'Portal is inactive.';
case 'expired':
return 'Portal subscription has expired.';
case 'unavailable':
return 'Could not connect to the portal.';
case 'checking':
return 'Checking portal status…';
default:
return '';
}
}
/**
* Gets a CSS class name based on the portal status
*
@@ -2,8 +2,8 @@
type="button"
class="window-control"
data-test-id="window-minimize"
aria-label="Minimize"
title="Minimize"
[attr.aria-label]="'WORKSPACE.SHELL.WINDOW_MINIMIZE' | translate"
[attr.title]="'WORKSPACE.SHELL.WINDOW_MINIMIZE' | translate"
(click)="onMinimize()"
>
<svg viewBox="0 0 10 10" class="crisp" aria-hidden="true">
@@ -15,8 +15,8 @@
type="button"
class="window-control"
data-test-id="window-maximize"
[attr.aria-label]="isMaximized() ? 'Restore' : 'Maximize'"
[attr.title]="isMaximized() ? 'Restore' : 'Maximize'"
[attr.aria-label]="maximizeLabelKey() | translate"
[attr.title]="maximizeLabelKey() | translate"
(click)="onToggleMaximize()"
>
@if (isMaximized()) {
@@ -45,8 +45,8 @@
type="button"
class="window-control close"
data-test-id="window-close"
aria-label="Close"
title="Close"
[attr.aria-label]="'WORKSPACE.SHELL.WINDOW_CLOSE' | translate"
[attr.title]="'WORKSPACE.SHELL.WINDOW_CLOSE' | translate"
(click)="onClose()"
>
<svg viewBox="0 0 10 10" aria-hidden="true">
@@ -1,5 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ElectronBridgeWindowState } from '@iptvnator/shared/interfaces';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { WindowControlsComponent } from './window-controls.component';
describe('WindowControlsComponent', () => {
@@ -42,9 +43,22 @@ describe('WindowControlsComponent', () => {
(window as { electron?: unknown }).electron = electronMock;
await TestBed.configureTestingModule({
imports: [WindowControlsComponent],
imports: [WindowControlsComponent, TranslateModule.forRoot()],
}).compileComponents();
const translate = TestBed.inject(TranslateService);
translate.setTranslation('de', {
WORKSPACE: {
SHELL: {
WINDOW_MINIMIZE: 'Minimieren',
WINDOW_MAXIMIZE: 'Maximieren',
WINDOW_RESTORE: 'Wiederherstellen',
WINDOW_CLOSE: 'Schließen',
},
},
});
translate.use('de');
fixture = TestBed.createComponent(WindowControlsComponent);
fixture.detectChanges();
});
@@ -62,6 +76,17 @@ describe('WindowControlsComponent', () => {
expect(electronMock.getWindowState).toHaveBeenCalled();
});
it('labels every button in the active language', () => {
for (const [testId, label] of [
['window-minimize', 'Minimieren'],
['window-maximize', 'Maximieren'],
['window-close', 'Schließen'],
]) {
expect(query(testId)?.getAttribute('aria-label')).toBe(label);
expect(query(testId)?.getAttribute('title')).toBe(label);
}
});
it('calls the bridge methods when the buttons are clicked', () => {
query('window-minimize')?.click();
expect(electronMock.minimizeWindow).toHaveBeenCalledTimes(1);
@@ -80,7 +105,7 @@ describe('WindowControlsComponent', () => {
expect(query('window-restore-glyph')).not.toBeNull();
expect(query('window-maximize-glyph')).toBeNull();
expect(query('window-maximize')?.getAttribute('aria-label')).toBe(
'Restore'
'Wiederherstellen'
);
});
@@ -2,12 +2,14 @@ import {
AfterViewInit,
ChangeDetectionStrategy,
Component,
computed,
DestroyRef,
ElementRef,
inject,
signal,
} from '@angular/core';
import { ElectronBridgeWindowState } from '@iptvnator/shared/interfaces';
import { TranslatePipe } from '@ngx-translate/core';
/**
* Renderer-drawn window-management buttons (minimize / maximize-restore /
@@ -25,6 +27,7 @@ import { ElectronBridgeWindowState } from '@iptvnator/shared/interfaces';
*/
@Component({
selector: 'app-window-controls',
imports: [TranslatePipe],
templateUrl: './window-controls.component.html',
styleUrl: './window-controls.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -36,6 +39,11 @@ import { ElectronBridgeWindowState } from '@iptvnator/shared/interfaces';
export class WindowControlsComponent implements AfterViewInit {
readonly isMaximized = signal(false);
readonly isFullScreen = signal(false);
readonly maximizeLabelKey = computed(() =>
this.isMaximized()
? 'WORKSPACE.SHELL.WINDOW_RESTORE'
: 'WORKSPACE.SHELL.WINDOW_MAXIMIZE'
);
private readonly host =
inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
@@ -158,13 +158,10 @@ export class EpgProgressPanelComponent {
item.errorCode ===
ELECTRON_BRIDGE_SECURITY_ERROR_CODES.EpgPrivateNetworkBlocked
) {
return this.translateWithFallback(
'EPG.ALLOW_PRIVATE_SOURCE',
'Allow source'
);
return this.translate.instant('EPG.ALLOW_PRIVATE_SOURCE');
}
return this.translateWithFallback('EPG.TRUST_TLS_HOST', 'Trust host');
return this.translate.instant('EPG.TRUST_TLS_HOST');
}
confirmTrust(item: EpgImportProgress): void {
@@ -174,17 +171,14 @@ export class EpgProgressPanelComponent {
) {
this.openTrustDialog(
{
title: this.translateWithFallback(
'EPG.ALLOW_PRIVATE_SOURCE_TITLE',
'Allow private-network EPG source?'
title: this.translate.instant(
'EPG.ALLOW_PRIVATE_SOURCE_TITLE'
),
message: this.translateWithFallback(
'EPG.ALLOW_PRIVATE_SOURCE_WARNING',
'Only allow this if you trust the EPG source. IPTVnator will let this exact EPG URL connect to private or local network addresses.'
message: this.translate.instant(
'EPG.ALLOW_PRIVATE_SOURCE_WARNING'
),
confirmLabel: this.translateWithFallback(
'EPG.ALLOW_PRIVATE_SOURCE',
'Allow source'
confirmLabel: this.translate.instant(
'EPG.ALLOW_PRIVATE_SOURCE'
),
},
() => {
@@ -198,18 +192,9 @@ export class EpgProgressPanelComponent {
this.openTrustDialog(
{
title: this.translateWithFallback(
'EPG.TRUST_TLS_HOST_TITLE',
'Trust invalid certificate?'
),
message: this.translateWithFallback(
'EPG.TRUST_TLS_HOST_WARNING',
'Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.'
),
confirmLabel: this.translateWithFallback(
'EPG.TRUST_TLS_HOST',
'Trust host'
),
title: this.translate.instant('EPG.TRUST_TLS_HOST_TITLE'),
message: this.translate.instant('EPG.TRUST_TLS_HOST_WARNING'),
confirmLabel: this.translate.instant('EPG.TRUST_TLS_HOST'),
},
() => {
void this.epgProgress.trustInsecureTlsHostAndRetry(
@@ -236,9 +221,4 @@ export class EpgProgressPanelComponent {
}
});
}
private translateWithFallback(key: string, fallback: string): string {
const translated = this.translate.instant(key);
return translated === key ? fallback : translated;
}
}
@@ -315,7 +315,7 @@ export class EmbeddedMpvControlsAdapter implements PlayerController {
return {
status: 'idle',
statusMessage:
support.reason ??
support.reason ||
this.translate.instant('EMBEDDED_MPV.PLAYER.NOT_AVAILABLE'),
};
}
@@ -372,7 +372,7 @@ export class EmbeddedMpvControlsAdapter implements PlayerController {
return {
status: 'error',
statusMessage:
session.error ??
session.error ||
this.translate.instant(
'EMBEDDED_MPV.PLAYER.PLAYBACK_FAILED'
),
@@ -224,7 +224,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
}
if (session?.status === 'error') {
return (
session.error ??
session.error ||
this.translate.instant('EMBEDDED_MPV.PLAYER.PLAYBACK_FAILED')
);
}
@@ -238,7 +238,7 @@ export class EmbeddedMpvPlayerComponent implements OnDestroy {
}
if (!this.isSupported()) {
return (
this.support()?.reason ??
this.support()?.reason ||
this.translate.instant('EMBEDDED_MPV.PLAYER.NOT_AVAILABLE')
);
}
@@ -3,6 +3,7 @@ import {
EmbeddedMpvSession,
ResolvedPortalPlayback,
} from '@iptvnator/shared/interfaces';
import { TranslateModule } from '@ngx-translate/core';
import { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
/**
@@ -105,6 +106,7 @@ describe('EmbeddedMpvSessionController devicePixelRatio watch', () => {
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
providers: [EmbeddedMpvSessionController],
});
});
@@ -5,6 +5,7 @@ import {
EmbeddedMpvSession,
ResolvedPortalPlayback,
} from '@iptvnator/shared/interfaces';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
describe('EmbeddedMpvSessionController (lifecycle & support edges)', () => {
@@ -68,6 +69,7 @@ describe('EmbeddedMpvSessionController (lifecycle & support edges)', () => {
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
providers: [EmbeddedMpvSessionController],
});
});
@@ -83,10 +85,10 @@ describe('EmbeddedMpvSessionController (lifecycle & support edges)', () => {
setBridge(undefined);
const controller = TestBed.inject(EmbeddedMpvSessionController);
// No English reason: the player falls back to its translated text.
expect(controller.support()).toEqual({
supported: false,
platform: 'unknown',
reason: 'Embedded MPV requires the Electron desktop build.',
});
});
@@ -208,6 +210,64 @@ describe('EmbeddedMpvSessionController (lifecycle & support edges)', () => {
expect(electron.createEmbeddedMpvSession).not.toHaveBeenCalled();
});
describe('its own failure messages', () => {
beforeEach(() => {
const translate = TestBed.inject(TranslateService);
translate.setTranslation('de', {
EMBEDDED_MPV: {
PLAYER: {
NOT_AVAILABLE: 'Eingebettetes MPV ist nicht verfügbar.',
FRAME_VIEW_FAILED:
'Die MPV-Videoansicht konnte nicht starten.',
},
},
});
translate.use('de');
});
it('are translated when prepare reports unsupported without a reason', async () => {
electron.prepareEmbeddedMpv.mockResolvedValueOnce({
supported: false,
platform: 'darwin',
});
const controller = TestBed.inject(EmbeddedMpvSessionController);
controller.startSession(createHost(), createPlayback(), 0.5);
await waitFor(
() => controller.session()?.status === 'error',
'error session to be set'
);
expect(controller.session()?.error).toBe(
'Eingebettetes MPV ist nicht verfügbar.'
);
});
it('are translated when the frame view fails to attach', async () => {
const frameCopySupport = createSupport('frame-copy');
electron.getEmbeddedMpvSupport.mockResolvedValueOnce(
frameCopySupport
);
electron.prepareEmbeddedMpv.mockResolvedValueOnce(frameCopySupport);
electron.attachEmbeddedMpvFrameView.mockResolvedValueOnce(false);
const controller = TestBed.inject(EmbeddedMpvSessionController);
await waitFor(
() => controller.support() === frameCopySupport,
'frame-copy support probe to resolve'
);
controller.startSession(createHost(), createPlayback(), 0.5);
await waitFor(
() => controller.session()?.status === 'error',
'error session to be set'
);
expect(controller.session()?.error).toBe(
'Die MPV-Videoansicht konnte nicht starten.'
);
});
});
it('retry clears session state, stall flag, and bumps the retry token', () => {
const controller = TestBed.inject(EmbeddedMpvSessionController);
controller.sessionId.set('mpv-1');
@@ -4,6 +4,7 @@ import {
EmbeddedMpvSession,
ResolvedPortalPlayback,
} from '@iptvnator/shared/interfaces';
import { TranslateModule } from '@ngx-translate/core';
import { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
/**
@@ -71,6 +72,7 @@ describe('EmbeddedMpvSessionController position drift poll', () => {
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
providers: [EmbeddedMpvSessionController],
});
});
@@ -3,6 +3,7 @@ import {
EmbeddedMpvSession,
ResolvedPortalPlayback,
} from '@iptvnator/shared/interfaces';
import { TranslateModule } from '@ngx-translate/core';
import { EmbeddedMpvSessionController } from './embedded-mpv-session-controller';
describe('EmbeddedMpvSessionController', () => {
@@ -100,6 +101,7 @@ describe('EmbeddedMpvSessionController', () => {
});
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
providers: [EmbeddedMpvSessionController],
});
});
@@ -17,6 +17,7 @@ import {
ResolvedPortalPlayback,
watchEmbeddedMpvSupport,
} from '@iptvnator/shared/interfaces';
import { TranslateService } from '@ngx-translate/core';
import { EmbeddedMpvCommandRunner } from './embedded-mpv-command-runner';
import { measureBounds } from './embedded-mpv-format.utils';
import {
@@ -80,6 +81,7 @@ export class EmbeddedMpvSessionController {
private readonly destroyRef = inject(DestroyRef);
private readonly zone = inject(NgZone);
private readonly translate = inject(TranslateService);
private readonly unsubscribeSessionUpdate?: () => void;
private boundsProvider: EmbeddedMpvBoundsProvider = (host) =>
@@ -100,10 +102,10 @@ export class EmbeddedMpvSessionController {
if (typeof window.electron?.getEmbeddedMpvSupport === 'function') {
stopSupportWatch = this.watchSupport();
} else {
// No reason: the player shows its translated "not available".
this.support.set({
supported: false,
platform: typeof window === 'undefined' ? 'web' : 'unknown',
reason: 'Embedded MPV requires the Electron desktop build.',
});
}
@@ -247,9 +249,7 @@ export class EmbeddedMpvSessionController {
const electron = this.getElectronBridge();
if (!electron) {
throw new Error(
'Embedded MPV requires the Electron desktop build.'
);
throw new Error(this.notAvailableMessage());
}
const prepared = await electron.prepareEmbeddedMpv?.();
@@ -257,10 +257,7 @@ export class EmbeddedMpvSessionController {
return;
}
if (prepared && !prepared.supported) {
throw new Error(
prepared.reason ??
'Embedded MPV is not available in this environment.'
);
throw new Error(prepared.reason || this.notAvailableMessage());
}
const created = await electron.createEmbeddedMpvSession(
@@ -298,7 +295,9 @@ export class EmbeddedMpvSessionController {
.disposeEmbeddedMpvSession(created.id)
.catch(() => undefined);
throw new Error(
'The embedded MPV frame view failed to initialize.'
this.translate.instant(
'EMBEDDED_MPV.PLAYER.FRAME_VIEW_FAILED'
)
);
}
}
@@ -394,9 +393,10 @@ export class EmbeddedMpvSessionController {
() => {
const electron = this.getElectronBridge();
if (!electron?.getEmbeddedMpvSupport) {
throw new Error(
'Embedded MPV requires the Electron desktop build.'
);
return Promise.resolve({
supported: false,
platform: electron?.platform ?? 'unknown',
});
}
return electron.getEmbeddedMpvSupport();
},
@@ -414,4 +414,8 @@ export class EmbeddedMpvSessionController {
private getElectronBridge(): ElectronBridge | undefined {
return window.electron;
}
private notAvailableMessage(): string {
return this.translate.instant('EMBEDDED_MPV.PLAYER.NOT_AVAILABLE');
}
}
+1
View File
@@ -1,2 +1,3 @@
export * from './lib/remote-control/remote-control.component';
export * from './lib/remote-control/remote-control.service';
export * from './lib/remote-control/remote-control-language';
@@ -0,0 +1,59 @@
import { readdirSync } from 'node:fs';
import { resolve } from 'node:path';
import {
REMOTE_CONTROL_TRANSLATIONS,
resolveRemoteControlLanguage,
} from './remote-control-language';
describe('resolveRemoteControlLanguage', () => {
it.each([
[['de-DE', 'en'], 'de', 'de-DE'],
[['fr'], 'fr', 'fr'],
[['zh-TW'], 'zhtw', 'zh-TW'],
[['zh-Hant-HK'], 'zhtw', 'zh-Hant-HK'],
[['zh-CN'], 'zh', 'zh-CN'],
[['be-BY'], 'by', 'be-BY'],
[['ar-MA'], 'ary', 'ar-MA'],
[['ar-EG'], 'ar', 'ar-EG'],
[['pt_BR'], 'pt', 'pt_BR'],
])(
'maps %j to the %s translation',
(languages, translation, documentLanguage) => {
expect(resolveRemoteControlLanguage(languages)).toEqual({
translation,
documentLanguage,
});
}
);
it('skips languages without a translation', () => {
expect(resolveRemoteControlLanguage(['sv-SE', 'ru-RU'])).toEqual({
translation: 'ru',
documentLanguage: 'ru-RU',
});
});
it('falls back to English', () => {
expect(resolveRemoteControlLanguage(['sv-SE'])).toEqual({
translation: 'en',
documentLanguage: 'en',
});
expect(resolveRemoteControlLanguage([])).toEqual({
translation: 'en',
documentLanguage: 'en',
});
});
it('lists exactly the translation files the page loads', () => {
const directory = resolve(
__dirname,
'../../../../../../apps/web/src/assets/i18n'
);
const files = readdirSync(directory)
.filter((file) => file.endsWith('.json'))
.map((file) => file.replace(/\.json$/, ''))
.sort();
expect([...REMOTE_CONTROL_TRANSLATIONS].sort()).toEqual(files);
});
});
@@ -0,0 +1,72 @@
/**
* Translation files the page loads from `apps/web/src/assets/i18n` (copied
* by the build). Kept here instead of importing the app's `Language` enum:
* a dependency on shared-interfaces pulls its build cycle into this app's
* build. The spec checks the list against the files.
*/
export const REMOTE_CONTROL_TRANSLATIONS: readonly string[] = [
'ar',
'ary',
'by',
'de',
'el',
'en',
'es',
'fr',
'hu',
'it',
'ja',
'ko',
'nl',
'pl',
'pt',
'ru',
'tr',
'zh',
'zhtw',
];
const TRANSLATED_LANGUAGES: ReadonlySet<string> = new Set(
REMOTE_CONTROL_TRANSLATIONS
);
const TRADITIONAL_CHINESE_SUBTAGS = new Set(['hant', 'tw', 'hk', 'mo']);
export interface RemoteControlLanguage {
/** Translation file to load, an app language code such as `zhtw`. */
translation: string;
/** BCP 47 tag for `<html lang>`. */
documentLanguage: string;
}
/**
* The remote page opens in the phone's browser, which never sees the desktop
* app's language setting. It follows the browser's preferred languages
* instead: the first one with a translation wins, otherwise English.
*/
export function resolveRemoteControlLanguage(
browserLanguages: readonly string[]
): RemoteControlLanguage {
for (const tag of browserLanguages) {
const translation = toTranslationCode(tag);
if (translation) {
return { translation, documentLanguage: tag };
}
}
return { translation: 'en', documentLanguage: 'en' };
}
function toTranslationCode(tag: string): string | null {
const [language, ...subtags] = tag.trim().toLowerCase().split(/[-_]/);
if (language === 'zh') {
return subtags.some((subtag) => TRADITIONAL_CHINESE_SUBTAGS.has(subtag))
? 'zhtw'
: 'zh';
}
if (language === 'ar' && subtags.includes('ma')) {
return 'ary';
}
if (language === 'be') {
return 'by';
}
return TRANSLATED_LANGUAGES.has(language) ? language : null;
}
@@ -2,56 +2,78 @@
<div class="remote__body">
<div class="now-card">
<div class="now-card__top">
<div class="now-card__label">Now Playing</div>
<div class="now-card__label">
{{ 'REMOTE_CONTROL.NOW_PLAYING' | translate }}
</div>
<div class="now-card__statusline">
<span class="status-dot" [class.is-active]="isReady"></span>
<span>{{ isLoading ? 'Executing' : portalLabel }}</span>
<span>{{
(isLoading
? 'REMOTE_CONTROL.EXECUTING'
: portalLabelKey
) | translate
}}</span>
</div>
</div>
<div class="now-card__channel">
{{ status?.channelName || 'No live stream selected' }}
{{
status?.channelName ||
('REMOTE_CONTROL.NO_STREAM' | translate)
}}
</div>
<div class="now-card__meta">
<span>
@if (status?.channelNumber) {
CH {{ status?.channelNumber }}
} @else {
CH -
}
{{ 'REMOTE_CONTROL.CHANNEL_ABBR' | translate }}
{{ status?.channelNumber || '-' }}
</span>
<span>{{ status?.portal || 'unknown' }}</span>
<span>{{
hasKnownPortal
? status?.portal
: ('REMOTE_CONTROL.UNKNOWN_SOURCE' | translate)
}}</span>
</div>
<div class="now-card__program">
{{ status?.epgTitle || 'No current EPG program' }}
{{
status?.epgTitle ||
('REMOTE_CONTROL.NO_PROGRAM' | translate)
}}
</div>
</div>
@if (error) {
<div class="remote__error">
{{ error }}
{{ error | translate }}
</div>
}
<div class="cluster">
<div class="cluster__title">Channel Navigation</div>
<div class="cluster__title">
{{ 'REMOTE_CONTROL.CHANNEL_NAVIGATION' | translate }}
</div>
<div class="touchpad__glass">
<button
class="touchpad__button touchpad__button--up"
(click)="changeChannelUp()"
[disabled]="isLoading"
[class.is-loading]="isLoading"
aria-label="Channel Up"
[attr.aria-label]="'REMOTE_CONTROL.UP_CHANNEL' | translate"
>
<span>CH +</span>
<span
>{{ 'REMOTE_CONTROL.CHANNEL_ABBR' | translate }} +</span
>
</button>
<button
class="touchpad__button touchpad__button--down"
(click)="changeChannelDown()"
[disabled]="isLoading"
[class.is-loading]="isLoading"
aria-label="Channel Down"
[attr.aria-label]="
'REMOTE_CONTROL.DOWN_CHANNEL' | translate
"
>
<span>CH -</span>
<span
>{{ 'REMOTE_CONTROL.CHANNEL_ABBR' | translate }} -</span
>
</button>
<div class="touchpad__center">
<span>{{ status?.channelNumber || '-' }}</span>
@@ -60,7 +82,9 @@
</div>
<div class="cluster">
<div class="cluster__title">Channel Number</div>
<div class="cluster__title">
{{ 'REMOTE_CONTROL.CHANNEL_NUMBER' | translate }}
</div>
<div class="digit-display">{{ numericInput || '----' }}</div>
<div class="keypad">
@for (digit of digits; track digit) {
@@ -77,14 +101,14 @@
(click)="backspaceDigit()"
[disabled]="isLoading"
>
DEL
{{ 'REMOTE_CONTROL.KEYPAD_DELETE' | translate }}
</button>
<button
class="keypad__key keypad__key--alt"
(click)="clearDigits()"
[disabled]="isLoading"
>
CLR
{{ 'REMOTE_CONTROL.KEYPAD_CLEAR' | translate }}
</button>
<button
class="keypad__key keypad__key--go"
@@ -97,32 +121,35 @@
</div>
<div class="cluster">
<div class="cluster__title">Volume</div>
<div class="cluster__title">
{{ 'REMOTE_CONTROL.VOLUME' | translate }}
</div>
<div class="volume-row">
<button
class="volume-row__button"
(click)="volumeDown()"
[disabled]="isLoading || !status?.supportsVolume"
>
VOL -
{{ 'REMOTE_CONTROL.VOLUME_ABBR' | translate }} -
</button>
<button
class="volume-row__button volume-row__button--mute"
(click)="toggleMute()"
[disabled]="isLoading || !status?.supportsVolume"
>
@if (status?.muted) {
UNMUTE
} @else {
MUTE
}
{{
(status?.muted
? 'REMOTE_CONTROL.UNMUTE'
: 'REMOTE_CONTROL.MUTE'
) | translate
}}
</button>
<button
class="volume-row__button"
(click)="volumeUp()"
[disabled]="isLoading || !status?.supportsVolume"
>
VOL +
{{ 'REMOTE_CONTROL.VOLUME_ABBR' | translate }} +
</button>
</div>
<div class="volume-meter">
@@ -135,22 +162,27 @@
@if (status?.supportsVolume) {
{{ volumePercent ?? 0 }}%
} @else {
Volume control unavailable for this player mode
{{ 'REMOTE_CONTROL.VOLUME_UNAVAILABLE' | translate }}
}
</div>
</div>
<div class="remote__footer">
@if (isStatusLoading) {
Syncing status...
{{ 'REMOTE_CONTROL.SYNCING_STATUS' | translate }}
} @else if (status?.updatedAt) {
Updated
{{
$safeNavigationMigration(status?.updatedAt)
| date: 'HH:mm:ss'
'REMOTE_CONTROL.UPDATED_AT'
| translate
: {
time:
($safeNavigationMigration(
status?.updatedAt
) | date: 'HH:mm:ss'),
}
}}
} @else {
Waiting for status...
{{ 'REMOTE_CONTROL.WAITING_FOR_STATUS' | translate }}
}
</div>
</div>
@@ -0,0 +1,122 @@
import { ChangeDetectorRef } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { RemoteControlComponent } from './remote-control.component';
import {
RemoteControlService,
RemoteControlStatus,
} from './remote-control.service';
const GERMAN = {
REMOTE_CONTROL: {
NOW_PLAYING: 'Läuft gerade',
SOURCE_XTREAM: 'Xtream live',
WAITING_FOR_PLAYBACK: 'Warte auf Wiedergabe',
UNKNOWN_SOURCE: 'unbekannt',
NO_STREAM: 'Kein Livestream ausgewählt',
NO_PROGRAM: 'Keine aktuelle Sendung',
CHANNEL_ABBR: 'K',
UP_CHANNEL: 'Kanal hoch',
MUTE: 'STUMM',
VOLUME_UNAVAILABLE: 'Lautstärke nicht verfügbar',
WAITING_FOR_STATUS: 'Warte auf Status…',
ERRORS: { CHANNEL_UP: 'Kanalwechsel fehlgeschlagen' },
},
};
describe('RemoteControlComponent', () => {
let fixture: ComponentFixture<RemoteControlComponent>;
let service: {
getStatus: jest.Mock<Promise<RemoteControlStatus>>;
channelUp: jest.Mock<Promise<void>>;
};
const text = (selector: string): string =>
(
fixture.nativeElement.querySelector(selector) as HTMLElement | null
)?.textContent?.trim() ?? '';
beforeEach(async () => {
jest.useFakeTimers();
service = {
getStatus: jest
.fn()
.mockResolvedValue({ portal: 'unknown', isLiveView: false }),
channelUp: jest.fn().mockRejectedValue(new Error('offline')),
};
await TestBed.configureTestingModule({
imports: [RemoteControlComponent, TranslateModule.forRoot()],
providers: [{ provide: RemoteControlService, useValue: service }],
}).compileComponents();
const translate = TestBed.inject(TranslateService);
translate.setTranslation('de', GERMAN);
translate.use('de');
jest.spyOn(console, 'error').mockImplementation(() => undefined);
// The first status answer stays pending through the first render.
let answerFirstStatus!: (status: RemoteControlStatus) => void;
service.getStatus.mockReturnValueOnce(
new Promise((resolve) => (answerFirstStatus = resolve))
);
fixture = TestBed.createComponent(RemoteControlComponent);
fixture.detectChanges();
answerFirstStatus({ portal: 'unknown', isLiveView: false });
await settle();
});
/**
* The component keeps plain fields, so in this zoneless test bed a
* resolved status request neither schedules nor dirties a render (the
* app itself runs Zone.js): flush it on the fake clock, mark the view,
* then render.
*/
async function settle(milliseconds = 0): Promise<void> {
await jest.advanceTimersByTimeAsync(milliseconds);
fixture.debugElement.injector.get(ChangeDetectorRef).markForCheck();
fixture.detectChanges();
}
afterEach(() => {
fixture.destroy();
jest.useRealTimers();
jest.restoreAllMocks();
});
it('renders its labels in the active language', () => {
expect(text('.now-card__label')).toBe('Läuft gerade');
expect(text('.now-card__statusline')).toBe('Warte auf Wiedergabe');
expect(text('.now-card__channel')).toBe('Kein Livestream ausgewählt');
expect(text('.now-card__meta')).toContain('unbekannt');
expect(text('.now-card__program')).toBe('Keine aktuelle Sendung');
expect(text('.volume-row__button--mute')).toBe('STUMM');
expect(text('.volume-caption')).toBe('Lautstärke nicht verfügbar');
expect(text('.remote__footer')).toBe('Warte auf Status…');
expect(
fixture.nativeElement
.querySelector('.touchpad__button--up')
.getAttribute('aria-label')
).toBe('Kanal hoch');
});
it('names the source of the current playback', async () => {
service.getStatus.mockResolvedValue({
portal: 'xtream',
isLiveView: true,
channelNumber: 7,
});
await settle(2000);
expect(text('.now-card__statusline')).toBe('Xtream live');
expect(text('.now-card__meta')).toContain('K 7');
expect(text('.now-card__meta')).toContain('xtream');
});
it('shows a failed action in the active language', async () => {
await fixture.componentInstance.changeChannelUp();
await settle();
expect(text('.remote__error')).toBe('Kanalwechsel fehlgeschlagen');
});
});
@@ -6,14 +6,21 @@ import {
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import {
RemoteControlService,
RemoteControlStatus,
} from './remote-control.service';
const PORTAL_LABEL_KEYS: Readonly<Record<string, string>> = {
m3u: 'REMOTE_CONTROL.SOURCE_M3U',
xtream: 'REMOTE_CONTROL.SOURCE_XTREAM',
stalker: 'REMOTE_CONTROL.SOURCE_STALKER',
};
@Component({
selector: 'lib-remote-control',
imports: [CommonModule],
imports: [CommonModule, TranslatePipe],
templateUrl: './remote-control.component.html',
// eslint-disable-next-line @angular-eslint/prefer-on-push-component-change-detection -- Preserve pre-Angular 22 eager checking during the framework upgrade.
changeDetection: ChangeDetectionStrategy.Eager,
@@ -25,6 +32,7 @@ export class RemoteControlComponent implements OnInit, OnDestroy {
isLoading = false;
isStatusLoading = false;
/** Translation key of the last failed action. */
error: string | null = null;
status: RemoteControlStatus | null = null;
numericInput = '';
@@ -47,14 +55,14 @@ export class RemoteControlComponent implements OnInit, OnDestroy {
async changeChannelUp(): Promise<void> {
await this.executeAction(
() => this.remoteControlService.channelUp(),
'Failed to change channel up'
'REMOTE_CONTROL.ERRORS.CHANNEL_UP'
);
}
async changeChannelDown(): Promise<void> {
await this.executeAction(
() => this.remoteControlService.channelDown(),
'Failed to change channel down'
'REMOTE_CONTROL.ERRORS.CHANNEL_DOWN'
);
}
@@ -82,7 +90,7 @@ export class RemoteControlComponent implements OnInit, OnDestroy {
await this.executeAction(
() =>
this.remoteControlService.selectChannelByNumber(channelNumber),
'Failed to switch by channel number'
'REMOTE_CONTROL.ERRORS.SELECT_NUMBER'
);
this.clearDigits();
}
@@ -90,30 +98,33 @@ export class RemoteControlComponent implements OnInit, OnDestroy {
async volumeUp(): Promise<void> {
await this.executeAction(
() => this.remoteControlService.volumeUp(),
'Failed to increase volume'
'REMOTE_CONTROL.ERRORS.VOLUME_UP'
);
}
async volumeDown(): Promise<void> {
await this.executeAction(
() => this.remoteControlService.volumeDown(),
'Failed to decrease volume'
'REMOTE_CONTROL.ERRORS.VOLUME_DOWN'
);
}
async toggleMute(): Promise<void> {
await this.executeAction(
() => this.remoteControlService.toggleMute(),
'Failed to toggle mute'
'REMOTE_CONTROL.ERRORS.TOGGLE_MUTE'
);
}
get portalLabel(): string {
const portal = this.status?.portal ?? 'unknown';
if (portal === 'm3u') return 'M3U Live';
if (portal === 'xtream') return 'Xtream Live';
if (portal === 'stalker') return 'Stalker ITV';
return 'Waiting For Playback';
get portalLabelKey(): string {
return (
PORTAL_LABEL_KEYS[this.status?.portal ?? ''] ??
'REMOTE_CONTROL.WAITING_FOR_PLAYBACK'
);
}
get hasKnownPortal(): boolean {
return !!this.status?.portal && this.status.portal !== 'unknown';
}
get isReady(): boolean {
@@ -136,7 +147,7 @@ export class RemoteControlComponent implements OnInit, OnDestroy {
this.status = await this.remoteControlService.getStatus();
} catch (err) {
if (!silent) {
this.error = 'Failed to fetch remote status';
this.error = 'REMOTE_CONTROL.ERRORS.STATUS';
console.error(err);
}
} finally {
@@ -148,7 +159,7 @@ export class RemoteControlComponent implements OnInit, OnDestroy {
private async executeAction(
action: () => Promise<void>,
errorMessage: string
errorKey: string
): Promise<void> {
this.isLoading = true;
this.error = null;
@@ -156,7 +167,7 @@ export class RemoteControlComponent implements OnInit, OnDestroy {
await action();
await this.refreshStatus(true);
} catch (err) {
this.error = errorMessage;
this.error = errorKey;
console.error(err);
} finally {
this.isLoading = false;
@@ -2,7 +2,9 @@
<section
class="hero"
data-test-id="dashboard-hero"
aria-roledescription="carousel"
[attr.aria-roledescription]="
'WORKSPACE.DASHBOARD.HERO_ROLE_CAROUSEL' | translate
"
tabindex="0"
[attr.aria-label]="'WORKSPACE.DASHBOARD.HERO_LABEL' | translate"
[class.hero--paused]="paused()"
@@ -67,7 +69,9 @@
<div
class="hero__content"
role="group"
aria-roledescription="slide"
[attr.aria-roledescription]="
'WORKSPACE.DASHBOARD.HERO_ROLE_SLIDE' | translate
"
[attr.data-test-id]="current ? 'dashboard-hero-slide' : null"
[attr.inert]="current ? null : ''"
[attr.data-hero-kind]="slide.kind"
@@ -43,7 +43,7 @@
class="rail__chev rail__chev--left"
[class.rail__chev--visible]="canScrollLeft()"
(click)="scrollBy(-1)"
aria-label="Scroll left"
[attr.aria-label]="'PORTALS.SCROLL_LEFT' | translate"
tabindex="-1"
>
<mat-icon>chevron_left</mat-icon>
@@ -312,7 +312,7 @@
class="rail__chev rail__chev--right"
[class.rail__chev--visible]="canScrollRight()"
(click)="scrollBy(1)"
aria-label="Scroll right"
[attr.aria-label]="'PORTALS.SCROLL_RIGHT' | translate"
tabindex="-1"
>
<mat-icon>chevron_right</mat-icon>
@@ -11,7 +11,7 @@ import {
describe('DashboardRailComponent', () => {
const createComponent = async (stripCountryPrefix: boolean) => {
await TestBed.configureTestingModule({
imports: [DashboardRailComponent],
imports: [DashboardRailComponent, TranslateModule.forRoot()],
providers: [
{
provide: SettingsStore,
+1 -1
View File
@@ -374,7 +374,7 @@
{
"name": "remote-control",
"root": "libs/ui/remote-control",
"reason": "No unit specs yet; validate through remote-control flows or add focused tests when behavior changes directly."
"reason": "Small phone UI: focused specs cover its labels and language choice; validate the rest through remote-control flows."
},
{
"name": "shared-portals",
+29
View File
@@ -327,6 +327,7 @@
"CHANNELS.DETAILS_DIALOG.HTTP_USER_AGENT": "User-Agent",
"CHANNELS.DETAILS_DIALOG.NAME": "Name",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.RECORDING_CHIP": "REC",
"DOWNLOADS.TITLE": "Downloads",
"EMBEDDED_MPV.PLAYER.AUDIO_TRACK_FALLBACK": "Audio {{index}}",
"EMBEDDED_MPV.PLAYER.LIVE_BADGE": "LIVE",
@@ -574,6 +575,7 @@
"CHANNELS.DETAILS_DIALOG.TVG_ID": "TVG ID",
"CHANNELS.DETAILS_DIALOG.TVG_REC": "TVG rec",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.RECORDING_CHIP": "REC",
"EMBEDDED_MPV.PLAYER.AUDIO_TRACK_FALLBACK": "Audio {{index}}",
"EMBEDDED_MPV.PLAYER.STATS_AUDIO": "Audio",
"EMBEDDED_MPV.PLAYER.TIMELINE_SEGMENT_POSITION": "{{title}} · {{time}}",
@@ -621,11 +623,13 @@
"PORTALS.MULTI_SOURCE.FILTER_HD": "HD+",
"PORTALS.SERIES": "Series",
"PORTALS.SIDEBAR.SERIES": "Series",
"REMOTE_CONTROL.VOLUME_ABBR": "VOL",
"SETTINGS.APP_UPDATE_CHANNEL_NIGHTLY": "Nightly",
"SETTINGS.APP_UPDATE_CHANNEL_STABLE": "Stable",
"SETTINGS.BROWSE_EPG_FILE": "Choose a local XMLTV file…",
"SETTINGS.CARD_XTREAM": "Xtream",
"SETTINGS.CHIP_EXPERIMENTAL": "Experimental",
"SETTINGS.EPG_OFFSET_UNIT": "min",
"SETTINGS.EPG_SOURCE_FORMATS_EXAMPLES": "Examples:",
"SETTINGS.EPG_SOURCE_FORMATS_HINT": "Remote XMLTV links (http/https) and files on this computer are both supported, as plain .xml or gzip-compressed .xml.gz. Local files need an absolute path or a file:// URL.",
"SETTINGS.EPG_SOURCE_LIST_TITLE": "Source list",
@@ -705,6 +709,7 @@
"CHANNELS.DETAILS_DIALOG.TVG_ID": "TVG ID",
"CHANNELS.DETAILS_DIALOG.TVG_REC": "TVG rec",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.RECORDING_CHIP": "REC",
"EMBEDDED_MPV.PLAYER.AUDIO_TRACK_FALLBACK": "Audio {{index}}",
"EMBEDDED_MPV.PLAYER.PAUSE": "Pause",
"EMBEDDED_MPV.PLAYER.QUALITY_AUTO": "Auto",
@@ -759,6 +764,9 @@
"PORTALS.MULTI_SOURCE.SOURCE": "Source",
"PORTALS.MULTI_SOURCE.SOURCES": "Sources",
"PORTALS.PAGE": "Page",
"REMOTE_CONTROL.CHANNEL_ABBR": "CH",
"REMOTE_CONTROL.VOLUME": "Volume",
"REMOTE_CONTROL.VOLUME_ABBR": "VOL",
"SETTINGS.APP_UPDATE_CHANNEL_NIGHTLY": "Nightly",
"SETTINGS.APP_UPDATE_CHANNEL_STABLE": "Stable",
"SETTINGS.BROWSE_EPG_FILE": "Choose a local XMLTV file…",
@@ -766,6 +774,7 @@
"SETTINGS.CARD_GUIDE": "Guide",
"SETTINGS.CARD_SOURCES": "Sources",
"SETTINGS.CARD_XTREAM": "Xtream",
"SETTINGS.EPG_OFFSET_UNIT": "min",
"SETTINGS.EPG_SOURCE_FORMATS_EXAMPLES": "Examples:",
"SETTINGS.EPG_SOURCE_FORMATS_HINT": "Remote XMLTV links (http/https) and files on this computer are both supported, as plain .xml or gzip-compressed .xml.gz. Local files need an absolute path or a file:// URL.",
"SETTINGS.EPG_SOURCE_LIST_TITLE": "Source list",
@@ -844,6 +853,7 @@
"DOWNLOADS.ACTION_FAILED": "Action failed",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.PAUSE": "Pause",
"DOWNLOADS.RECORDING_CHIP": "REC",
"DOWNLOADS.RESUME": "Resume",
"DOWNLOADS.STATUS.PAUSED": "Paused",
"EMBEDDED_MPV.PLAYER.BACK": "Back",
@@ -978,6 +988,7 @@
"CHANNELS.DETAILS_DIALOG.TVG_ID": "TVG ID",
"CHANNELS.DETAILS_DIALOG.TVG_REC": "TVG rec",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.RECORDING_CHIP": "REC",
"EMBEDDED_MPV.PLAYER.AUDIO_TRACK_FALLBACK": "Audio {{index}}",
"EMBEDDED_MPV.PLAYER.STATS_AUDIO": "Audio",
"EMBEDDED_MPV.PLAYER.STATS_BUFFER": "Buffer",
@@ -1031,12 +1042,15 @@
"PORTALS.DETAIL.TRAILER_OF": "{{title}} · Trailer",
"PORTALS.ERROR_VIEW.HOME": "Home",
"PORTALS.MULTI_SOURCE.FILTER_HD": "HD+",
"REMOTE_CONTROL.VOLUME": "Volume",
"REMOTE_CONTROL.VOLUME_ABBR": "VOL",
"SETTINGS.APP_UPDATE_CHANNEL_NIGHTLY": "Nightly",
"SETTINGS.APP_UPDATE_CHANNEL_STABLE": "Stable",
"SETTINGS.BROWSE_EPG_FILE": "Choose a local XMLTV file…",
"SETTINGS.CARD_CACHE": "Cache",
"SETTINGS.CARD_DESKTOP": "Desktop",
"SETTINGS.CARD_XTREAM": "Xtream",
"SETTINGS.EPG_OFFSET_UNIT": "min",
"SETTINGS.EPG_SOURCE_FORMATS_EXAMPLES": "Examples:",
"SETTINGS.EPG_SOURCE_FORMATS_HINT": "Remote XMLTV links (http/https) and files on this computer are both supported, as plain .xml or gzip-compressed .xml.gz. Local files need an absolute path or a file:// URL.",
"SETTINGS.EPG_SOURCE_LIST_TITLE": "Source list",
@@ -1217,6 +1231,7 @@
"CHANNELS.DETAILS_DIALOG.TVG_REC": "TVG rec",
"CHANNELS.DETAILS_DIALOG.TVG_URL": "TVG URL",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.RECORDING_CHIP": "REC",
"EMBEDDED_MPV.PLAYER.TIMELINE_SEGMENT_POSITION": "{{title}} · {{time}}",
"EPG.DOWNLOAD_SUCCESS": "EPG was successfully downloaded.",
"EPG.ERROR": "Ooops, EPG could not be loaded.",
@@ -1338,6 +1353,7 @@
"CHANNELS.DETAILS_DIALOG.HTTP_REFERRER": "Referrer",
"CHANNELS.DETAILS_DIALOG.HTTP_USER_AGENT": "User-Agent",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.RECORDING_CHIP": "REC",
"DOWNLOADS.TITLE": "Downloads",
"EMBEDDED_MPV.PLAYER.AUDIO_TRACK_FALLBACK": "Audio {{index}}",
"EMBEDDED_MPV.PLAYER.LIVE_BADGE": "LIVE",
@@ -1404,6 +1420,8 @@
"PORTALS.SERIES": "Series",
"PORTALS.SIDEBAR.RECENT": "Recent",
"PORTALS.SIDEBAR.SERIES": "Series",
"REMOTE_CONTROL.VOLUME": "Volume",
"REMOTE_CONTROL.VOLUME_ABBR": "VOL",
"SETTINGS.APP_UPDATE_CHANNEL_NIGHTLY": "Nightly",
"SETTINGS.APP_UPDATE_CHANNEL_STABLE": "Stable",
"SETTINGS.BROWSE_EPG_FILE": "Choose a local XMLTV file…",
@@ -1411,6 +1429,7 @@
"SETTINGS.CARD_DESKTOP": "Desktop",
"SETTINGS.CARD_XTREAM": "Xtream",
"SETTINGS.COMMUNITY": "Project & community",
"SETTINGS.EPG_OFFSET_UNIT": "min",
"SETTINGS.EPG_SOURCE_FORMATS_EXAMPLES": "Examples:",
"SETTINGS.EPG_SOURCE_FORMATS_HINT": "Remote XMLTV links (http/https) and files on this computer are both supported, as plain .xml or gzip-compressed .xml.gz. Local files need an absolute path or a file:// URL.",
"SETTINGS.EPG_SOURCE_LIST_TITLE": "Source list",
@@ -1506,6 +1525,7 @@
"CHANNELS.DETAILS_DIALOG.TVG_ID": "TVG ID",
"CHANNELS.DETAILS_DIALOG.TVG_REC": "TVG rec",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.RECORDING_CHIP": "REC",
"EMBEDDED_MPV.PLAYER.AUDIO_TRACK_FALLBACK": "Audio {{index}}",
"EMBEDDED_MPV.PLAYER.TIMELINE_SEGMENT_POSITION": "{{title}} · {{time}}",
"EPG.TIMELINE.MINUTES_SHORT": "min",
@@ -1545,11 +1565,14 @@
"PLAYBACK_DIAGNOSTICS.EXTERNAL_STARTED": "Player started",
"PORTALS.DETAIL.DURATION_MINUTES": "{{minutes}} min",
"PORTALS.MULTI_SOURCE.FILTER_HD": "HD+",
"REMOTE_CONTROL.CHANNEL_ABBR": "CH",
"REMOTE_CONTROL.VOLUME_ABBR": "VOL",
"SETTINGS.APP_UPDATE_CHANNEL_NIGHTLY": "Nightly",
"SETTINGS.APP_UPDATE_CHANNEL_STABLE": "Stable",
"SETTINGS.BROWSE_EPG_FILE": "Choose a local XMLTV file…",
"SETTINGS.CARD_XTREAM": "Xtream",
"SETTINGS.CHIP_RESTART": "Restart",
"SETTINGS.EPG_OFFSET_UNIT": "min",
"SETTINGS.EPG_SOURCE_FORMATS_EXAMPLES": "Examples:",
"SETTINGS.EPG_SOURCE_FORMATS_HINT": "Remote XMLTV links (http/https) and files on this computer are both supported, as plain .xml or gzip-compressed .xml.gz. Local files need an absolute path or a file:// URL.",
"SETTINGS.EPG_SOURCE_LIST_TITLE": "Source list",
@@ -1620,6 +1643,7 @@
"CHANNELS.DETAILS_DIALOG.TVG_ID": "TVG ID",
"CHANNELS.DETAILS_DIALOG.TVG_REC": "TVG rec",
"DOWNLOADS.OFFLINE_DETAIL.FILE_SIZE": "{{size}}",
"DOWNLOADS.RECORDING_CHIP": "REC",
"DOWNLOADS.TITLE": "Downloads",
"EMBEDDED_MPV.PLAYER.STATS_BUFFER": "Buffer",
"EMBEDDED_MPV.PLAYER.TIMELINE_SEGMENT_POSITION": "{{title}} · {{time}}",
@@ -1670,12 +1694,16 @@
"PORTALS.DETAIL.TRAILER_OF": "{{title}} · Trailer",
"PORTALS.ITEM": "item",
"PORTALS.MULTI_SOURCE.FILTER_HD": "HD+",
"REMOTE_CONTROL.CHANNEL_ABBR": "CH",
"REMOTE_CONTROL.VOLUME": "Volume",
"REMOTE_CONTROL.VOLUME_ABBR": "VOL",
"SETTINGS.APP_UPDATE_CHANNEL_NIGHTLY": "Nightly",
"SETTINGS.APP_UPDATE_CHANNEL_STABLE": "Stable",
"SETTINGS.BROWSE_EPG_FILE": "Choose a local XMLTV file…",
"SETTINGS.CARD_CACHE": "Cache",
"SETTINGS.CARD_XTREAM": "Xtream",
"SETTINGS.CHIP_EXPERIMENTAL": "Experimental",
"SETTINGS.EPG_OFFSET_UNIT": "min",
"SETTINGS.EPG_SOURCE_FORMATS_EXAMPLES": "Examples:",
"SETTINGS.EPG_SOURCE_FORMATS_HINT": "Remote XMLTV links (http/https) and files on this computer are both supported, as plain .xml or gzip-compressed .xml.gz. Local files need an absolute path or a file:// URL.",
"SETTINGS.EPG_SOURCE_LIST_TITLE": "Source list",
@@ -1720,6 +1748,7 @@
"SOURCE_HEALTH.RECHECK": "Check again",
"STALKER.ACCOUNT_INFO.PORTAL_INFO": "Portal",
"WORKSPACE.CONTEXT.ITEM_COUNT_ONE": "1 item",
"WORKSPACE.DASHBOARD.HERO_ROLE_SLIDE": "slide",
"WORKSPACE.DASHBOARD.M3U": "M3U",
"WORKSPACE.DASHBOARD.STALKER": "Stalker",
"WORKSPACE.DASHBOARD.XTREAM": "Xtream",
+4 -4
View File
@@ -20,12 +20,12 @@
"measuredWith": "pnpm run perf:journeys (ubuntu-latest, xvfb), identical in 3 runs"
},
"renderer.domMutationsToFirstCard": {
"value": 558,
"value": 569,
"unit": "mutations",
"slack": 0,
"updatedAt": "2026-10-04",
"evidencePr": 1828,
"evidenceRun": "https://github.com/4gray/iptvnator/actions/runs/37192092882",
"updatedAt": "2026-10-10",
"evidencePr": 1871,
"evidenceRun": "https://github.com/4gray/iptvnator/actions/runs/38045444230",
"measuredWith": "pnpm run perf:journeys (ubuntu-latest, xvfb), identical in 3 runs"
},
"main.modulesRegisteredBeforeWindow": {