feat(native/blocker): type picker + mail patterns in AddDomainSheet
AddDomainSheet now opens with a Seite / E-Mail segmented control.
Web keeps the existing flow (label, placeholder, favicon preview,
domain normalization). Mail switches to a free-form pattern input
(address / domain / display-name — user types what they see in
their inbox) with a mail-icon preview after the field is filled.
addDomain(pattern, kind) now sends { pattern, kind: 'web' | 'mail' }
and the server decides the concrete type. Type field flows through
the CustomDomain type so DomainGrid tiles render the mail-outline
icon for mail entries instead of the favicon fallback.
i18n: blocker.type_web / type_mail / add_web_* / add_mail_* across
de/en/fr with %{var} placeholders per repo convention.
This commit is contained in:
parent
7dbcac6700
commit
4eab5df7e2
@ -387,8 +387,8 @@ export default function BlockerScreen() {
|
|||||||
setAddSheetOpen(false);
|
setAddSheetOpen(false);
|
||||||
refreshDomains();
|
refreshDomains();
|
||||||
}}
|
}}
|
||||||
onAdd={async (d) => {
|
onAdd={async (pattern, kind) => {
|
||||||
const result = await addDomain(d);
|
const result = await addDomain(pattern, kind);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
// Neue Custom-Domain → Filter muss aktualisierten Hash-Set kriegen
|
// Neue Custom-Domain → Filter muss aktualisierten Hash-Set kriegen
|
||||||
const sync = await syncBlocklist();
|
const sync = await syncBlocklist();
|
||||||
|
|||||||
@ -13,27 +13,30 @@ import {
|
|||||||
normalizeDomain,
|
normalizeDomain,
|
||||||
type Tier,
|
type Tier,
|
||||||
} from '../../hooks/useCustomDomains';
|
} from '../../hooks/useCustomDomains';
|
||||||
import { useColors } from '../../lib/theme';
|
import { useColors, type ColorScheme } from '../../lib/theme';
|
||||||
import { FormSheet } from '../FormSheet';
|
import { FormSheet } from '../FormSheet';
|
||||||
import { SheetFieldStack } from '../SheetFieldStack';
|
import { SheetFieldStack } from '../SheetFieldStack';
|
||||||
|
|
||||||
|
type InputKind = 'web' | 'mail';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
visible: boolean;
|
visible: boolean;
|
||||||
tier: Tier;
|
tier: Tier;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onAdd: (domain: string) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>;
|
onAdd: (pattern: string, kind: InputKind) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
|
const [kind, setKind] = useState<InputKind>('web');
|
||||||
const [input, setInput] = useState('');
|
const [input, setInput] = useState('');
|
||||||
const [confirmPermanent, setConfirmPermanent] = useState(false);
|
const [confirmPermanent, setConfirmPermanent] = useState(false);
|
||||||
const [adding, setAdding] = useState(false);
|
const [adding, setAdding] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [fieldsDone, setFieldsDone] = useState(false);
|
const [fieldsDone, setFieldsDone] = useState(false);
|
||||||
|
|
||||||
const normalized = normalizeDomain(input);
|
const normalizedWeb = kind === 'web' ? normalizeDomain(input) : '';
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
setInput('');
|
setInput('');
|
||||||
@ -43,18 +46,32 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
onClose();
|
onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleKindChange(next: InputKind) {
|
||||||
|
if (next === kind) return;
|
||||||
|
setKind(next);
|
||||||
|
setInput('');
|
||||||
|
setError(null);
|
||||||
|
setFieldsDone(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isInputValid(): boolean {
|
||||||
|
if (kind === 'web') return isValidDomain(input);
|
||||||
|
return input.trim().length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
async function handleAdd() {
|
async function handleAdd() {
|
||||||
if (!isValidDomain(input) || !confirmPermanent || adding) return;
|
if (!isInputValid() || !confirmPermanent || adding) return;
|
||||||
setAdding(true);
|
setAdding(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
const result = await onAdd(input);
|
const pattern = kind === 'web' ? input : input.trim();
|
||||||
|
const result = await onAdd(pattern, kind);
|
||||||
setAdding(false);
|
setAdding(false);
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
close();
|
close();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (result.alreadyGlobal) {
|
if (result.alreadyGlobal) {
|
||||||
setError(t('blocker.add_sheet_already_global', { domain: normalized }));
|
setError(t('blocker.add_sheet_already_global', { domain: normalizedWeb || input.trim() }));
|
||||||
} else {
|
} else {
|
||||||
setError(result.error ?? t('blocker.add_sheet_add_failed'));
|
setError(result.error ?? t('blocker.add_sheet_add_failed'));
|
||||||
}
|
}
|
||||||
@ -65,6 +82,22 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
? t('blocker.add_sheet_warning_free')
|
? t('blocker.add_sheet_warning_free')
|
||||||
: t('blocker.add_sheet_warning_pro');
|
: t('blocker.add_sheet_warning_pro');
|
||||||
|
|
||||||
|
const inputLabel = kind === 'web'
|
||||||
|
? t('blocker.add_web_label')
|
||||||
|
: t('blocker.add_mail_label');
|
||||||
|
|
||||||
|
const inputPlaceholder = kind === 'web'
|
||||||
|
? t('blocker.add_web_placeholder')
|
||||||
|
: t('blocker.add_mail_placeholder');
|
||||||
|
|
||||||
|
const helpText = kind === 'web'
|
||||||
|
? t('blocker.add_web_help')
|
||||||
|
: t('blocker.add_mail_help');
|
||||||
|
|
||||||
|
const validateField = kind === 'web'
|
||||||
|
? (v: string) => isValidDomain(v) ? undefined : t('blocker.add_sheet_invalid')
|
||||||
|
: (v: string) => v.trim().length > 0 ? undefined : t('blocker.add_mail_invalid');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormSheet
|
<FormSheet
|
||||||
visible={visible}
|
visible={visible}
|
||||||
@ -74,54 +107,127 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
growWithKeyboard
|
growWithKeyboard
|
||||||
>
|
>
|
||||||
<SheetFieldStack
|
<SheetFieldStack
|
||||||
|
intro={
|
||||||
|
<TypePicker kind={kind} onChange={handleKindChange} />
|
||||||
|
}
|
||||||
fields={[
|
fields={[
|
||||||
{
|
{
|
||||||
key: 'domain',
|
key: 'pattern',
|
||||||
label: t('blocker.add_sheet_label'),
|
label: inputLabel,
|
||||||
placeholder: t('blocker.add_sheet_placeholder'),
|
placeholder: inputPlaceholder,
|
||||||
value: input,
|
value: input,
|
||||||
onChangeText: (v) => { setInput(v); setError(null); },
|
onChangeText: (v) => { setInput(v); setError(null); },
|
||||||
normalize: normalizeDomain,
|
normalize: kind === 'web' ? normalizeDomain : undefined,
|
||||||
keyboardType: 'url',
|
keyboardType: kind === 'web' ? 'url' : 'default',
|
||||||
autoCapitalize: 'none',
|
autoCapitalize: 'none',
|
||||||
autoCorrect: false,
|
autoCorrect: false,
|
||||||
validate: (v) =>
|
validate: validateField,
|
||||||
isValidDomain(v) ? undefined : t('blocker.add_sheet_invalid'),
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onComplete={() => setFieldsDone(true)}
|
onComplete={() => setFieldsDone(true)}
|
||||||
>
|
>
|
||||||
{/* Favicon-Preview */}
|
{/* Help-Text */}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
alignItems: 'center',
|
gap: 8,
|
||||||
gap: 10,
|
|
||||||
padding: 12,
|
padding: 12,
|
||||||
backgroundColor: colors.surfaceElevated,
|
backgroundColor: colors.surfaceElevated,
|
||||||
borderRadius: 12,
|
borderRadius: 12,
|
||||||
marginBottom: 12,
|
marginBottom: 8,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Image
|
<Ionicons
|
||||||
source={{
|
name={kind === 'web' ? 'globe-outline' : 'mail-outline'}
|
||||||
uri: `https://www.google.com/s2/favicons?domain=${normalized}&sz=64`,
|
size={16}
|
||||||
}}
|
color={colors.textMuted}
|
||||||
style={{ width: 24, height: 24, borderRadius: 4 }}
|
style={{ marginTop: 1 }}
|
||||||
/>
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 14,
|
fontSize: 12,
|
||||||
fontFamily: 'Nunito_600SemiBold',
|
fontFamily: 'Nunito_400Regular',
|
||||||
color: colors.text,
|
color: colors.textMuted,
|
||||||
|
lineHeight: 17,
|
||||||
}}
|
}}
|
||||||
numberOfLines={1}
|
|
||||||
>
|
>
|
||||||
{normalized}
|
{helpText}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
{/* Favicon-Preview (nur Web) */}
|
||||||
|
{kind === 'web' && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: colors.surfaceElevated,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
source={{
|
||||||
|
uri: `https://www.google.com/s2/favicons?domain=${normalizedWeb}&sz=64`,
|
||||||
|
}}
|
||||||
|
style={{ width: 24, height: 24, borderRadius: 4 }}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
|
color: colors.text,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{normalizedWeb}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mail-Typ Icon-Preview */}
|
||||||
|
{kind === 'mail' && (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: 10,
|
||||||
|
padding: 12,
|
||||||
|
backgroundColor: colors.surfaceElevated,
|
||||||
|
borderRadius: 12,
|
||||||
|
marginBottom: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
borderRadius: 4,
|
||||||
|
backgroundColor: '#dbeafe',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="mail-outline" size={14} color="#2563eb" />
|
||||||
|
</View>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
|
color: colors.text,
|
||||||
|
}}
|
||||||
|
numberOfLines={1}
|
||||||
|
>
|
||||||
|
{input.trim() || inputPlaceholder}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Warnung */}
|
{/* Warnung */}
|
||||||
<View
|
<View
|
||||||
style={{
|
style={{
|
||||||
@ -230,3 +336,88 @@ export function AddDomainSheet({ visible, tier, onClose, onAdd }: Props) {
|
|||||||
</FormSheet>
|
</FormSheet>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── TypePicker ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function TypePicker({ kind, onChange }: { kind: InputKind; onChange: (k: InputKind) => void }) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
backgroundColor: colors.surfaceElevated,
|
||||||
|
borderRadius: 12,
|
||||||
|
padding: 3,
|
||||||
|
gap: 3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TypePill
|
||||||
|
icon="globe-outline"
|
||||||
|
label={t('blocker.type_web')}
|
||||||
|
active={kind === 'web'}
|
||||||
|
onPress={() => onChange('web')}
|
||||||
|
colors={colors}
|
||||||
|
/>
|
||||||
|
<TypePill
|
||||||
|
icon="mail-outline"
|
||||||
|
label={t('blocker.type_mail')}
|
||||||
|
active={kind === 'mail'}
|
||||||
|
onPress={() => onChange('mail')}
|
||||||
|
colors={colors}
|
||||||
|
/>
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function TypePill({
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
active,
|
||||||
|
onPress,
|
||||||
|
colors,
|
||||||
|
}: {
|
||||||
|
icon: 'globe-outline' | 'mail-outline';
|
||||||
|
label: string;
|
||||||
|
active: boolean;
|
||||||
|
onPress: () => void;
|
||||||
|
colors: ColorScheme;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
onPress={onPress}
|
||||||
|
activeOpacity={0.8}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
flexDirection: 'row',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
gap: 6,
|
||||||
|
paddingVertical: 9,
|
||||||
|
borderRadius: 10,
|
||||||
|
backgroundColor: active ? colors.bg : 'transparent',
|
||||||
|
shadowColor: active ? '#000' : 'transparent',
|
||||||
|
shadowOffset: { width: 0, height: 1 },
|
||||||
|
shadowOpacity: active ? 0.08 : 0,
|
||||||
|
shadowRadius: 2,
|
||||||
|
elevation: active ? 1 : 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons
|
||||||
|
name={icon}
|
||||||
|
size={15}
|
||||||
|
color={active ? colors.text : colors.textMuted}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 13,
|
||||||
|
fontFamily: active ? 'Nunito_700Bold' : 'Nunito_400Regular',
|
||||||
|
color: active ? colors.text : colors.textMuted,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -391,9 +391,22 @@ function DomainTile({
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
{/* Mitte: Favicon + Domain-Name (zentriert, flex-1) */}
|
{/* Mitte: Icon + Domain-Name (zentriert, flex-1) */}
|
||||||
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 4, paddingVertical: 8 }}>
|
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 4, paddingVertical: 8 }}>
|
||||||
{!imgError ? (
|
{domain.type === 'mail_domain' || domain.type === 'mail_display_name' ? (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
width: 26,
|
||||||
|
height: 26,
|
||||||
|
borderRadius: 5,
|
||||||
|
backgroundColor: '#dbeafe',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Ionicons name="mail-outline" size={14} color="#2563eb" />
|
||||||
|
</View>
|
||||||
|
) : !imgError ? (
|
||||||
<Image
|
<Image
|
||||||
source={{ uri: `https://www.google.com/s2/favicons?domain=${stripped}&sz=128` }}
|
source={{ uri: `https://www.google.com/s2/favicons?domain=${stripped}&sz=128` }}
|
||||||
style={{ width: 26, height: 26, borderRadius: 5 }}
|
style={{ width: 26, height: 26, borderRadius: 5 }}
|
||||||
|
|||||||
@ -3,9 +3,12 @@ import { apiFetch } from '../lib/api';
|
|||||||
|
|
||||||
export type DomainStatus = 'active' | 'submitted' | 'approved' | 'rejected';
|
export type DomainStatus = 'active' | 'submitted' | 'approved' | 'rejected';
|
||||||
|
|
||||||
|
export type EntryKind = 'web' | 'mail_domain' | 'mail_display_name';
|
||||||
|
|
||||||
export type CustomDomain = {
|
export type CustomDomain = {
|
||||||
id: string;
|
id: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
type?: EntryKind;
|
||||||
status: DomainStatus;
|
status: DomainStatus;
|
||||||
addedAt?: string;
|
addedAt?: string;
|
||||||
postId?: string | null;
|
postId?: string | null;
|
||||||
@ -45,7 +48,7 @@ export type UseCustomDomainsReturn = {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
addDomain: (domain: string) => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>;
|
addDomain: (domain: string, kind?: 'web' | 'mail') => Promise<{ ok: boolean; error?: string; alreadyGlobal?: boolean }>;
|
||||||
submitDomain: (id: string) => Promise<{ ok: boolean; error?: string }>;
|
submitDomain: (id: string) => Promise<{ ok: boolean; error?: string }>;
|
||||||
removeDomain: (id: string) => Promise<{ ok: boolean; error?: string }>;
|
removeDomain: (id: string) => Promise<{ ok: boolean; error?: string }>;
|
||||||
/** Live-Validate (regex) ob string gültiger Domain-Name ist. */
|
/** Live-Validate (regex) ob string gültiger Domain-Name ist. */
|
||||||
@ -109,17 +112,16 @@ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn {
|
|||||||
}, [fetchDomains]);
|
}, [fetchDomains]);
|
||||||
|
|
||||||
const addDomain = useCallback(
|
const addDomain = useCallback(
|
||||||
async (input: string) => {
|
async (input: string, kind: 'web' | 'mail' = 'web') => {
|
||||||
if (!isValidDomain(input)) return { ok: false, error: 'invalid_domain' };
|
if (kind === 'web' && !isValidDomain(input)) return { ok: false, error: 'invalid_domain' };
|
||||||
|
if (kind === 'mail' && !input.trim()) return { ok: false, error: 'invalid_pattern' };
|
||||||
const tier = deriveTier(plan, domains);
|
const tier = deriveTier(plan, domains);
|
||||||
if (tier.atLimit) return { ok: false, error: 'limit_reached' };
|
if (tier.atLimit) return { ok: false, error: 'limit_reached' };
|
||||||
const normalized = normalizeDomain(input);
|
const pattern = kind === 'web' ? normalizeDomain(input) : input.trim();
|
||||||
try {
|
try {
|
||||||
// Backend könnte einen `alreadyGlobal`-Flag setzen wenn die Domain
|
|
||||||
// bereits in der globalen Blocklist ist (Slot wird nicht verbraucht).
|
|
||||||
const res = await apiFetch<any>('/api/custom-domains', {
|
const res = await apiFetch<any>('/api/custom-domains', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: { domain: normalized },
|
body: { pattern, kind },
|
||||||
});
|
});
|
||||||
if (res?.alreadyGlobal) {
|
if (res?.alreadyGlobal) {
|
||||||
return { ok: false, alreadyGlobal: true };
|
return { ok: false, alreadyGlobal: true };
|
||||||
|
|||||||
@ -316,7 +316,16 @@
|
|||||||
"cooldown_elapsed_message": "Der Cooldown ist abgelaufen — der Schutz wurde deaktiviert. Du kannst den ReBreak-Bedienungshilfe-Dienst jetzt in den Einstellungen ausschalten.",
|
"cooldown_elapsed_message": "Der Cooldown ist abgelaufen — der Schutz wurde deaktiviert. Du kannst den ReBreak-Bedienungshilfe-Dienst jetzt in den Einstellungen ausschalten.",
|
||||||
"cooldown_elapsed_open_settings": "Einstellungen öffnen",
|
"cooldown_elapsed_open_settings": "Einstellungen öffnen",
|
||||||
"app_lock_coming_soon_badge": "Bald",
|
"app_lock_coming_soon_badge": "Bald",
|
||||||
"app_lock_coming_soon_desc": "App-Sperre wird bald verfügbar — Schutz ist bereits aktiv."
|
"app_lock_coming_soon_desc": "App-Sperre wird bald verfügbar — Schutz ist bereits aktiv.",
|
||||||
|
"type_web": "Seite",
|
||||||
|
"type_mail": "E-Mail",
|
||||||
|
"add_web_label": "Domain",
|
||||||
|
"add_web_placeholder": "z.B. casino.com",
|
||||||
|
"add_web_help": "Diese Webseite wird auf allen geschützten Geräten blockiert.",
|
||||||
|
"add_mail_label": "E-Mail-Absender oder Display-Name",
|
||||||
|
"add_mail_placeholder": "z.B. only4-subscribers.com oder EXTRASPIN",
|
||||||
|
"add_mail_help": "Mail-Adresse, Domain oder Display-Name. Wir blockieren alle Mails die diesem Muster entsprechen.",
|
||||||
|
"add_mail_invalid": "Bitte ein Muster eingeben."
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"title": "Mail-Schutz",
|
"title": "Mail-Schutz",
|
||||||
|
|||||||
@ -316,7 +316,16 @@
|
|||||||
"cooldown_elapsed_message": "The cooldown has elapsed — protection was disabled. You can now turn off the ReBreak accessibility service in Settings.",
|
"cooldown_elapsed_message": "The cooldown has elapsed — protection was disabled. You can now turn off the ReBreak accessibility service in Settings.",
|
||||||
"cooldown_elapsed_open_settings": "Open Settings",
|
"cooldown_elapsed_open_settings": "Open Settings",
|
||||||
"app_lock_coming_soon_badge": "Soon",
|
"app_lock_coming_soon_badge": "Soon",
|
||||||
"app_lock_coming_soon_desc": "App lock coming soon — filter protection is already active."
|
"app_lock_coming_soon_desc": "App lock coming soon — filter protection is already active.",
|
||||||
|
"type_web": "Website",
|
||||||
|
"type_mail": "Email",
|
||||||
|
"add_web_label": "Domain",
|
||||||
|
"add_web_placeholder": "e.g. casino.com",
|
||||||
|
"add_web_help": "This website will be blocked on all your protected devices.",
|
||||||
|
"add_mail_label": "Email sender or display name",
|
||||||
|
"add_mail_placeholder": "e.g. only4-subscribers.com or EXTRASPIN",
|
||||||
|
"add_mail_help": "Email address, domain or display name. We block all emails matching this pattern.",
|
||||||
|
"add_mail_invalid": "Please enter a pattern."
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"title": "Mail Shield",
|
"title": "Mail Shield",
|
||||||
|
|||||||
@ -316,7 +316,16 @@
|
|||||||
"cooldown_elapsed_message": "La pause de sécurité est terminée — la protection a été désactivée. Vous pouvez maintenant désactiver le service d'accessibilité ReBreak dans les Réglages.",
|
"cooldown_elapsed_message": "La pause de sécurité est terminée — la protection a été désactivée. Vous pouvez maintenant désactiver le service d'accessibilité ReBreak dans les Réglages.",
|
||||||
"cooldown_elapsed_open_settings": "Ouvrir les Réglages",
|
"cooldown_elapsed_open_settings": "Ouvrir les Réglages",
|
||||||
"app_lock_coming_soon_badge": "Bientôt",
|
"app_lock_coming_soon_badge": "Bientôt",
|
||||||
"app_lock_coming_soon_desc": "Verrouillage de l'app bientôt disponible — la protection par filtre est déjà active."
|
"app_lock_coming_soon_desc": "Verrouillage de l'app bientôt disponible — la protection par filtre est déjà active.",
|
||||||
|
"type_web": "Site web",
|
||||||
|
"type_mail": "E-mail",
|
||||||
|
"add_web_label": "Domaine",
|
||||||
|
"add_web_placeholder": "ex. casino.com",
|
||||||
|
"add_web_help": "Ce site sera bloqué sur tous vos appareils protégés.",
|
||||||
|
"add_mail_label": "Expéditeur ou nom affiché",
|
||||||
|
"add_mail_placeholder": "ex. only4-subscribers.com ou EXTRASPIN",
|
||||||
|
"add_mail_help": "Adresse e-mail, domaine ou nom affiché. Nous bloquons tous les mails correspondant à ce modèle.",
|
||||||
|
"add_mail_invalid": "Veuillez saisir un modèle."
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"title": "Protection Mail",
|
"title": "Protection Mail",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user