i18n(mobile): polish UI strings → English

Tłumaczenie wszystkich user-facing stringów PL→EN (bug-report 2026-05-31
"dalej wszystko po polsku"). Alerty, przyciski, placeholdery, labelki w 12
ekranach/komponentach: BugReportFAB, AppLock(Screen/Settings/PinEntry),
applock biometric prompts, doodstream error msgs, MovieDetail, PlaybackQuality,
Player, SceneDetail, ScenesFilter, SiteScenes. Komentarze w kodzie zostają PL.

Zmiany były WIP drugiego okna (uncommitted); wjechały do bundla 0.2.1 przy
buildzie (były w working tree) — apka zainstalowana już ma EN. Ten commit
utrwala je w gicie żeby nie zginęły. Czysto stringi, zero zmian logiki.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-05-31 16:27:55 +02:00
parent 55503136e6
commit b942565a6e
12 changed files with 80 additions and 80 deletions

View file

@ -87,11 +87,11 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
const submit = useCallback(async () => { const submit = useCallback(async () => {
if (!client) { if (!client) {
Alert.alert('Bug report', 'Brak połączenia z backendem.'); Alert.alert('Bug report', 'No connection to the backend.');
return; return;
} }
if (!message.trim()) { if (!message.trim()) {
Alert.alert('Bug report', 'Wpisz krótki opis.'); Alert.alert('Bug report', 'Enter a short description.');
return; return;
} }
setSubmitting(true); setSubmitting(true);
@ -136,9 +136,9 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
setOpen(false); setOpen(false);
setMessage(''); setMessage('');
setScreenshot(null); setScreenshot(null);
Alert.alert('Bug report', 'Wysłano. Dzięki!'); Alert.alert('Bug report', 'Sent. Thanks!');
} catch (e) { } catch (e) {
Alert.alert('Bug report', `Nie udało się wysłać: ${(e as Error).message}`); Alert.alert('Bug report', `Failed to send: ${(e as Error).message}`);
} finally { } finally {
setSubmitting(false); setSubmitting(false);
} }
@ -164,7 +164,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
> >
<Pressable style={styles.backdropTap} onPress={() => setOpen(false)} /> <Pressable style={styles.backdropTap} onPress={() => setOpen(false)} />
<View style={styles.sheet}> <View style={styles.sheet}>
<Text style={styles.title}>Zgłoś buga</Text> <Text style={styles.title}>Report a bug</Text>
{screenshot ? ( {screenshot ? (
<View style={styles.preview}> <View style={styles.preview}>
@ -174,7 +174,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
resizeMode="contain" resizeMode="contain"
/> />
<View style={styles.toggleRow}> <View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>Dołącz screenshot</Text> <Text style={styles.toggleLabel}>Attach screenshot</Text>
<Switch <Switch
value={includeScreenshot} value={includeScreenshot}
onValueChange={setIncludeScreenshot} onValueChange={setIncludeScreenshot}
@ -188,7 +188,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
</View> </View>
) : ( ) : (
<Text style={styles.noScreenshot}> <Text style={styles.noScreenshot}>
Screenshot niedostępny (capture fail). Wyślę sam tekst. Screenshot unavailable (capture failed). Sending text only.
</Text> </Text>
)} )}
@ -196,7 +196,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
style={styles.input} style={styles.input}
value={message} value={message}
onChangeText={setMessage} onChangeText={setMessage}
placeholder="Co jest nie tak? Tytuł sceny / co próbujesz zrobić / co zobaczyłeś" placeholder="What's wrong? Scene title / what you're trying to do / what you saw"
placeholderTextColor={theme.mutedDim} placeholderTextColor={theme.mutedDim}
multiline multiline
autoFocus autoFocus
@ -208,7 +208,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
onPress={() => setOpen(false)} onPress={() => setOpen(false)}
disabled={submitting} disabled={submitting}
> >
<Text style={styles.btnText}>Anuluj</Text> <Text style={styles.btnText}>Cancel</Text>
</TouchableOpacity> </TouchableOpacity>
<TouchableOpacity <TouchableOpacity
style={[styles.btn, styles.btnSend]} style={[styles.btn, styles.btnSend]}
@ -218,7 +218,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
{submitting ? ( {submitting ? (
<ActivityIndicator color={theme.fg} /> <ActivityIndicator color={theme.fg} />
) : ( ) : (
<Text style={styles.btnText}>Wyślij</Text> <Text style={styles.btnText}>Send</Text>
)} )}
</TouchableOpacity> </TouchableOpacity>
</View> </View>

View file

@ -44,7 +44,7 @@ export async function setTimeoutSeconds(s: number): Promise<void> {
} }
export async function setPin(pin: string): Promise<void> { export async function setPin(pin: string): Promise<void> {
if (!/^\d{4,8}$/.test(pin)) throw new Error('PIN musi mieć 4-8 cyfr'); if (!/^\d{4,8}$/.test(pin)) throw new Error('PIN must be 4-8 digits');
await SecureStore.setItemAsync(PIN_KEY, pin); await SecureStore.setItemAsync(PIN_KEY, pin);
} }
@ -66,8 +66,8 @@ export async function biometricAvailable(): Promise<boolean> {
export async function authenticateBiometric(): Promise<boolean> { export async function authenticateBiometric(): Promise<boolean> {
const res = await LocalAuthentication.authenticateAsync({ const res = await LocalAuthentication.authenticateAsync({
promptMessage: 'Odblokuj Goon', promptMessage: 'Unlock Goon',
cancelLabel: 'Użyj PIN', cancelLabel: 'Use PIN',
disableDeviceFallback: true, disableDeviceFallback: true,
}); });
return res.success; return res.success;

View file

@ -103,7 +103,7 @@ export async function resolveDoodStream(
// 30x → resolve Location, kontynuuj. Inaczej traktuj jako final. // 30x → resolve Location, kontynuuj. Inaczej traktuj jako final.
if (r.status >= 300 && r.status < 400) { if (r.status >= 300 && r.status < 400) {
const loc = r.headers.get('location'); const loc = r.headers.get('location');
if (!loc) return { error: `redirect ${r.status} bez Location` }; if (!loc) return { error: `redirect ${r.status} without Location` };
try { try {
currentUrl = new URL(loc, currentUrl).toString(); currentUrl = new URL(loc, currentUrl).toString();
} catch { } catch {
@ -147,7 +147,7 @@ async function _continueAfterFetch(
// Tu odsyłamy do WebView fallback. Sam `turnstile` w HTML nie wystarcza — // Tu odsyłamy do WebView fallback. Sam `turnstile` w HTML nie wystarcza —
// pełna strona playera ZAWIERA opcjonalny turnstile container. // pełna strona playera ZAWIERA opcjonalny turnstile container.
if (html.length < 2000 || /challenge-platform/i.test(html)) { if (html.length < 2000 || /challenge-platform/i.test(html)) {
return { error: 'captcha_gate (mobile IP też zablokowane?)' }; return { error: 'captcha_gate (mobile IP also blocked?)' };
} }
return { error: 'no_pass_md5_in_html' }; return { error: 'no_pass_md5_in_html' };
} }

View file

@ -99,9 +99,9 @@ export function AppLockScreen({ onUnlock, onLogout }: Props) {
<View style={styles.lockBadge}> <View style={styles.lockBadge}>
<Text style={styles.lockGlyph}>g</Text> <Text style={styles.lockGlyph}>g</Text>
</View> </View>
<Text style={styles.title}>Goon zablokowany</Text> <Text style={styles.title}>Goon locked</Text>
<Text style={styles.subtitle}> <Text style={styles.subtitle}>
{bioEnabled && bioAvailable ? 'Użyj odcisku lub PIN-u' : 'Wpisz PIN'} {bioEnabled && bioAvailable ? 'Use fingerprint or PIN' : 'Enter PIN'}
</Text> </Text>
</View> </View>
@ -148,7 +148,7 @@ export function AppLockScreen({ onUnlock, onLogout }: Props) {
{onLogout ? ( {onLogout ? (
<Pressable onPress={onLogout} hitSlop={12} style={styles.logout}> <Pressable onPress={onLogout} hitSlop={12} style={styles.logout}>
<Text style={styles.logoutText}>Wyloguj się</Text> <Text style={styles.logoutText}>Log out</Text>
</Pressable> </Pressable>
) : null} ) : null}
</View> </View>

View file

@ -26,8 +26,8 @@ import { theme } from '../theme';
import { PinEntry } from './PinEntry'; import { PinEntry } from './PinEntry';
const TIMEOUT_OPTIONS: { label: string; seconds: number }[] = [ const TIMEOUT_OPTIONS: { label: string; seconds: number }[] = [
{ label: 'Natychmiast', seconds: 0 }, { label: 'Immediately', seconds: 0 },
{ label: '30 sek', seconds: 30 }, { label: '30 sec', seconds: 30 },
{ label: '1 min', seconds: 60 }, { label: '1 min', seconds: 60 },
{ label: '5 min', seconds: 300 }, { label: '5 min', seconds: 300 },
{ label: '15 min', seconds: 900 }, { label: '15 min', seconds: 900 },
@ -63,7 +63,7 @@ export function AppLockSettingsScreen() {
if (stage === 'enter-current') { if (stage === 'enter-current') {
return ( return (
<PinEntry <PinEntry
title="Wpisz aktualny PIN" title="Enter current PIN"
error={errorText} error={errorText}
onCancel={() => { onCancel={() => {
setStage('menu'); setStage('menu');
@ -72,7 +72,7 @@ export function AppLockSettingsScreen() {
onSubmit={async (candidate) => { onSubmit={async (candidate) => {
const ok = await verifyPin(candidate); const ok = await verifyPin(candidate);
if (!ok) { if (!ok) {
setErrorText('Nieprawidłowy PIN'); setErrorText('Incorrect PIN');
return; return;
} }
setErrorText(null); setErrorText(null);
@ -85,7 +85,7 @@ export function AppLockSettingsScreen() {
if (stage === 'set-new') { if (stage === 'set-new') {
return ( return (
<PinEntry <PinEntry
title="Wpisz nowy PIN (4-8 cyfr)" title="Enter new PIN (4-8 digits)"
error={errorText} error={errorText}
onCancel={() => { onCancel={() => {
setStage('menu'); setStage('menu');
@ -94,7 +94,7 @@ export function AppLockSettingsScreen() {
}} }}
onSubmit={async (candidate) => { onSubmit={async (candidate) => {
if (candidate.length < 4) { if (candidate.length < 4) {
setErrorText('Min. 4 cyfry'); setErrorText('Min. 4 digits');
return; return;
} }
setNewPin(candidate); setNewPin(candidate);
@ -108,7 +108,7 @@ export function AppLockSettingsScreen() {
if (stage === 'confirm-new') { if (stage === 'confirm-new') {
return ( return (
<PinEntry <PinEntry
title="Potwierdź PIN" title="Confirm PIN"
error={errorText} error={errorText}
onCancel={() => { onCancel={() => {
setStage('menu'); setStage('menu');
@ -117,7 +117,7 @@ export function AppLockSettingsScreen() {
}} }}
onSubmit={async (candidate) => { onSubmit={async (candidate) => {
if (candidate !== newPin) { if (candidate !== newPin) {
setErrorText('PIN-y nie pasują'); setErrorText('PINs do not match');
return; return;
} }
await setPin(newPin); await setPin(newPin);
@ -126,7 +126,7 @@ export function AppLockSettingsScreen() {
setErrorText(null); setErrorText(null);
setStage('menu'); setStage('menu');
await refresh(); await refresh();
Alert.alert('Gotowe', 'PIN zapisany. Blokada włączona.'); Alert.alert('Done', 'PIN saved. App lock enabled.');
}} }}
/> />
); );
@ -135,7 +135,7 @@ export function AppLockSettingsScreen() {
if (stage === 'disable-confirm') { if (stage === 'disable-confirm') {
return ( return (
<PinEntry <PinEntry
title="Wpisz PIN aby wyłączyć blokadę" title="Enter PIN to disable lock"
error={errorText} error={errorText}
onCancel={() => { onCancel={() => {
setStage('menu'); setStage('menu');
@ -144,7 +144,7 @@ export function AppLockSettingsScreen() {
onSubmit={async (candidate) => { onSubmit={async (candidate) => {
const ok = await verifyPin(candidate); const ok = await verifyPin(candidate);
if (!ok) { if (!ok) {
setErrorText('Nieprawidłowy PIN'); setErrorText('Incorrect PIN');
return; return;
} }
await setEnabled(false); await setEnabled(false);
@ -161,13 +161,13 @@ export function AppLockSettingsScreen() {
return ( return (
<ScrollView style={styles.root} contentContainerStyle={{ paddingBottom: 40 }}> <ScrollView style={styles.root} contentContainerStyle={{ paddingBottom: 40 }}>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Blokada aplikacji</Text> <Text style={styles.sectionTitle}>App lock</Text>
<View style={styles.row}> <View style={styles.row}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.label}>Włączona</Text> <Text style={styles.label}>Enabled</Text>
<Text style={styles.hint}> <Text style={styles.hint}>
{settings.enabled ? 'Wymaga PIN przy starcie i po przerwie' : 'Wyłączona'} {settings.enabled ? 'Requires PIN at startup and after a break' : 'Disabled'}
</Text> </Text>
</View> </View>
<Switch <Switch
@ -194,7 +194,7 @@ export function AppLockSettingsScreen() {
setErrorText(null); setErrorText(null);
}} }}
> >
<Text style={styles.actionText}>Zmień PIN</Text> <Text style={styles.actionText}>Change PIN</Text>
</Pressable> </Pressable>
) : null} ) : null}
</View> </View>
@ -202,14 +202,14 @@ export function AppLockSettingsScreen() {
{settings.enabled ? ( {settings.enabled ? (
<> <>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Biometria</Text> <Text style={styles.sectionTitle}>Biometrics</Text>
<View style={styles.row}> <View style={styles.row}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.label}>Odcisk / Face Unlock</Text> <Text style={styles.label}>Fingerprint / Face Unlock</Text>
<Text style={styles.hint}> <Text style={styles.hint}>
{bioAvailable {bioAvailable
? 'Szybsze odblokowywanie, PIN jako fallback' ? 'Faster unlocking, PIN as fallback'
: 'Brak biometrii w urządzeniu'} : 'No biometrics on this device'}
</Text> </Text>
</View> </View>
<Switch <Switch
@ -226,8 +226,8 @@ export function AppLockSettingsScreen() {
</View> </View>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>Czas do zablokowania</Text> <Text style={styles.sectionTitle}>Time until lock</Text>
<Text style={styles.hint}>Po wyjściu z aplikacji ile czekać przed lockiem</Text> <Text style={styles.hint}>How long to wait after leaving the app before locking</Text>
<View style={styles.chipRow}> <View style={styles.chipRow}>
{TIMEOUT_OPTIONS.map((opt) => { {TIMEOUT_OPTIONS.map((opt) => {
const active = settings.timeoutSeconds === opt.seconds; const active = settings.timeoutSeconds === opt.seconds;
@ -253,16 +253,16 @@ export function AppLockSettingsScreen() {
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.hint}> <Text style={styles.hint}>
Aplikacja jest również ukryta na liście ostatnich aplikacji i blokuje zrzuty ekranu. The app is also hidden in the recent apps list and blocks screenshots.
</Text> </Text>
</View> </View>
<View style={styles.section}> <View style={styles.section}>
<Text style={styles.sectionTitle}>O aplikacji</Text> <Text style={styles.sectionTitle}>About</Text>
<View style={styles.row}> <View style={styles.row}>
<View style={{ flex: 1 }}> <View style={{ flex: 1 }}>
<Text style={styles.label}>Wersja</Text> <Text style={styles.label}>Version</Text>
<Text style={styles.hint}>Bieżący JS bundle (OTA-updated)</Text> <Text style={styles.hint}>Current JS bundle (OTA-updated)</Text>
</View> </View>
<Text style={styles.versionValue}>{APP_VERSION}</Text> <Text style={styles.versionValue}>{APP_VERSION}</Text>
</View> </View>

View file

@ -200,7 +200,7 @@ function WatchChip({
if (parts.length > 1) { if (parts.length > 1) {
Alert.alert( Alert.alert(
title, title,
'Film składa się z kilku części. Wybierz którą zacząć.', 'This movie has several parts. Choose which one to start.',
[ [
...parts.map((p) => ({ ...parts.map((p) => ({
text: ((p.raw as any).part_label as string) ?? p.quality ?? 'Part', text: ((p.raw as any).part_label as string) ?? p.quality ?? 'Part',
@ -216,7 +216,7 @@ function WatchChip({
}); });
}, },
})), })),
{ text: 'Anuluj', style: 'cancel' as const }, { text: 'Cancel', style: 'cancel' as const },
], ],
); );
return; return;
@ -246,36 +246,36 @@ function WatchChip({
const onLongPress = () => { const onLongPress = () => {
Alert.alert( Alert.alert(
pb.origin, pb.origin,
'Co zrobić z tym linkiem?', 'What do you want to do with this link?',
[ [
{ {
text: 'Otwórz w przeglądarce (diagnostyka)', text: 'Open in browser (diagnostics)',
onPress: async () => { onPress: async () => {
try { try {
const url = pb.page_url || pb.embed_url; const url = pb.page_url || pb.embed_url;
if (url) { if (url) {
await Linking.openURL(url); await Linking.openURL(url);
} else { } else {
Alert.alert('Brak URL', 'Ten playback nie ma page_url do otworzenia.'); Alert.alert('No URL', 'This playback has no page_url to open.');
} }
} catch (e) { } catch (e) {
Alert.alert('Nie udało się otworzyć', e instanceof Error ? e.message : String(e)); Alert.alert('Could not open', e instanceof Error ? e.message : String(e));
} }
}, },
}, },
{ {
text: 'Oznacz jako nieprawidłowy', text: 'Mark as invalid',
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
try { try {
await client.markMoviePlaybackDead(movieId, pb.id); await client.markMoviePlaybackDead(movieId, pb.id);
queryClient.invalidateQueries({ queryKey: ['movie', movieId] }); queryClient.invalidateQueries({ queryKey: ['movie', movieId] });
} catch (e) { } catch (e) {
Alert.alert('Nie udało się', e instanceof Error ? e.message : String(e)); Alert.alert('Failed', e instanceof Error ? e.message : String(e));
} }
}, },
}, },
{ text: 'Anuluj', style: 'cancel' }, { text: 'Cancel', style: 'cancel' },
], ],
); );
}; };

View file

@ -57,14 +57,14 @@ export function PinEntry({ title, error, onSubmit, onCancel }: Props) {
<View style={styles.actions}> <View style={styles.actions}>
<Pressable style={styles.ghostBtn} onPress={onCancel}> <Pressable style={styles.ghostBtn} onPress={onCancel}>
<Text style={styles.ghostText}>Anuluj</Text> <Text style={styles.ghostText}>Cancel</Text>
</Pressable> </Pressable>
<Pressable <Pressable
style={[styles.primaryBtn, pin.length < 4 && styles.primaryBtnDisabled]} style={[styles.primaryBtn, pin.length < 4 && styles.primaryBtnDisabled]}
disabled={pin.length < 4} disabled={pin.length < 4}
onPress={() => onSubmit(pin)} onPress={() => onSubmit(pin)}
> >
<Text style={styles.primaryText}>Dalej</Text> <Text style={styles.primaryText}>Next</Text>
</Pressable> </Pressable>
</View> </View>
</View> </View>

View file

@ -50,7 +50,7 @@ export function PlaybackQualityModal({
> >
<Pressable style={styles.backdrop} onPress={onCancel}> <Pressable style={styles.backdrop} onPress={onCancel}>
<Pressable style={styles.sheet} onPress={(e) => e.stopPropagation()}> <Pressable style={styles.sheet} onPress={(e) => e.stopPropagation()}>
<Text style={styles.title}>Wybierz jakość</Text> <Text style={styles.title}>Select quality</Text>
{sorted.map((link, i) => { {sorted.map((link, i) => {
const px = qualityToInt(link.quality); const px = qualityToInt(link.quality);
return ( return (
@ -70,7 +70,7 @@ export function PlaybackQualityModal({
); );
})} })}
<Pressable style={styles.cancel} onPress={onCancel}> <Pressable style={styles.cancel} onPress={onCancel}>
<Text style={styles.cancelText}>Anuluj</Text> <Text style={styles.cancelText}>Cancel</Text>
</Pressable> </Pressable>
</Pressable> </Pressable>
</Pressable> </Pressable>

View file

@ -606,7 +606,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
<View style={styles.overlay}> <View style={styles.overlay}>
<Text style={styles.errorTitle}>Playback failed</Text> <Text style={styles.errorTitle}>Playback failed</Text>
<Text style={styles.errorBody}> <Text style={styles.errorBody}>
{playerError?.message ?? 'Stream nie odpalił się.'} {playerError?.message ?? 'The stream did not start.'}
</Text> </Text>
<Pressable style={styles.btn} onPress={() => nav.goBack()}> <Pressable style={styles.btn} onPress={() => nav.goBack()}>
<Text style={styles.btnText}>Back</Text> <Text style={styles.btnText}>Back</Text>
@ -1129,7 +1129,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
return ( return (
<View style={[styles.root, styles.overlay]}> <View style={[styles.root, styles.overlay]}>
<ActivityIndicator color={theme.fg} size="large" /> <ActivityIndicator color={theme.fg} size="large" />
<Text style={styles.overlayText}>Resolwuję bezpośredni link...</Text> <Text style={styles.overlayText}>Resolving direct link...</Text>
</View> </View>
); );
} }
@ -1153,7 +1153,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
borderRadius: 8, borderRadius: 8,
}} }}
> >
<Text style={{ color: theme.fg, fontWeight: '600' }}>Otwórz w WebView</Text> <Text style={{ color: theme.fg, fontWeight: '600' }}>Open in WebView</Text>
</Pressable> </Pressable>
</View> </View>
); );
@ -1197,7 +1197,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
}) })
} }
> >
<Text style={styles.extractText}> Otwórz w native playerze</Text> <Text style={styles.extractText}> Open in native player</Text>
<Text style={styles.extractSub}>{extractedUrl.slice(0, 80)}</Text> <Text style={styles.extractSub}>{extractedUrl.slice(0, 80)}</Text>
</Pressable> </Pressable>
)} )}

View file

@ -521,7 +521,7 @@ function PlaybackButton({
const embedLinks = unique.filter((l) => !l.stream_url && !!l.embed_url); const embedLinks = unique.filter((l) => !l.stream_url && !!l.embed_url);
if (directLinks.length === 0 && embedLinks.length === 0) { if (directLinks.length === 0 && embedLinks.length === 0) {
Alert.alert('No stream', 'porn-app nie zwrócił żadnego stream URL — fallback do strony.'); Alert.alert('No stream', 'porn-app did not return any stream URL — falling back to the page.');
await openUrl(source.page_url); await openUrl(source.page_url);
return; return;
} }
@ -567,8 +567,8 @@ function PlaybackButton({
// lub tube page 404/410). Refresh sceny żeby ten button zniknął z listy. // lub tube page 404/410). Refresh sceny żeby ten button zniknął z listy.
if (e instanceof ApiError && e.status === 410) { if (e instanceof ApiError && e.status === 410) {
Alert.alert( Alert.alert(
'Link martwy', 'Dead link',
'Tube usunął ten film. Oznaczyliśmy źródło, więcej go nie zobaczysz.', 'The tube removed this video. We marked the source, you won\'t see it again.',
); );
queryClient.invalidateQueries({ queryKey: ['scene', sceneId] }); queryClient.invalidateQueries({ queryKey: ['scene', sceneId] });
queryClient.invalidateQueries({ queryKey: ['scenes'] }); queryClient.invalidateQueries({ queryKey: ['scenes'] });
@ -579,8 +579,8 @@ function PlaybackButton({
// następne kliknięcie pewnie zadziała. // następne kliknięcie pewnie zadziała.
if (e instanceof ApiError && e.status === 503) { if (e instanceof ApiError && e.status === 503) {
Alert.alert( Alert.alert(
'Spróbuj ponownie', 'Try again',
'Chwilowy problem z hosterem. Kliknij Play ponownie za moment.', 'Temporary problem with the host. Tap Play again in a moment.',
); );
return; return;
} }
@ -638,25 +638,25 @@ function PlaybackButton({
const onLongPress = () => { const onLongPress = () => {
Alert.alert( Alert.alert(
label, label,
'Co zrobić z tym linkiem?', 'What do you want to do with this link?',
[ [
{ {
text: 'Otwórz w przeglądarce (diagnostyka)', text: 'Open in browser (diagnostics)',
onPress: async () => { onPress: async () => {
try { try {
const url = source.page_url || source.embed_url || source.stream_url; const url = source.page_url || source.embed_url || source.stream_url;
if (url) { if (url) {
await Linking.openURL(url); await Linking.openURL(url);
} else { } else {
Alert.alert('Brak URL', 'Ten playback nie ma page_url do otworzenia.'); Alert.alert('No URL', 'This playback has no page_url to open.');
} }
} catch (e) { } catch (e) {
Alert.alert('Nie udało się otworzyć', e instanceof Error ? e.message : String(e)); Alert.alert('Could not open', e instanceof Error ? e.message : String(e));
} }
}, },
}, },
{ {
text: 'Oznacz jako nieprawidłowy', text: 'Mark as invalid',
style: 'destructive', style: 'destructive',
onPress: async () => { onPress: async () => {
try { try {
@ -664,11 +664,11 @@ function PlaybackButton({
queryClient.invalidateQueries({ queryKey: ['scene', sceneId] }); queryClient.invalidateQueries({ queryKey: ['scene', sceneId] });
queryClient.invalidateQueries({ queryKey: ['scenes'] }); queryClient.invalidateQueries({ queryKey: ['scenes'] });
} catch (e) { } catch (e) {
Alert.alert('Nie udało się', e instanceof Error ? e.message : String(e)); Alert.alert('Failed', e instanceof Error ? e.message : String(e));
} }
}, },
}, },
{ text: 'Anuluj', style: 'cancel' }, { text: 'Cancel', style: 'cancel' },
], ],
); );
}; };

View file

@ -168,7 +168,7 @@ export function ScenesFilterModal({
style={styles.search} style={styles.search}
value={draft.origin} value={draft.origin}
onChangeText={(v) => setDraft({ ...draft, origin: v })} onChangeText={(v) => setDraft({ ...draft, origin: v })}
placeholder="np. hqporner, porntrex, xnxx — puste = wszystkie" placeholder="e.g. hqporner, porntrex, xnxx — empty = all"
placeholderTextColor={theme.muted} placeholderTextColor={theme.muted}
autoCapitalize="none" autoCapitalize="none"
autoCorrect={false} autoCorrect={false}

View file

@ -82,12 +82,12 @@ export function SiteScenesScreen() {
onPress={() => setFilterOpen(true)} onPress={() => setFilterOpen(true)}
> >
<Text style={styles.filterBtnText}> <Text style={styles.filterBtnText}>
Tagi{selectedTags.length > 0 ? ` ${selectedTags.length}` : ''} Tags{selectedTags.length > 0 ? ` ${selectedTags.length}` : ''}
</Text> </Text>
</Pressable> </Pressable>
{selectedTags.length > 0 ? ( {selectedTags.length > 0 ? (
<Pressable style={styles.clearBtn} onPress={() => setSelectedTags([])}> <Pressable style={styles.clearBtn} onPress={() => setSelectedTags([])}>
<Text style={styles.clearBtnText}>Wyczyść</Text> <Text style={styles.clearBtnText}>Clear</Text>
</Pressable> </Pressable>
) : null} ) : null}
</View> </View>
@ -181,14 +181,14 @@ function TagPickerModal({
<View style={modalStyles.backdrop}> <View style={modalStyles.backdrop}>
<View style={modalStyles.sheet}> <View style={modalStyles.sheet}>
<View style={modalStyles.header}> <View style={modalStyles.header}>
<Text style={modalStyles.title}>Filtruj po tagach</Text> <Text style={modalStyles.title}>Filter by tags</Text>
<Pressable onPress={onClose}> <Pressable onPress={onClose}>
<Text style={modalStyles.close}></Text> <Text style={modalStyles.close}></Text>
</Pressable> </Pressable>
</View> </View>
<TextInput <TextInput
style={modalStyles.search} style={modalStyles.search}
placeholder="Szukaj tagu…" placeholder="Search tags…"
placeholderTextColor={theme.muted} placeholderTextColor={theme.muted}
value={q} value={q}
onChangeText={setQ} onChangeText={setQ}
@ -213,7 +213,7 @@ function TagPickerModal({
); );
})} })}
{tags.length === 0 ? ( {tags.length === 0 ? (
<Text style={modalStyles.muted}>Brak wyników</Text> <Text style={modalStyles.muted}>No results</Text>
) : null} ) : null}
</ScrollView> </ScrollView>
)} )}
@ -226,7 +226,7 @@ function TagPickerModal({
<Text <Text
style={[modalStyles.footerBtnText, selected.length === 0 && { opacity: 0.4 }]} style={[modalStyles.footerBtnText, selected.length === 0 && { opacity: 0.4 }]}
> >
Wyczyść Clear
</Text> </Text>
</Pressable> </Pressable>
<Pressable <Pressable
@ -234,7 +234,7 @@ function TagPickerModal({
onPress={() => onApply(selected)} onPress={() => onApply(selected)}
> >
<Text style={modalStyles.footerBtnTextPrimary}> <Text style={modalStyles.footerBtnTextPrimary}>
Zastosuj{selected.length > 0 ? ` (${selected.length})` : ''} Apply{selected.length > 0 ? ` (${selected.length})` : ''}
</Text> </Text>
</Pressable> </Pressable>
</View> </View>