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 () => {
if (!client) {
Alert.alert('Bug report', 'Brak połączenia z backendem.');
Alert.alert('Bug report', 'No connection to the backend.');
return;
}
if (!message.trim()) {
Alert.alert('Bug report', 'Wpisz krótki opis.');
Alert.alert('Bug report', 'Enter a short description.');
return;
}
setSubmitting(true);
@ -136,9 +136,9 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
setOpen(false);
setMessage('');
setScreenshot(null);
Alert.alert('Bug report', 'Wysłano. Dzięki!');
Alert.alert('Bug report', 'Sent. Thanks!');
} 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 {
setSubmitting(false);
}
@ -164,7 +164,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
>
<Pressable style={styles.backdropTap} onPress={() => setOpen(false)} />
<View style={styles.sheet}>
<Text style={styles.title}>Zgłoś buga</Text>
<Text style={styles.title}>Report a bug</Text>
{screenshot ? (
<View style={styles.preview}>
@ -174,7 +174,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
resizeMode="contain"
/>
<View style={styles.toggleRow}>
<Text style={styles.toggleLabel}>Dołącz screenshot</Text>
<Text style={styles.toggleLabel}>Attach screenshot</Text>
<Switch
value={includeScreenshot}
onValueChange={setIncludeScreenshot}
@ -188,7 +188,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
</View>
) : (
<Text style={styles.noScreenshot}>
Screenshot niedostępny (capture fail). Wyślę sam tekst.
Screenshot unavailable (capture failed). Sending text only.
</Text>
)}
@ -196,7 +196,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
style={styles.input}
value={message}
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}
multiline
autoFocus
@ -208,7 +208,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
onPress={() => setOpen(false)}
disabled={submitting}
>
<Text style={styles.btnText}>Anuluj</Text>
<Text style={styles.btnText}>Cancel</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.btn, styles.btnSend]}
@ -218,7 +218,7 @@ export function BugReportFAB({ client, appVersion, navRef }: Props) {
{submitting ? (
<ActivityIndicator color={theme.fg} />
) : (
<Text style={styles.btnText}>Wyślij</Text>
<Text style={styles.btnText}>Send</Text>
)}
</TouchableOpacity>
</View>

View file

@ -44,7 +44,7 @@ export async function setTimeoutSeconds(s: number): 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);
}
@ -66,8 +66,8 @@ export async function biometricAvailable(): Promise<boolean> {
export async function authenticateBiometric(): Promise<boolean> {
const res = await LocalAuthentication.authenticateAsync({
promptMessage: 'Odblokuj Goon',
cancelLabel: 'Użyj PIN',
promptMessage: 'Unlock Goon',
cancelLabel: 'Use PIN',
disableDeviceFallback: true,
});
return res.success;

View file

@ -103,7 +103,7 @@ export async function resolveDoodStream(
// 30x → resolve Location, kontynuuj. Inaczej traktuj jako final.
if (r.status >= 300 && r.status < 400) {
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 {
currentUrl = new URL(loc, currentUrl).toString();
} catch {
@ -147,7 +147,7 @@ async function _continueAfterFetch(
// Tu odsyłamy do WebView fallback. Sam `turnstile` w HTML nie wystarcza —
// pełna strona playera ZAWIERA opcjonalny turnstile container.
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' };
}

View file

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

View file

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

View file

@ -200,7 +200,7 @@ function WatchChip({
if (parts.length > 1) {
Alert.alert(
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) => ({
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;
@ -246,36 +246,36 @@ function WatchChip({
const onLongPress = () => {
Alert.alert(
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 () => {
try {
const url = pb.page_url || pb.embed_url;
if (url) {
await Linking.openURL(url);
} 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) {
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',
onPress: async () => {
try {
await client.markMoviePlaybackDead(movieId, pb.id);
queryClient.invalidateQueries({ queryKey: ['movie', movieId] });
} 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}>
<Pressable style={styles.ghostBtn} onPress={onCancel}>
<Text style={styles.ghostText}>Anuluj</Text>
<Text style={styles.ghostText}>Cancel</Text>
</Pressable>
<Pressable
style={[styles.primaryBtn, pin.length < 4 && styles.primaryBtnDisabled]}
disabled={pin.length < 4}
onPress={() => onSubmit(pin)}
>
<Text style={styles.primaryText}>Dalej</Text>
<Text style={styles.primaryText}>Next</Text>
</Pressable>
</View>
</View>

View file

@ -50,7 +50,7 @@ export function PlaybackQualityModal({
>
<Pressable style={styles.backdrop} onPress={onCancel}>
<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) => {
const px = qualityToInt(link.quality);
return (
@ -70,7 +70,7 @@ export function PlaybackQualityModal({
);
})}
<Pressable style={styles.cancel} onPress={onCancel}>
<Text style={styles.cancelText}>Anuluj</Text>
<Text style={styles.cancelText}>Cancel</Text>
</Pressable>
</Pressable>
</Pressable>

View file

@ -606,7 +606,7 @@ function NativeVideoPlayer({ params }: { params: RouteParams }) {
<View style={styles.overlay}>
<Text style={styles.errorTitle}>Playback failed</Text>
<Text style={styles.errorBody}>
{playerError?.message ?? 'Stream nie odpalił się.'}
{playerError?.message ?? 'The stream did not start.'}
</Text>
<Pressable style={styles.btn} onPress={() => nav.goBack()}>
<Text style={styles.btnText}>Back</Text>
@ -1129,7 +1129,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
return (
<View style={[styles.root, styles.overlay]}>
<ActivityIndicator color={theme.fg} size="large" />
<Text style={styles.overlayText}>Resolwuję bezpośredni link...</Text>
<Text style={styles.overlayText}>Resolving direct link...</Text>
</View>
);
}
@ -1153,7 +1153,7 @@ function EmbedWebViewPlayer({ params }: { params: RouteParams }) {
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>
</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>
</Pressable>
)}

View file

@ -521,7 +521,7 @@ function PlaybackButton({
const embedLinks = unique.filter((l) => !l.stream_url && !!l.embed_url);
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);
return;
}
@ -567,8 +567,8 @@ function PlaybackButton({
// lub tube page 404/410). Refresh sceny żeby ten button zniknął z listy.
if (e instanceof ApiError && e.status === 410) {
Alert.alert(
'Link martwy',
'Tube usunął ten film. Oznaczyliśmy źródło, więcej go nie zobaczysz.',
'Dead link',
'The tube removed this video. We marked the source, you won\'t see it again.',
);
queryClient.invalidateQueries({ queryKey: ['scene', sceneId] });
queryClient.invalidateQueries({ queryKey: ['scenes'] });
@ -579,8 +579,8 @@ function PlaybackButton({
// następne kliknięcie pewnie zadziała.
if (e instanceof ApiError && e.status === 503) {
Alert.alert(
'Spróbuj ponownie',
'Chwilowy problem z hosterem. Kliknij Play ponownie za moment.',
'Try again',
'Temporary problem with the host. Tap Play again in a moment.',
);
return;
}
@ -638,25 +638,25 @@ function PlaybackButton({
const onLongPress = () => {
Alert.alert(
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 () => {
try {
const url = source.page_url || source.embed_url || source.stream_url;
if (url) {
await Linking.openURL(url);
} 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) {
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',
onPress: async () => {
try {
@ -664,11 +664,11 @@ function PlaybackButton({
queryClient.invalidateQueries({ queryKey: ['scene', sceneId] });
queryClient.invalidateQueries({ queryKey: ['scenes'] });
} 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}
value={draft.origin}
onChangeText={(v) => setDraft({ ...draft, origin: v })}
placeholder="np. hqporner, porntrex, xnxx — puste = wszystkie"
placeholder="e.g. hqporner, porntrex, xnxx — empty = all"
placeholderTextColor={theme.muted}
autoCapitalize="none"
autoCorrect={false}

View file

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