feat(native/blocker): underlined Seiten/Mails tabs + per-type counter
Top-tabs above the custom-domains grid: Seiten (web) and Mails (mail_*).
2px underline highlight in colors.brandOrange for the active tab, the
muted label otherwise — matches the community/feed tab style we already
use. Pill segmented control would have needed extra inset math for two
tabs without adding clarity.
- DomainGrid filters items by the active tab. Tab-specific empty-state
copy and icon (mail-outline for the Mails tab) so the empty Mails tab
doesn't read like a broken Web view.
- mail_display_name tiles hide the submit-to-global button entirely —
matches the v1.0 backend lock; the user can't accidentally tap into a
400 from the API.
- useCustomDomains exposes countsByType + limits. Provisional client-
side estimation until the new API response shape (extended in the
parallel backend commit f2b81ee) is wired through — same TS shape,
so dropping the estimation is a one-line swap when ready.
- AddDomainSheet picks up initialType so tapping "+" while the Mails tab
is active opens the sheet pre-selected to E-Mail. Plan-limit error
handling maps WEB_LIMIT_REACHED / MAIL_LIMIT_REACHED to the right
per-bucket message.
i18n: tabs_web / tabs_mail / count_label / error_web_limit_reached /
error_mail_limit_reached / empty_web / empty_mail across DE/EN/FR with
%{var} placeholders.
This commit is contained in:
parent
f2b81eef54
commit
5c6fa3d45b
@ -1,9 +1,10 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { ScrollView, Text, View, Alert, ActivityIndicator } from 'react-native';
|
import { ScrollView, Text, View, Alert, ActivityIndicator, TouchableOpacity } from 'react-native';
|
||||||
import { useRouter } from 'expo-router';
|
import { useRouter } from 'expo-router';
|
||||||
import { useBottomTabBarHeight } from 'react-native-bottom-tabs';
|
import { useBottomTabBarHeight } from 'react-native-bottom-tabs';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { Ionicons } from '@expo/vector-icons';
|
import { Ionicons } from '@expo/vector-icons';
|
||||||
|
import type { CountsByType, LimitsByType } from '../../hooks/useCustomDomains';
|
||||||
import { AppHeader } from '../../components/AppHeader';
|
import { AppHeader } from '../../components/AppHeader';
|
||||||
import { LayerSwitchCard } from '../../components/blocker/LayerSwitchCard';
|
import { LayerSwitchCard } from '../../components/blocker/LayerSwitchCard';
|
||||||
import { ProtectionLockedCard } from '../../components/blocker/ProtectionLockedCard';
|
import { ProtectionLockedCard } from '../../components/blocker/ProtectionLockedCard';
|
||||||
@ -42,6 +43,8 @@ export default function BlockerScreen() {
|
|||||||
const {
|
const {
|
||||||
domains,
|
domains,
|
||||||
tier,
|
tier,
|
||||||
|
countsByType,
|
||||||
|
limits,
|
||||||
addDomain,
|
addDomain,
|
||||||
submitDomain,
|
submitDomain,
|
||||||
refresh: refreshDomains,
|
refresh: refreshDomains,
|
||||||
@ -62,6 +65,9 @@ export default function BlockerScreen() {
|
|||||||
}, [refreshDomains, syncBlocklist, refresh]);
|
}, [refreshDomains, syncBlocklist, refresh]);
|
||||||
useDomainSubmissionRealtime(onDomainChange, true);
|
useDomainSubmissionRealtime(onDomainChange, true);
|
||||||
|
|
||||||
|
// Tab-State
|
||||||
|
const [activeTab, setActiveTab] = useState<'web' | 'mail'>('web');
|
||||||
|
|
||||||
// Sheet-States
|
// Sheet-States
|
||||||
const [addSheetOpen, setAddSheetOpen] = useState(false);
|
const [addSheetOpen, setAddSheetOpen] = useState(false);
|
||||||
const [detailsOpen, setDetailsOpen] = useState(false);
|
const [detailsOpen, setDetailsOpen] = useState(false);
|
||||||
@ -367,11 +373,20 @@ export default function BlockerScreen() {
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Top-Tabs: Seiten / Mails */}
|
||||||
|
<BlockerTabBar
|
||||||
|
activeTab={activeTab}
|
||||||
|
onTabChange={setActiveTab}
|
||||||
|
countsByType={countsByType}
|
||||||
|
limits={limits}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Domain Grid mit inline + Button neben SlotPill */}
|
{/* Domain Grid mit inline + Button neben SlotPill */}
|
||||||
<View style={{ marginTop: 8 }}>
|
<View style={{ marginTop: 8 }}>
|
||||||
<DomainGrid
|
<DomainGrid
|
||||||
domains={domains}
|
domains={domains}
|
||||||
tier={tier}
|
tier={tier}
|
||||||
|
activeTab={activeTab}
|
||||||
onAdd={() => setAddSheetOpen(true)}
|
onAdd={() => setAddSheetOpen(true)}
|
||||||
onSubmit={submitDomain}
|
onSubmit={submitDomain}
|
||||||
onUpgradePro={() => Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))}
|
onUpgradePro={() => Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))}
|
||||||
@ -383,6 +398,7 @@ export default function BlockerScreen() {
|
|||||||
<AddDomainSheet
|
<AddDomainSheet
|
||||||
visible={addSheetOpen}
|
visible={addSheetOpen}
|
||||||
tier={tier}
|
tier={tier}
|
||||||
|
initialType={activeTab}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setAddSheetOpen(false);
|
setAddSheetOpen(false);
|
||||||
refreshDomains();
|
refreshDomains();
|
||||||
@ -417,3 +433,88 @@ export default function BlockerScreen() {
|
|||||||
</View>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── BlockerTabBar ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function BlockerTabBar({
|
||||||
|
activeTab,
|
||||||
|
onTabChange,
|
||||||
|
countsByType,
|
||||||
|
limits,
|
||||||
|
}: {
|
||||||
|
activeTab: 'web' | 'mail';
|
||||||
|
onTabChange: (tab: 'web' | 'mail') => void;
|
||||||
|
countsByType: CountsByType;
|
||||||
|
limits: LimitsByType;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const colors = useColors();
|
||||||
|
|
||||||
|
const tabs: { key: 'web' | 'mail'; label: string; count: number; max: number }[] = [
|
||||||
|
{ key: 'web', label: t('blocker.tabs_web'), count: countsByType.web, max: limits.web },
|
||||||
|
{ key: 'mail', label: t('blocker.tabs_mail'), count: countsByType.mail, max: limits.mail },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
flexDirection: 'row',
|
||||||
|
borderBottomWidth: 1,
|
||||||
|
borderBottomColor: colors.border,
|
||||||
|
marginBottom: 4,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tabs.map((tab) => {
|
||||||
|
const isActive = activeTab === tab.key;
|
||||||
|
const atMax = tab.count >= tab.max;
|
||||||
|
const badgeColor = atMax ? colors.error : colors.textMuted;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TouchableOpacity
|
||||||
|
key={tab.key}
|
||||||
|
onPress={() => onTabChange(tab.key)}
|
||||||
|
activeOpacity={0.7}
|
||||||
|
style={{
|
||||||
|
flex: 1,
|
||||||
|
alignItems: 'center',
|
||||||
|
paddingBottom: 10,
|
||||||
|
paddingTop: 4,
|
||||||
|
borderBottomWidth: 2,
|
||||||
|
borderBottomColor: isActive ? colors.brandOrange : 'transparent',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontFamily: isActive ? 'Nunito_700Bold' : 'Nunito_400Regular',
|
||||||
|
color: isActive ? colors.text : colors.textMuted,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
</Text>
|
||||||
|
<View
|
||||||
|
style={{
|
||||||
|
paddingHorizontal: 6,
|
||||||
|
paddingVertical: 1,
|
||||||
|
borderRadius: 999,
|
||||||
|
backgroundColor: atMax ? '#fee2e2' : colors.surfaceElevated,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
style={{
|
||||||
|
fontSize: 10,
|
||||||
|
fontFamily: 'Nunito_600SemiBold',
|
||||||
|
color: badgeColor,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t('blocker.count_label', { count: tab.count, max: tab.max })}
|
||||||
|
</Text>
|
||||||
|
</View>
|
||||||
|
</View>
|
||||||
|
</TouchableOpacity>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</View>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { useState, useMemo } from 'react';
|
|||||||
import {
|
import {
|
||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
Pressable,
|
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
Image,
|
Image,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
@ -50,6 +49,7 @@ function timeSinceSubmit(input?: string | Date): string {
|
|||||||
type Props = {
|
type Props = {
|
||||||
domains: CustomDomain[];
|
domains: CustomDomain[];
|
||||||
tier: Tier;
|
tier: Tier;
|
||||||
|
activeTab: 'web' | 'mail';
|
||||||
onAdd?: () => void;
|
onAdd?: () => void;
|
||||||
onSubmit?: (id: string) => Promise<{ ok: boolean }>;
|
onSubmit?: (id: string) => Promise<{ ok: boolean }>;
|
||||||
onRemove?: (id: string) => Promise<{ ok: boolean }>;
|
onRemove?: (id: string) => Promise<{ ok: boolean }>;
|
||||||
@ -65,14 +65,19 @@ const STATUS_PRIORITY: Record<string, number> = {
|
|||||||
approved: 99,
|
approved: 99,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) {
|
export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgradePro }: Props) {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const colors = useColors();
|
const colors = useColors();
|
||||||
// Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority,
|
// Filter by tab, then by status (exclude approved). Sort by status-priority, then newest-first.
|
||||||
// innerhalb gleicher Priority dann newest-first by addedAt.
|
|
||||||
const visible = useMemo(() => {
|
const visible = useMemo(() => {
|
||||||
return domains
|
return domains
|
||||||
.filter((d) => d.status !== 'approved')
|
.filter((d) => {
|
||||||
|
if (d.status === 'approved') return false;
|
||||||
|
if (activeTab === 'mail') {
|
||||||
|
return d.type === 'mail_domain' || d.type === 'mail_display_name';
|
||||||
|
}
|
||||||
|
return d.type === 'web' || !d.type;
|
||||||
|
})
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => {
|
.sort((a, b) => {
|
||||||
const pa = STATUS_PRIORITY[a.status] ?? 99;
|
const pa = STATUS_PRIORITY[a.status] ?? 99;
|
||||||
@ -82,7 +87,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
const tb = b.addedAt ? new Date(b.addedAt).getTime() : 0;
|
const tb = b.addedAt ? new Date(b.addedAt).getTime() : 0;
|
||||||
return tb - ta;
|
return tb - ta;
|
||||||
});
|
});
|
||||||
}, [domains]);
|
}, [domains, activeTab]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ gap: 12 }}>
|
<View style={{ gap: 12 }}>
|
||||||
@ -94,11 +99,11 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
|
||||||
<SlotPill tier={tier} />
|
<SlotPill tier={tier} />
|
||||||
{onAdd && (
|
{onAdd && (
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={tier.atLimit ? undefined : onAdd}
|
onPress={tier.atLimit ? undefined : onAdd}
|
||||||
accessibilityLabel={t('blocker.domain_add_a11y')}
|
accessibilityLabel={t('blocker.domain_add_a11y')}
|
||||||
accessibilityState={{ disabled: tier.atLimit }}
|
|
||||||
disabled={tier.atLimit}
|
disabled={tier.atLimit}
|
||||||
|
activeOpacity={0.75}
|
||||||
hitSlop={8}
|
hitSlop={8}
|
||||||
>
|
>
|
||||||
<View
|
<View
|
||||||
@ -106,7 +111,6 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
borderRadius: 16,
|
borderRadius: 16,
|
||||||
// atLimit → grau + 50% opacity (deutlich visuell disabled)
|
|
||||||
backgroundColor: tier.atLimit ? '#a3a3a3' : '#007AFF',
|
backgroundColor: tier.atLimit ? '#a3a3a3' : '#007AFF',
|
||||||
opacity: tier.atLimit ? 0.5 : 1,
|
opacity: tier.atLimit ? 0.5 : 1,
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
@ -115,7 +119,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
>
|
>
|
||||||
<Ionicons name="add" size={22} color="#fff" />
|
<Ionicons name="add" size={22} color="#fff" />
|
||||||
</View>
|
</View>
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
@ -179,7 +183,11 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Ionicons name="globe-outline" size={28} color={colors.textMuted} />
|
<Ionicons
|
||||||
|
name={activeTab === 'mail' ? 'mail-outline' : 'globe-outline'}
|
||||||
|
size={28}
|
||||||
|
color={colors.textMuted}
|
||||||
|
/>
|
||||||
<Text
|
<Text
|
||||||
style={{
|
style={{
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@ -189,7 +197,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro
|
|||||||
textAlign: 'center',
|
textAlign: 'center',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('blocker.domain_empty')}
|
{activeTab === 'mail' ? t('blocker.empty_mail') : t('blocker.empty_web')}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
@ -341,9 +349,10 @@ function DomainTile({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMailDisplayName = domain.type === 'mail_display_name';
|
||||||
const isFreeAndUsed = tier.plan === 'free' && domain.status !== 'active';
|
const isFreeAndUsed = tier.plan === 'free' && domain.status !== 'active';
|
||||||
const showSubmit = tier.canSubmit && domain.status === 'active';
|
const showSubmit = tier.canSubmit && domain.status === 'active' && !isMailDisplayName;
|
||||||
const showResubmit = tier.canSubmit && domain.status === 'rejected';
|
const showResubmit = tier.canSubmit && domain.status === 'rejected' && !isMailDisplayName;
|
||||||
const showInPruefungBtn = domain.status === 'submitted';
|
const showInPruefungBtn = domain.status === 'submitted';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -461,9 +470,10 @@ function DomainTile({
|
|||||||
</View>
|
</View>
|
||||||
)}
|
)}
|
||||||
{showSubmit && (
|
{showSubmit && (
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={openConfirm}
|
onPress={openConfirm}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
|
activeOpacity={0.75}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
@ -482,12 +492,13 @@ function DomainTile({
|
|||||||
{t('blocker.domain_btn_freigeben')}
|
{t('blocker.domain_btn_freigeben')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
{showResubmit && (
|
{showResubmit && (
|
||||||
<Pressable
|
<TouchableOpacity
|
||||||
onPress={openConfirm}
|
onPress={openConfirm}
|
||||||
disabled={submitting}
|
disabled={submitting}
|
||||||
|
activeOpacity={0.75}
|
||||||
style={{
|
style={{
|
||||||
flex: 1,
|
flex: 1,
|
||||||
borderRadius: 6,
|
borderRadius: 6,
|
||||||
@ -506,7 +517,7 @@ function DomainTile({
|
|||||||
{t('blocker.domain_btn_erneut')}
|
{t('blocker.domain_btn_erneut')}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
</Pressable>
|
</TouchableOpacity>
|
||||||
)}
|
)}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
|
|||||||
@ -42,9 +42,21 @@ function deriveTier(plan: Plan, domains: CustomDomain[]): Tier {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CountsByType = {
|
||||||
|
web: number;
|
||||||
|
mail: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LimitsByType = {
|
||||||
|
web: number;
|
||||||
|
mail: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type UseCustomDomainsReturn = {
|
export type UseCustomDomainsReturn = {
|
||||||
domains: CustomDomain[];
|
domains: CustomDomain[];
|
||||||
tier: Tier;
|
tier: Tier;
|
||||||
|
countsByType: CountsByType;
|
||||||
|
limits: LimitsByType;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
refresh: () => Promise<void>;
|
refresh: () => Promise<void>;
|
||||||
@ -165,9 +177,24 @@ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn {
|
|||||||
|
|
||||||
const tier = deriveTier(plan, domains);
|
const tier = deriveTier(plan, domains);
|
||||||
|
|
||||||
|
const countsByType: CountsByType = {
|
||||||
|
web: domains.filter(
|
||||||
|
(d) => d.status !== 'approved' && (d.type === 'web' || !d.type),
|
||||||
|
).length,
|
||||||
|
mail: domains.filter(
|
||||||
|
(d) => d.status !== 'approved' && (d.type === 'mail_domain' || d.type === 'mail_display_name'),
|
||||||
|
).length,
|
||||||
|
};
|
||||||
|
|
||||||
|
const webLimit = plan === 'legend' ? 8 : 4;
|
||||||
|
const mailLimit = plan === 'legend' ? 2 : 1;
|
||||||
|
const limits: LimitsByType = { web: webLimit, mail: mailLimit };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
domains,
|
domains,
|
||||||
tier,
|
tier,
|
||||||
|
countsByType,
|
||||||
|
limits,
|
||||||
loading,
|
loading,
|
||||||
error,
|
error,
|
||||||
refresh: fetchDomains,
|
refresh: fetchDomains,
|
||||||
|
|||||||
@ -325,7 +325,14 @@
|
|||||||
"add_mail_label": "E-Mail-Absender oder Display-Name",
|
"add_mail_label": "E-Mail-Absender oder Display-Name",
|
||||||
"add_mail_placeholder": "z.B. only4-subscribers.com oder EXTRASPIN",
|
"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_help": "Mail-Adresse, Domain oder Display-Name. Wir blockieren alle Mails die diesem Muster entsprechen.",
|
||||||
"add_mail_invalid": "Bitte ein Muster eingeben."
|
"add_mail_invalid": "Bitte ein Muster eingeben.",
|
||||||
|
"tabs_web": "Seiten",
|
||||||
|
"tabs_mail": "Mails",
|
||||||
|
"count_label": "%{count}/%{max}",
|
||||||
|
"error_web_limit_reached": "Du hast alle Domain-Slots aufgebraucht. Entferne eine Domain oder upgrade auf Pro/Legend.",
|
||||||
|
"error_mail_limit_reached": "Du hast alle Mail-Slots aufgebraucht. Entferne ein Mail-Pattern oder upgrade auf Pro/Legend.",
|
||||||
|
"empty_web": "Noch keine eigenen Domains.\nTippe + um eine hinzuzufügen.",
|
||||||
|
"empty_mail": "Noch keine E-Mail-Patterns. Tippe + um eine Adresse oder einen Display-Namen (z.B. EXTRASPIN) zu blockieren."
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"title": "Mail-Schutz",
|
"title": "Mail-Schutz",
|
||||||
|
|||||||
@ -325,7 +325,14 @@
|
|||||||
"add_mail_label": "Email sender or display name",
|
"add_mail_label": "Email sender or display name",
|
||||||
"add_mail_placeholder": "e.g. only4-subscribers.com or EXTRASPIN",
|
"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_help": "Email address, domain or display name. We block all emails matching this pattern.",
|
||||||
"add_mail_invalid": "Please enter a pattern."
|
"add_mail_invalid": "Please enter a pattern.",
|
||||||
|
"tabs_web": "Websites",
|
||||||
|
"tabs_mail": "Emails",
|
||||||
|
"count_label": "%{count}/%{max}",
|
||||||
|
"error_web_limit_reached": "You've used all your domain slots. Remove a domain or upgrade to Pro/Legend.",
|
||||||
|
"error_mail_limit_reached": "You've used all your email slots. Remove an email pattern or upgrade to Pro/Legend.",
|
||||||
|
"empty_web": "No custom domains yet.\nTap + to add one.",
|
||||||
|
"empty_mail": "No email patterns yet. Tap + to block an address or display name (e.g. EXTRASPIN)."
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"title": "Mail Shield",
|
"title": "Mail Shield",
|
||||||
|
|||||||
@ -325,7 +325,14 @@
|
|||||||
"add_mail_label": "Expéditeur ou nom affiché",
|
"add_mail_label": "Expéditeur ou nom affiché",
|
||||||
"add_mail_placeholder": "ex. only4-subscribers.com ou EXTRASPIN",
|
"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_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."
|
"add_mail_invalid": "Veuillez saisir un modèle.",
|
||||||
|
"tabs_web": "Sites",
|
||||||
|
"tabs_mail": "E-mails",
|
||||||
|
"count_label": "%{count}/%{max}",
|
||||||
|
"error_web_limit_reached": "Vous avez utilisé tous vos emplacements de domaines. Supprimez un domaine ou passez à Pro/Legend.",
|
||||||
|
"error_mail_limit_reached": "Vous avez utilisé tous vos emplacements e-mail. Supprimez un modèle ou passez à Pro/Legend.",
|
||||||
|
"empty_web": "Aucun domaine personnalisé.\nAppuyez sur + pour en ajouter un.",
|
||||||
|
"empty_mail": "Aucun filtre e-mail. Appuyez sur + pour bloquer une adresse ou un nom affiché (ex. EXTRASPIN)."
|
||||||
},
|
},
|
||||||
"mail": {
|
"mail": {
|
||||||
"title": "Protection Mail",
|
"title": "Protection Mail",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user