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:
chahinebrini 2026-05-17 19:58:56 +02:00
parent 1596a4ea7a
commit 33aa3464b8
16 changed files with 183 additions and 110 deletions

View File

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

View File

@ -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>
);
}

View File

@ -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"

View File

@ -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>
);
}

View File

@ -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,

View File

@ -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.

View File

@ -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": "تخطّي",

View File

@ -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",

View File

@ -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",

View File

@ -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",