feat(onboarding): protection pointer redesign + i18n screenshots + lockedIn fix
## Protection Pre-Explainer: External Pointer Vorher: Pulse-Ring absolute-positioniert IM Screenshot — Position musste per-locale fine-tuned werden weil Apple-Dialog-Höhe variiert (DE/EN/FR/AR haben unterschiedliche Text-Längen → Dialog hat verschiedene Höhen → Erlauben-Button rutscht). Jetzt: animierter Pfeil + Label-Pill UNTER dem Screenshot. Dimensions- agnostic, funktioniert in allen 4 Sprachen ohne Locale-spezifische Magie. - ScreenshotPointer komplett refactored: caret-up + bouncing pill mit Button-Label-Text (z.B. 'Tippe "Erlauben"' / 'Tap "Allow"' / etc.) - onboardingAssets.ts: getPointerPosition deprecated/entfernt - ProtectionSlide nutzt neue API mit buttonLabelKey - 4 Locales: dialog_button_allow + dialog_button_continue - tap_marker_hint refined (kein "roter Marker"-Ref mehr) ## i18n-aware Screenshots en/fr/ar Permission-Dialog-Screenshots zur Map ergänzt. Resolver fällt auf de zurück wenn andere Sprache fehlt. ## Dynamic Sizing ProtectionSlide nutzt useWindowDimensions: height: min(320, max(200, screenH * 0.32)) → passt auf iPhone SE (213px) bis Pro Max (320px capped) ohne Scroll. OnboardingShell ScrollView-Padding reduziert (16→12 top, 24→16 bottom). ProtectionSlide-Spacing tightened. ## Blocker: lockedIn Fix Bug: `lockedIn = appDeletionLockActive` ignorierte URL-Filter-State — wenn User nur FC aktivierte (ohne URL-Filter), zeigte App grünen "Schutz aktiv"-Banner obwohl URL-Filter aus war. Fix: lockedIn = urlFilter && appDeletionLock → Beide müssen wirklich aktiv sein für den grünen Banner. ## LayerSwitchCard: lockedHint Prop Optional Hint-Text der unter dem active Layer angezeigt wird, z.B. "System-gesperrt. Nur in iOS-Einstellungen → Bildschirmzeit → Verwaltung durch ReBreak deaktivierbar.". Wird für iOS App-Lock-Card genutzt. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
1596a4ea7a
commit
33aa3464b8
@ -74,7 +74,11 @@ export default function BlockerScreen() {
|
||||
const urlFilterActive = state?.layers.urlFilter === true;
|
||||
const familyControlsActive = state?.layers.familyControls === true;
|
||||
const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true;
|
||||
const lockedIn = appDeletionLockActive;
|
||||
// "lockedIn" = beide Layer aktiv: URL-Filter (echter Schutz) UND App-Lock
|
||||
// (Hardening). Family-Controls ALLEINE = kein Schutz, nur denyAppRemoval —
|
||||
// ohne URL-Filter sieht der User trotzdem Glücksspielseiten. Daher BEIDE
|
||||
// müssen an sein damit der "Schutz aktiv"-Banner gezeigt wird.
|
||||
const lockedIn = urlFilterActive && appDeletionLockActive;
|
||||
|
||||
const urlFilterActiveRef = useRef(urlFilterActive);
|
||||
useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]);
|
||||
@ -270,6 +274,7 @@ export default function BlockerScreen() {
|
||||
active={appDeletionLockActive}
|
||||
onActivate={handleActivateFamilyControls}
|
||||
warning={t('blocker.layers_app_lock_warning')}
|
||||
lockedHint={t('blocker.layers_app_lock_locked_hint')}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 91 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 125 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@ -11,11 +11,16 @@ type Props = {
|
||||
/** Aktivierung (zeigt System-Dialog). UI hat nur read-on-flow,
|
||||
* Toggle-off ist nicht hier — passiert nur über Cooldown. */
|
||||
onActivate: () => Promise<{ enabled: boolean; error?: string }>;
|
||||
/** Optional: Hinweistext unter Subtitle für commit-heavy Layer. */
|
||||
/** Optional: Hinweistext unter Subtitle. Wird OBEN angezeigt wenn inactive
|
||||
* (warnung vor Aktivierung) UND UNTEN wenn active+lockedHint (Hinweis
|
||||
* wo man's wieder deaktiviert — bei Family-Controls/Screen-Time z.B.). */
|
||||
warning?: string;
|
||||
/** Optional: Hinweis-Text wenn active=true (z.B. "Nur in iOS-Settings
|
||||
* deaktivierbar"). Macht die System-managed-Natur klar. */
|
||||
lockedHint?: string;
|
||||
};
|
||||
|
||||
export function LayerSwitchCard({ icon, title, subtitle, active, onActivate, warning }: Props) {
|
||||
export function LayerSwitchCard({ icon, title, subtitle, active, onActivate, warning, lockedHint }: Props) {
|
||||
const [busy, setBusy] = useState(false);
|
||||
|
||||
async function handleSwitch(v: boolean) {
|
||||
@ -106,6 +111,33 @@ export function LayerSwitchCard({ icon, title, subtitle, active, onActivate, war
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{lockedHint && active && (
|
||||
<View
|
||||
style={{
|
||||
marginTop: 10,
|
||||
paddingTop: 10,
|
||||
borderTopWidth: 1,
|
||||
borderTopColor: 'rgba(0,0,0,0.06)',
|
||||
flexDirection: 'row',
|
||||
gap: 8,
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Ionicons name="lock-closed" size={14} color="#16a34a" style={{ marginTop: 1 }} />
|
||||
<Text
|
||||
style={{
|
||||
flex: 1,
|
||||
fontSize: 11,
|
||||
fontFamily: 'Nunito_600SemiBold',
|
||||
color: '#16a34a',
|
||||
lineHeight: 16,
|
||||
}}
|
||||
>
|
||||
{lockedHint}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -48,9 +48,9 @@ export function OnboardingShell({
|
||||
<ScrollView
|
||||
style={{ flex: 1 }}
|
||||
contentContainerStyle={{
|
||||
padding: 20,
|
||||
paddingTop: 16,
|
||||
paddingBottom: 24,
|
||||
paddingHorizontal: 20,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 16,
|
||||
flexGrow: 1,
|
||||
}}
|
||||
keyboardShouldPersistTaps="handled"
|
||||
|
||||
@ -1,97 +1,113 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Animated, Easing, View } from 'react-native';
|
||||
import { Animated, Easing, Text, View } from 'react-native';
|
||||
import { Ionicons } from '@expo/vector-icons';
|
||||
import { useColors } from '../../lib/theme';
|
||||
|
||||
/**
|
||||
* Animierter Pulse-Marker für Screenshot-Overlays im Onboarding-Pre-Explainer.
|
||||
* "Tap-Here"-Indicator UNTER einem Screenshot.
|
||||
*
|
||||
* Positioniert via Prozent-Koordinaten relativ zum Container — so unabhängig
|
||||
* von Screen-Größe + iPad-Skalierung. Pulsiert (Outer-Ring) + arrow pointer
|
||||
* darunter zeigt klar wo der User tappen muss.
|
||||
* Vorher: absolut-positionierter Pulse-Ring INNERHALB des Screenshots
|
||||
* → musste per-locale fine-tuned werden weil Apple-Dialog-Dimensionen
|
||||
* pro Sprache variieren (DE-Text länger als EN, Modal höher, Button
|
||||
* rutscht runter…).
|
||||
*
|
||||
* <View style={{ aspectRatio: 9/16, ... }}>
|
||||
* <Image source={...} />
|
||||
* <ScreenshotPointer xPercent={50} yPercent={87} />
|
||||
* </View>
|
||||
* Jetzt: render-unter dem Screenshot. Layout-agnostic. Animation lenkt
|
||||
* Aufmerksamkeit auf die richtige Region ohne pixel-genaue Position.
|
||||
*
|
||||
* `xPercent=50, yPercent=87` ist die typische Position für "Erlauben"/
|
||||
* "Fortfahren" am Bottom des iOS-Permission-Dialogs.
|
||||
* ┌──────────────────────────┐
|
||||
* │ <iOS Dialog Screenshot> │
|
||||
* │ [Nicht erlauben] │
|
||||
* │ [Erlauben] │
|
||||
* └──────────────────────────┘
|
||||
* ▲
|
||||
* ┌─────────────┐
|
||||
* │ ▲ Tippe │ ← Animated bouncing pill mit Pfeil + Label
|
||||
* │ "Erlauben" │
|
||||
* └─────────────┘
|
||||
*/
|
||||
export function ScreenshotPointer({
|
||||
xPercent,
|
||||
yPercent,
|
||||
color = '#dc2626',
|
||||
buttonLabel,
|
||||
}: {
|
||||
/** Horizontale Position als 0..100 vom Container-Width */
|
||||
xPercent: number;
|
||||
/** Vertikale Position als 0..100 vom Container-Height */
|
||||
yPercent: number;
|
||||
/** Pulse-Color. Default rot — fällt auf, signalisiert "wichtig". */
|
||||
color?: string;
|
||||
/** Der Text auf dem korrekten Button im iOS-Dialog. Wird ins Label übernommen. */
|
||||
buttonLabel: string;
|
||||
}) {
|
||||
const colors = useColors();
|
||||
const bounce = useRef(new Animated.Value(0)).current;
|
||||
const pulse = useRef(new Animated.Value(0)).current;
|
||||
|
||||
useEffect(() => {
|
||||
const loop = Animated.loop(
|
||||
const bounceLoop = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulse, {
|
||||
Animated.timing(bounce, {
|
||||
toValue: 1,
|
||||
duration: 1100,
|
||||
duration: 600,
|
||||
useNativeDriver: true,
|
||||
easing: Easing.out(Easing.cubic),
|
||||
}),
|
||||
Animated.timing(pulse, {
|
||||
Animated.timing(bounce, {
|
||||
toValue: 0,
|
||||
duration: 1100,
|
||||
duration: 600,
|
||||
useNativeDriver: true,
|
||||
easing: Easing.in(Easing.cubic),
|
||||
}),
|
||||
]),
|
||||
);
|
||||
loop.start();
|
||||
return () => loop.stop();
|
||||
}, [pulse]);
|
||||
const pulseLoop = Animated.loop(
|
||||
Animated.sequence([
|
||||
Animated.timing(pulse, { toValue: 1, duration: 900, useNativeDriver: true }),
|
||||
Animated.timing(pulse, { toValue: 0, duration: 900, useNativeDriver: true }),
|
||||
]),
|
||||
);
|
||||
bounceLoop.start();
|
||||
pulseLoop.start();
|
||||
return () => {
|
||||
bounceLoop.stop();
|
||||
pulseLoop.stop();
|
||||
};
|
||||
}, [bounce, pulse]);
|
||||
|
||||
const scale = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.45] });
|
||||
const opacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.95, 0.2] });
|
||||
const translateY = bounce.interpolate({ inputRange: [0, 1], outputRange: [0, -6] });
|
||||
const arrowOpacity = pulse.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1] });
|
||||
const arrowScale = pulse.interpolate({ inputRange: [0, 1], outputRange: [1, 1.12] });
|
||||
|
||||
return (
|
||||
<View
|
||||
pointerEvents="none"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${yPercent}%`,
|
||||
left: `${xPercent}%`,
|
||||
// Marker center'n auf der Position
|
||||
transform: [{ translateX: -28 }, { translateY: -28 }],
|
||||
width: 56,
|
||||
height: 56,
|
||||
}}
|
||||
>
|
||||
{/* Outer pulse-ring */}
|
||||
<View style={{ alignItems: 'center', marginTop: 8 }}>
|
||||
{/* Animated Up-Arrow zeigt auf den Screenshot (auf dessen unteren Button) */}
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
borderWidth: 3,
|
||||
borderColor: color,
|
||||
opacity,
|
||||
transform: [{ scale }],
|
||||
opacity: arrowOpacity,
|
||||
transform: [{ translateY }, { scale: arrowScale }],
|
||||
}}
|
||||
/>
|
||||
{/* Inner solid ring */}
|
||||
<View
|
||||
>
|
||||
<Ionicons name="caret-up" size={28} color={colors.brandOrange} />
|
||||
</Animated.View>
|
||||
|
||||
{/* Label-Pille */}
|
||||
<Animated.View
|
||||
style={{
|
||||
position: 'absolute',
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: 28,
|
||||
borderWidth: 3,
|
||||
borderColor: color,
|
||||
backgroundColor: 'transparent',
|
||||
marginTop: 4,
|
||||
backgroundColor: colors.brandOrange,
|
||||
borderRadius: 999,
|
||||
paddingVertical: 8,
|
||||
paddingHorizontal: 16,
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
transform: [{ translateY }],
|
||||
}}
|
||||
/>
|
||||
>
|
||||
<Text style={{ fontSize: 16 }}>👆</Text>
|
||||
<Text
|
||||
style={{
|
||||
fontFamily: 'Nunito_700Bold',
|
||||
fontSize: 14,
|
||||
color: '#ffffff',
|
||||
letterSpacing: 0.2,
|
||||
}}
|
||||
>
|
||||
{buttonLabel}
|
||||
</Text>
|
||||
</Animated.View>
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import { useState } from 'react';
|
||||
import { Alert, Image, Platform, Text, View } from 'react-native';
|
||||
import { Alert, Image, Platform, Text, useWindowDimensions, View } from 'react-native';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useColors } from '../../../lib/theme';
|
||||
import { apiFetch } from '../../../lib/api';
|
||||
import { invalidateMe } from '../../../hooks/useMe';
|
||||
import { protection } from '../../../lib/protection';
|
||||
import { getPermissionScreenshot, getPointerPosition } from '../../../lib/onboardingAssets';
|
||||
import { getPermissionScreenshot } from '../../../lib/onboardingAssets';
|
||||
import { OnboardingShell } from '../OnboardingShell';
|
||||
import { LyraBubble } from '../LyraBubble';
|
||||
import { CTABar } from '../CTABar';
|
||||
@ -181,9 +181,18 @@ function PreExplainer({
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const colors = useColors();
|
||||
const { height: screenH } = useWindowDimensions();
|
||||
const lang = i18n.language || 'de';
|
||||
const screenshot = getPermissionScreenshot(dialog, lang);
|
||||
const pointer = getPointerPosition(dialog);
|
||||
const buttonLabelKey =
|
||||
dialog === 'url_filter'
|
||||
? 'onboarding.protection.dialog_button_allow'
|
||||
: 'onboarding.protection.dialog_button_continue';
|
||||
|
||||
// Dynamische Screenshot-Höhe: Auf kleinen Phones (SE/mini ~667-844 pt)
|
||||
// capped damit alles + CTA-Bar ohne Scroll passt. Auf großen Phones/iPad
|
||||
// skaliert es mit. Min 200, Max 320.
|
||||
const screenshotHeight = Math.min(320, Math.max(200, screenH * 0.32));
|
||||
|
||||
return (
|
||||
<OnboardingShell
|
||||
@ -201,10 +210,10 @@ function PreExplainer({
|
||||
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 22,
|
||||
marginTop: 14,
|
||||
fontFamily: 'Nunito_700Bold',
|
||||
fontSize: 14,
|
||||
letterSpacing: 0.5,
|
||||
fontSize: 12,
|
||||
letterSpacing: 0.6,
|
||||
color: colors.textMuted,
|
||||
textTransform: 'uppercase',
|
||||
textAlign: 'center',
|
||||
@ -213,14 +222,14 @@ function PreExplainer({
|
||||
{t(titleKey)}
|
||||
</Text>
|
||||
|
||||
{/* Screenshot mit Pulse-Pointer auf den korrekten Button */}
|
||||
{/* Screenshot — sauber ohne Overlay. Dynamisch dimensioniert. */}
|
||||
<View
|
||||
style={{
|
||||
marginTop: 12,
|
||||
marginTop: 8,
|
||||
alignSelf: 'center',
|
||||
width: '92%',
|
||||
aspectRatio: 0.9, // Screenshots sind etwa quadratisch / leicht hochkant
|
||||
borderRadius: 18,
|
||||
height: screenshotHeight,
|
||||
aspectRatio: 0.9,
|
||||
borderRadius: 16,
|
||||
overflow: 'hidden',
|
||||
backgroundColor: colors.surfaceElevated,
|
||||
borderWidth: 1,
|
||||
@ -232,15 +241,17 @@ function PreExplainer({
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
resizeMode="contain"
|
||||
/>
|
||||
<ScreenshotPointer xPercent={pointer.xPercent} yPercent={pointer.yPercent} />
|
||||
</View>
|
||||
|
||||
{/* Animierter Pointer UNTER dem Screenshot — Dimensions-agnostic. */}
|
||||
<ScreenshotPointer buttonLabel={t(buttonLabelKey)} />
|
||||
|
||||
<Text
|
||||
style={{
|
||||
marginTop: 16,
|
||||
marginTop: 10,
|
||||
fontFamily: 'Nunito_400Regular',
|
||||
fontSize: 13,
|
||||
lineHeight: 19,
|
||||
fontSize: 12,
|
||||
lineHeight: 17,
|
||||
color: colors.textMuted,
|
||||
textAlign: 'center',
|
||||
paddingHorizontal: 8,
|
||||
|
||||
@ -12,19 +12,30 @@
|
||||
type Dialog = 'url_filter' | 'screen_time';
|
||||
type Lang = 'de' | 'en' | 'fr' | 'ar';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
/* eslint-disable @typescript-eslint/no-require-imports */
|
||||
const URL_FILTER_DE = require('../assets/onboarding/de/url_filter_permission.jpeg');
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
const SCREEN_TIME_DE = require('../assets/onboarding/de/screen_time_permission.jpeg');
|
||||
const URL_FILTER_EN = require('../assets/onboarding/en/url_filter_permission.jpeg');
|
||||
const URL_FILTER_FR = require('../assets/onboarding/fr/url_filter_permission.jpeg');
|
||||
const URL_FILTER_AR = require('../assets/onboarding/ar/url_filter_permission.jpeg');
|
||||
|
||||
const SCREEN_TIME_DE = require('../assets/onboarding/de/screen_time_permission.jpeg');
|
||||
const SCREEN_TIME_EN = require('../assets/onboarding/en/screen_time_permission.jpeg');
|
||||
const SCREEN_TIME_FR = require('../assets/onboarding/fr/screen_time_permission.jpeg');
|
||||
const SCREEN_TIME_AR = require('../assets/onboarding/ar/screen_time_permission.jpeg');
|
||||
/* eslint-enable @typescript-eslint/no-require-imports */
|
||||
|
||||
// TODO: en / fr / ar Screenshots ergänzen sobald User uploaded:
|
||||
// require('../assets/onboarding/en/url_filter_permission.jpeg'), etc.
|
||||
const SCREENSHOTS: Record<Dialog, Partial<Record<Lang, number>>> = {
|
||||
url_filter: {
|
||||
de: URL_FILTER_DE,
|
||||
en: URL_FILTER_EN,
|
||||
fr: URL_FILTER_FR,
|
||||
ar: URL_FILTER_AR,
|
||||
},
|
||||
screen_time: {
|
||||
de: SCREEN_TIME_DE,
|
||||
en: SCREEN_TIME_EN,
|
||||
fr: SCREEN_TIME_FR,
|
||||
ar: SCREEN_TIME_AR,
|
||||
},
|
||||
};
|
||||
|
||||
@ -40,21 +51,7 @@ export function getPermissionScreenshot(dialog: Dialog, lang: string): number {
|
||||
return map[normalized] ?? map.de!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wo der Pointer-Marker auf dem Screenshot positioniert werden soll.
|
||||
* Werte sind Prozent (0..100) relativ zum Container.
|
||||
*
|
||||
* Beide iOS-Dialoge haben den korrekten Button am Bottom (~85-88% Y, 50% X).
|
||||
* Falls Apple das Layout pro Locale anders rendert — pro-Lang-Overrides
|
||||
* hier ergänzen.
|
||||
*/
|
||||
export function getPointerPosition(dialog: Dialog): { xPercent: number; yPercent: number } {
|
||||
switch (dialog) {
|
||||
case 'url_filter':
|
||||
// Button "Erlauben" am unteren Rand des Modals
|
||||
return { xPercent: 50, yPercent: 86 };
|
||||
case 'screen_time':
|
||||
// Button "Fortfahren" am unteren Rand
|
||||
return { xPercent: 50, yPercent: 86 };
|
||||
}
|
||||
}
|
||||
// (Deprecated) getPointerPosition entfernt — der Pointer wird jetzt extern
|
||||
// UNTER dem Screenshot gerendert (ScreenshotPointer-Komponente), nicht mehr
|
||||
// per-percent overlayed. Damit entfällt die Notwendigkeit pixel-genaue
|
||||
// Positionen pro Locale + Dialog zu pflegen — siehe ScreenshotPointer.tsx.
|
||||
|
||||
@ -303,6 +303,7 @@
|
||||
"layers_app_lock_subtitle_active": "مقفل — الإيقاف فقط عبر فترة التهدئة",
|
||||
"layers_app_lock_subtitle_inactive": "يمنع إيقاف rebreak أو الفلتر في لحظة الاندفاع",
|
||||
"layers_app_lock_warning": "بمجرد التفعيل لا يمكنك إيقاف الحماية إلا عبر تهدئة 24 ساعة. هذا مقصود.",
|
||||
"layers_app_lock_locked_hint": "مقفل بواسطة النظام. الإيقاف فقط عبر إعدادات iOS → مدة استخدام الجهاز → الإدارة بواسطة ReBreak.",
|
||||
"layers_a11y_subtitle_active": "إمكانية الوصول نشطة — حماية التطبيق مفعّلة",
|
||||
"layers_a11y_subtitle_inactive": "إمكانية الوصول غير مفعّلة — قم بالإعداد الآن",
|
||||
"kpi_global_label": "النطاقات المحجوبة عالمياً",
|
||||
@ -443,7 +444,9 @@
|
||||
"cta_primary": "فعّل الحماية",
|
||||
"url_title": "الخطوة 1 من 2 — فلتر المحتوى",
|
||||
"lock_title": "الخطوة 2 من 2 — قفل التطبيق",
|
||||
"tap_marker_hint": "العلامة الحمراء تشير إلى الزر الصحيح. Apple يضع الأزرق الكبير في الأعلى («عدم السماح») — لا تقع في الفخ.",
|
||||
"tap_marker_hint": "Apple يضع الزر الأزرق الكبير في الأعلى («عدم السماح») — اضغط الزر السفلي، وليس العلوي.",
|
||||
"dialog_button_allow": "اضغط «السماح»",
|
||||
"dialog_button_continue": "اضغط «متابعة»",
|
||||
"applock_failed_title": "فشل قفل التطبيق",
|
||||
"applock_failed_msg": "يمكنك المحاولة مرة أخرى أو تخطي هذه الخطوة — فلتر URL يعمل بالفعل.",
|
||||
"applock_skip": "تخطّي",
|
||||
|
||||
@ -303,6 +303,7 @@
|
||||
"layers_app_lock_subtitle_active": "Verriegelt — Abschalten nur über die Abkühlphase",
|
||||
"layers_app_lock_subtitle_inactive": "Verhindert, dass du ReBreak oder den Filter im Impuls abschaltest",
|
||||
"layers_app_lock_warning": "Sobald aktiv kannst du den Schutz nur über einen 24-Stunden-Cooldown abschalten. Das ist gewollt.",
|
||||
"layers_app_lock_locked_hint": "System-gesperrt. Deaktivierung nur in iOS-Einstellungen → Bildschirmzeit → Verwaltung durch ReBreak.",
|
||||
"layers_a11y_subtitle_active": "Eingabehilfe aktiv — App-Schutz armiert",
|
||||
"layers_a11y_subtitle_inactive": "Eingabehilfe nicht aktiviert — jetzt einrichten",
|
||||
"kpi_global_label": "Geblockte Domains weltweit",
|
||||
@ -443,7 +444,9 @@
|
||||
"cta_primary": "Schutz aktivieren",
|
||||
"url_title": "Schritt 1 von 2 — Inhaltsfilter",
|
||||
"lock_title": "Schritt 2 von 2 — App-Schutz",
|
||||
"tap_marker_hint": "Der rote Marker zeigt den richtigen Button. Apple platziert den großen blauen oben (\"Nicht erlauben\") — bitte nicht reinfallen.",
|
||||
"tap_marker_hint": "Apple platziert den großen blauen Button oben (\"Nicht erlauben\") — bitte den UNTEREN Button tippen, nicht den oberen.",
|
||||
"dialog_button_allow": "Tippe \"Erlauben\"",
|
||||
"dialog_button_continue": "Tippe \"Fortfahren\"",
|
||||
"applock_failed_title": "App-Schutz fehlgeschlagen",
|
||||
"applock_failed_msg": "Du kannst es nochmal versuchen oder den Schritt überspringen — der URL-Filter läuft schon.",
|
||||
"applock_skip": "Überspringen",
|
||||
|
||||
@ -303,6 +303,7 @@
|
||||
"layers_app_lock_subtitle_active": "Locked — disable only via the cooldown",
|
||||
"layers_app_lock_subtitle_inactive": "Stops you from switching off ReBreak or the filter on impulse",
|
||||
"layers_app_lock_warning": "Once active, you can only disable protection through a 24-hour cooldown. That's by design.",
|
||||
"layers_app_lock_locked_hint": "System-locked. Only disable via iOS Settings → Screen Time → Management by ReBreak.",
|
||||
"layers_a11y_subtitle_active": "Accessibility active — app protection armed",
|
||||
"layers_a11y_subtitle_inactive": "Accessibility not enabled — set it up now",
|
||||
"kpi_global_label": "Domains blocked worldwide",
|
||||
@ -443,7 +444,9 @@
|
||||
"cta_primary": "Activate protection",
|
||||
"url_title": "Step 1 of 2 — Content filter",
|
||||
"lock_title": "Step 2 of 2 — App lock",
|
||||
"tap_marker_hint": "The red marker shows the correct button. Apple puts the big blue one on top (\"Don't Allow\") — please don't fall for it.",
|
||||
"tap_marker_hint": "Apple puts the big blue button on top (\"Don't Allow\") — please tap the BOTTOM button, not the top one.",
|
||||
"dialog_button_allow": "Tap \"Allow\"",
|
||||
"dialog_button_continue": "Tap \"Continue\"",
|
||||
"applock_failed_title": "App lock failed",
|
||||
"applock_failed_msg": "You can try again or skip this step — the URL filter is already running.",
|
||||
"applock_skip": "Skip",
|
||||
|
||||
@ -303,6 +303,7 @@
|
||||
"layers_app_lock_subtitle_active": "Verrouillé — désactivation uniquement via la pause de sécurité",
|
||||
"layers_app_lock_subtitle_inactive": "Vous empêche de désactiver ReBreak ou le filtre sous l'impulsion",
|
||||
"layers_app_lock_warning": "Une fois actif, vous ne pouvez désactiver la protection que via une pause de sécurité de 24 heures. C'est voulu.",
|
||||
"layers_app_lock_locked_hint": "Verrouillé par le système. Désactivation uniquement via Réglages iOS → Temps d'écran → Gestion par ReBreak.",
|
||||
"kpi_global_label": "Domaines bloqués dans le monde",
|
||||
"kpi_global_subtitle": "Entrées actives dans la liste de blocage globale",
|
||||
"delta_week": "cette semaine",
|
||||
@ -441,7 +442,9 @@
|
||||
"cta_primary": "Activer la protection",
|
||||
"url_title": "Étape 1 sur 2 — Filtre de contenu",
|
||||
"lock_title": "Étape 2 sur 2 — Verrou d'app",
|
||||
"tap_marker_hint": "Le marqueur rouge indique le bon bouton. Apple place le grand bleu en haut (« Refuser ») — n'y tombe pas.",
|
||||
"tap_marker_hint": "Apple place le grand bouton bleu en haut (« Refuser ») — touche le bouton du BAS, pas celui du haut.",
|
||||
"dialog_button_allow": "Touche « Autoriser »",
|
||||
"dialog_button_continue": "Touche « Continuer »",
|
||||
"applock_failed_title": "Échec du verrou d'app",
|
||||
"applock_failed_msg": "Tu peux réessayer ou ignorer cette étape — le filtre URL est déjà actif.",
|
||||
"applock_skip": "Ignorer",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user