mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
fix(i18n): translate hard-coded labels, snackbars and missing keys (#1867)
* fix(i18n): translate hard-coded labels, snackbars and missing keys Catalog screens, snackbars and external player messages showed English in every locale, and seven keys used in code were missing from en.json, so ngx-translate rendered them raw. - Catalog: "All items", item and channel counts, channel sort menus and tooltips, unnamed-category and empty-category labels, LIVE/PAUSED badges and the Xtream global search summary are translated. The Xtream and Stalker stores no longer bake an English name into the every-item sentinel; the facades return a null title and the view translates it. - Snackbars: Xtream/Stalker request failures, the 413 upload error, backup export/import results and the category visibility failure use keys; every "Close" action uses CLOSE. - External player: the main process sends an error code instead of an English sentence (player-error event, session errorCode, and a tag in rejected launch errors); the renderer, dock and VOD primary button translate it. The external player info dialog is translated. - Adds the seven missing keys and 43 new ones, translated in all 18 locales; seven legitimately identical values are baselined. - tools/i18n/check-usage.mjs fails on keys used in code but missing from en.json; it runs in i18n:check (and so in CI through i18n:validate). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): translate catalog counts in the template and skip inline template comments - The category subtitle hands a key to the translate pipe instead of caching translate.instant(), so a cold start re-renders it once the language file loads. - The Xtream live root count uses the singular/plural item keys, so one result no longer reads "1 channels". - check-usage.mjs strips HTML comments inside inline templates of TypeScript files, so a commented-out key no longer fails the check. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): check keys in parenthesised translate pipe operands `(expanded() ? 'SHOW_LESS' : 'SHOW_MORE') | translate` yields keys that neither precede the pipe directly nor contain a dot, so the usage check missed them. It now reads the ternary and fallback branches of a parenthesised operand; a literal compared in the condition is not read as a key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(e2e): expect the translated external player failure in the dock A launch failure without an error code now shows the translated generic status in the playback dock, with the raw main-process detail as its tooltip. The ClearKey DASH flow asserted the raw English text. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(playback): keep the IPC error tag out of the stored session detail A tagged launch failure reached ExternalPlayerSession.error unchanged, so the dock tooltip showed "[iptvnator:external-player:start-failed]". The registry now strips the tag when it stores the detail; the rejected IPC error keeps it for the renderer to read the code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(web): expect translated portal request failure toasts #1861's new resolved-failure specs asserted the English toast text; the toasts now go through PORTALS.REQUEST_ERRORS keys, so the specs check the key and its message/status params. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(i18n): read only returned branches of a grouped translate operand A literal at the start of a condition, as in `('ERROR' === status() ? 'CLOSE' : 'CLOSE') | translate`, was taken for a translation key, so a valid template could fail the usage check. A grouped literal now counts only when it ends its operand (end of group, `:`, `||` or `??`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
98784815b1
commit
6e18983400
106 files changed
+2695
-360
No files matched your search
@@ -19,7 +19,10 @@ import { ContentMetadataPatch } from './content-metadata.interface';
|
||||
import { DownloadMetadataSnapshot } from './download-metadata.interface';
|
||||
import { EpgChannelMetadata } from './epg-channel-metadata.model';
|
||||
import { EpgProgram } from './epg-program.model';
|
||||
import { ExternalPlayerSession } from './external-player-session.interface';
|
||||
import {
|
||||
ExternalPlayerErrorCode,
|
||||
ExternalPlayerSession,
|
||||
} from './external-player-session.interface';
|
||||
import {
|
||||
GlobalSearchPaginationOptions,
|
||||
GlobalSearchResult,
|
||||
@@ -603,10 +606,13 @@ export interface ElectronBridgeRemoteControlStatus {
|
||||
muted?: boolean;
|
||||
}
|
||||
|
||||
/** Payload of the main process `player-error` event. */
|
||||
export interface ElectronBridgePlayerError {
|
||||
player: string;
|
||||
player: 'MPV' | 'VLC';
|
||||
/** Null when no code describes the output; see `error`. */
|
||||
code: ExternalPlayerErrorCode | null;
|
||||
/** Raw player output, for logs and the unknown-error message. */
|
||||
error: string;
|
||||
originalError: string;
|
||||
}
|
||||
|
||||
export interface ElectronBridgePlaybackPositionInput extends Omit<
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
EXTERNAL_PLAYER_ERROR_KEYS,
|
||||
readExternalPlayerErrorCode,
|
||||
stripExternalPlayerErrorTag,
|
||||
tagExternalPlayerError,
|
||||
} from './external-player-session.interface';
|
||||
|
||||
describe('external player error codes', () => {
|
||||
it('reads the code back from a tagged message', () => {
|
||||
const message = tagExternalPlayerError(
|
||||
'start-failed',
|
||||
'Failed to start MPV player: spawn mpv ENOENT'
|
||||
);
|
||||
|
||||
expect(readExternalPlayerErrorCode(new Error(message))).toBe(
|
||||
'start-failed'
|
||||
);
|
||||
expect(readExternalPlayerErrorCode(message)).toBe('start-failed');
|
||||
});
|
||||
|
||||
it('survives the prefix Electron adds to a rejected IPC call', () => {
|
||||
const rejected = new Error(
|
||||
`Error invoking remote method 'OPEN_MPV_PLAYER': Error: ${tagExternalPlayerError(
|
||||
'previous-closing',
|
||||
'Cannot launch player'
|
||||
)}`
|
||||
);
|
||||
|
||||
expect(readExternalPlayerErrorCode(rejected)).toBe('previous-closing');
|
||||
});
|
||||
|
||||
it('strips the tag and leaves untagged messages alone', () => {
|
||||
expect(
|
||||
stripExternalPlayerErrorTag(
|
||||
tagExternalPlayerError('start-failed', 'Failed to start MPV')
|
||||
)
|
||||
).toBe('Failed to start MPV');
|
||||
expect(stripExternalPlayerErrorTag('MPV IPC command timed out')).toBe(
|
||||
'MPV IPC command timed out'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null for untagged messages and unknown codes', () => {
|
||||
expect(readExternalPlayerErrorCode(new Error('boom'))).toBeNull();
|
||||
expect(
|
||||
readExternalPlayerErrorCode(
|
||||
'[iptvnator:external-player:not-a-code] boom'
|
||||
)
|
||||
).toBeNull();
|
||||
expect(
|
||||
readExternalPlayerErrorCode(
|
||||
'[iptvnator:external-player:constructor]'
|
||||
)
|
||||
).toBeNull();
|
||||
expect(readExternalPlayerErrorCode(null)).toBeNull();
|
||||
expect(readExternalPlayerErrorCode({ message: 42 })).toBeNull();
|
||||
});
|
||||
|
||||
it('maps every code to a key in the EXTERNAL_PLAYER.ERRORS group', () => {
|
||||
for (const key of Object.values(EXTERNAL_PLAYER_ERROR_KEYS)) {
|
||||
expect(key).toMatch(/^EXTERNAL_PLAYER\.ERRORS\.[A-Z_]+$/);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,83 @@ export type ExternalPlayerName = 'mpv' | 'vlc';
|
||||
export type ExternalPlayerSessionStatus =
|
||||
'launching' | 'opened' | 'playing' | 'error' | 'closed';
|
||||
|
||||
/**
|
||||
* Why an external player failed. The main process classifies the failure and
|
||||
* the renderer translates it; the raw player output stays in `error`.
|
||||
*/
|
||||
export type ExternalPlayerErrorCode =
|
||||
| 'start-failed'
|
||||
| 'previous-closing'
|
||||
| 'closed-unexpectedly'
|
||||
| 'stream-open-failed'
|
||||
| 'unsupported-protocol'
|
||||
| 'connection-failed'
|
||||
| 'access-denied'
|
||||
| 'stream-not-found'
|
||||
| 'timed-out';
|
||||
|
||||
/** Translation keys for each code; a message may use `{{player}}`. */
|
||||
export const EXTERNAL_PLAYER_ERROR_KEYS: Readonly<
|
||||
Record<ExternalPlayerErrorCode, string>
|
||||
> = {
|
||||
'start-failed': 'EXTERNAL_PLAYER.ERRORS.START_FAILED',
|
||||
'previous-closing': 'EXTERNAL_PLAYER.ERRORS.PREVIOUS_CLOSING',
|
||||
'closed-unexpectedly': 'EXTERNAL_PLAYER.ERRORS.CLOSED_UNEXPECTEDLY',
|
||||
'stream-open-failed': 'EXTERNAL_PLAYER.ERRORS.STREAM_OPEN_FAILED',
|
||||
'unsupported-protocol': 'EXTERNAL_PLAYER.ERRORS.UNSUPPORTED_PROTOCOL',
|
||||
'connection-failed': 'EXTERNAL_PLAYER.ERRORS.CONNECTION_FAILED',
|
||||
'access-denied': 'EXTERNAL_PLAYER.ERRORS.ACCESS_DENIED',
|
||||
'stream-not-found': 'EXTERNAL_PLAYER.ERRORS.STREAM_NOT_FOUND',
|
||||
'timed-out': 'EXTERNAL_PLAYER.ERRORS.TIMED_OUT',
|
||||
};
|
||||
|
||||
/** For output no code describes; takes `{{player}}` and the raw `{{error}}`. */
|
||||
export const EXTERNAL_PLAYER_UNKNOWN_ERROR_KEY =
|
||||
'EXTERNAL_PLAYER.ERRORS.UNKNOWN';
|
||||
|
||||
/** For a rejected launch that carries no code; takes `{{player}}`. */
|
||||
export const EXTERNAL_PLAYER_LAUNCH_FAILED_KEY =
|
||||
'EXTERNAL_PLAYER.ERRORS.LAUNCH_FAILED';
|
||||
|
||||
const EXTERNAL_PLAYER_ERROR_TAG = /\[iptvnator:external-player:([a-z-]+)\]/;
|
||||
|
||||
/**
|
||||
* Prefixes an error message with its code. A rejected IPC call reaches the
|
||||
* renderer as a message only, so the code has to travel inside it.
|
||||
*/
|
||||
export function tagExternalPlayerError(
|
||||
code: ExternalPlayerErrorCode,
|
||||
message: string
|
||||
): string {
|
||||
return `[iptvnator:external-player:${code}] ${message}`;
|
||||
}
|
||||
|
||||
/** Removes the code tag so the message can be shown or stored as detail. */
|
||||
export function stripExternalPlayerErrorTag(message: string): string {
|
||||
return message.replace(
|
||||
new RegExp(`${EXTERNAL_PLAYER_ERROR_TAG.source}\\s*`, 'g'),
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
/** Reads the code `tagExternalPlayerError` put into an error message. */
|
||||
export function readExternalPlayerErrorCode(
|
||||
error: unknown
|
||||
): ExternalPlayerErrorCode | null {
|
||||
const message =
|
||||
typeof error === 'string'
|
||||
? error
|
||||
: (error as { message?: unknown } | null)?.message;
|
||||
const code =
|
||||
typeof message === 'string'
|
||||
? EXTERNAL_PLAYER_ERROR_TAG.exec(message)?.[1]
|
||||
: undefined;
|
||||
return code &&
|
||||
Object.prototype.hasOwnProperty.call(EXTERNAL_PLAYER_ERROR_KEYS, code)
|
||||
? (code as ExternalPlayerErrorCode)
|
||||
: null;
|
||||
}
|
||||
|
||||
export interface ExternalPlayerSession {
|
||||
id: string;
|
||||
player: ExternalPlayerName;
|
||||
@@ -15,7 +92,9 @@ export interface ExternalPlayerSession {
|
||||
contentInfo?: PlayerContentInfo;
|
||||
startedAt: string;
|
||||
updatedAt: string;
|
||||
/** Raw failure detail; the dock translates `errorCode` instead. */
|
||||
error?: string;
|
||||
errorCode?: ExternalPlayerErrorCode;
|
||||
canClose: boolean;
|
||||
/** Exact failed replacement displaced by this restored live session. */
|
||||
restoredFromSessionId?: string;
|
||||
|
||||
Reference in new issue
Block a user