mirror of
https://github.com/AdguardTeam/AdGuardHome.git
synced 2026-08-04 15:28:58 +00:00
Pull request 2721: AGDNS-4260 fix dashboard issues
Squashed commit of the following:
commit ac3ca0023f1f70dc921987220514e49596d55017
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Jul 20 16:07:18 2026 +0300
fix dropdown height
commit 76ef09fe3903f1bbaf9ecf9754c66dd6cdf24606
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Jul 20 15:18:27 2026 +0300
remove unused translations
commit 2c5d31cbab579cdd3a3f59b5c6b7bd397a85917c
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Jul 20 15:00:37 2026 +0300
fix bootstrap desc
commit 2939ccbe37ab88f8e1f3769c1c7167b513ed2293
Merge: d9195156d 5a3f84a2f
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Jul 20 14:58:26 2026 +0300
Merge branch 'master' into AGDNS-4260
commit d9195156dcfae15b2ca3b93e0c5a93a376011c97
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Jul 20 14:53:31 2026 +0300
allow comments for bootstrap upstreams
commit 33e74cf18367caf6a866d1b7a57ce1ef2cb67083
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Jul 20 14:38:26 2026 +0300
fix mobile tooltip
commit d8a0dc60b2d38bd27a5eb7f940c0055acb16e738
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Jul 20 12:04:44 2026 +0300
highlight disallowed domain comments
commit b4a1bf7c91
Author: Ildar Kamalov <ik@adguard.com>
Date: Mon Jul 20 09:46:44 2026 +0300
translations
commit a25b2fd3c9
Author: Ildar Kamalov <ik@adguard.com>
Date: Sun Jul 19 19:54:33 2026 +0300
AGDNS-4260 fix translation placeholders
commit 149e67210a
Author: Ildar Kamalov <ik@adguard.com>
Date: Fri Jul 17 10:28:33 2026 +0300
fix table height
commit 17d1e22fa0
Author: Ildar Kamalov <ik@adguard.com>
Date: Fri Jul 17 10:09:33 2026 +0300
AGDNS-4250 fix port validation during setup
commit 0082164623
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Jul 16 19:03:51 2026 +0300
fix
commit 720d3fa238
Merge: 3b01d9bf0 ef585d45c
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Jul 16 18:58:18 2026 +0300
Merge branch 'master' into AGDNS-4260
commit 3b01d9bf02
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Jul 16 18:56:34 2026 +0300
fix
commit 68989c918b
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Jul 16 18:56:14 2026 +0300
AGDNS-4260 fix dashboard issues
commit b5bfdc81c4
Author: Ildar Kamalov <ik@adguard.com>
Date: Thu Jul 16 11:33:09 2026 +0300
support all comments for ignored domains
commit decb5b6587
Author: Ildar Kamalov <ik@adguard.com>
Date: Wed Jul 15 17:58:39 2026 +0300
fix bootstrap and ignored domains
commit bd7d342b2f
Author: Ildar Kamalov <ik@adguard.com>
Date: Wed Jul 15 14:40:53 2026 +0300
remove output from test frontend
commit 5c9de23626
Author: Ildar Kamalov <ik@adguard.com>
Date: Wed Jul 15 14:18:50 2026 +0300
fix upstream comments
commit e103540657
Author: Ildar Kamalov <ik@adguard.com>
Date: Wed Jul 15 13:32:34 2026 +0300
add comments for clients upstreams
commit a65ee2cf2a
Author: Ildar Kamalov <ik@adguard.com>
Date: Wed Jul 15 12:30:04 2026 +0300
config
commit 67207b0464
Author: Ildar Kamalov <ik@adguard.com>
Date: Wed Jul 15 12:06:04 2026 +0300
fix protection menu
commit 380ac0797e
Merge: 10945e24d c1f208ef6
Author: Ildar Kamalov <ik@adguard.com>
Date: Wed Jul 15 11:55:28 2026 +0300
Merge branch 'master' into AGDNS-4229
commit 10945e24df
Author: Ildar Kamalov <ik@adguard.com>
Date: Tue Jul 14 17:54:41 2026 +0300
fix dockerfile
commit faedfede45
Author: Ildar Kamalov <ik@adguard.com>
Date: Tue Jul 14 17:27:53 2026 +0300
makefile
... and 2 more commits
This commit is contained in:
parent
5a3f84a2f4
commit
e3d638007a
87 changed files with 2639 additions and 1548 deletions
|
|
@ -16,6 +16,8 @@
|
|||
"add_allowlist": "Tilføj hvidliste",
|
||||
"add_blocklist": "Tilføj sortliste",
|
||||
"add_persistent_client": "Tilføj som vedvarende klient",
|
||||
"add_tls_certificate": "Tilføj TLS-certifikat",
|
||||
"add_tls_certificate_private_key": "Tilføj TLS-certifikats private nøgle",
|
||||
"add_to_allowlist": "Føj til hvidliste",
|
||||
"ads_blocked": "Blokerede annoncer",
|
||||
"ads_blocked_card": "Blokerede annoncer",
|
||||
|
|
@ -35,9 +37,14 @@
|
|||
"allowlist_remove": "Fjern hvidliste?",
|
||||
"allowlist_remove_desc": "<strong>%value%</strong> fjernes",
|
||||
"allowlists": "Hvidlister",
|
||||
"allowlists_desc": "Domæner på disse hvidlister tillades, selv hvis de er på en sortliste",
|
||||
"allowlists_title": "Hvidlister",
|
||||
"apply": "Anvend",
|
||||
"aria_clear_input": "Ryd input",
|
||||
"aria_next_page": "Næste side",
|
||||
"aria_previous_page": "Foregående side",
|
||||
"auto_clients_desc": "Information om IP-adresserne på enheder, som kan bruge AdGuard Home",
|
||||
"auto_clients_title": "Runtime",
|
||||
"autofix_warning_list": "<p>Den vil udføre disse opgaver:</p><p>Deaktivere systemets DNSStubListener</p><p>Sætte DNS-serveradressen til 127.0.0.1</p><p>Erstatte det symbolske linkmål for /etc/resolv.conf med /run/systemd/resolve/resolv.conf</p><p>Stoppe DNSStubListener (genindlæse tjenesten systemd-resolved)</p>",
|
||||
"autofix_warning_result": "Det betyder, at alle DNS-forespørgsler fra dit system som standard behandles af AdGuard Home.",
|
||||
"autofix_warning_text": "Klikker du på \"Reparér\", opsætter AdGuard Home dit system til brug med AdGuard Home DNS-server.",
|
||||
|
|
@ -112,6 +119,7 @@
|
|||
"client_identifier": "Identifikator",
|
||||
"client_ip": "Klient-IP",
|
||||
"client_location": "Klientplacering",
|
||||
"client_name_already_exists": "Dette klientnavn findes allerede",
|
||||
"client_removed": "Klient fjernet",
|
||||
"client_settings": "Klientindstillinger",
|
||||
"client_table_header": "Klient",
|
||||
|
|
@ -139,6 +147,8 @@
|
|||
"clients_remove_title": "Fjern klient?",
|
||||
"clients_tags": "Tags",
|
||||
"clients_tags_desc": "Der kan føjes tags til denne klient og inkludere disse i filtreringsregler. <a>Læs mere</a>",
|
||||
"clients_tags_placeholder": "Tilføj tags",
|
||||
"clients_title": "Vedvarende",
|
||||
"clients_upstreams_desc": "Upstream DNS-servere, én pr. linje",
|
||||
"clients_use_dns_cache": "Brug DNS-cache",
|
||||
"clients_use_global_settings": "Brug globale indstillinger",
|
||||
|
|
@ -155,6 +165,7 @@
|
|||
"copy": "Kopiér",
|
||||
"copy_to_clipboard": "Kopiér til udklipsholder",
|
||||
"custom_filtering_rules": "Tilpassede filtreringsregler",
|
||||
"custom_rules": "Tilpassede regler",
|
||||
"dark_theme": "Mørkt tema",
|
||||
"dashboard": "Kontrolpanel",
|
||||
"delete_btn": "Slet",
|
||||
|
|
@ -163,21 +174,30 @@
|
|||
"delete_table_action": "Slet",
|
||||
"delete_table_action_confirm": "Ja, slet",
|
||||
"device_type": "Enhedstype",
|
||||
"dhcp": "DHCP",
|
||||
"dhcp_add_static_lease": "Tilføj statisk lease",
|
||||
"dhcp_config_saved": "DHCP-opsætning gemt",
|
||||
"dhcp_dynamic_ip_found": "Systemet anvender dynamisk IP-adresseosægning for grænsefladen %interface_name%. For at bruge DHCP-server skal en statisk IP-adresse angives. Den aktuelle IP-adresse er %ip%. AdGuard Home vil automatisk angive denne IP-adresse som statisk ved tryk på knappen \"Aktivér DHCP-server\".",
|
||||
"dhcp_edit_static_lease": "Redigér statisk tildeling",
|
||||
"dhcp_enable": "Brug AdGuards egen indbyggede DHCP-server",
|
||||
"dhcp_error": "Mislykkedes at tjekke DHCP-serveren. Vent lidt inden et nyt forsøg",
|
||||
"dhcp_form_gateway_address": "Gateway IP-adresse",
|
||||
"dhcp_form_gateway_address_value": "<strong>Gateway IP-adresse:</strong> %value%",
|
||||
"dhcp_form_gateway_input": "Gateway IP",
|
||||
"dhcp_form_lease_title": "DHCP-lejemålstid i sekunder",
|
||||
"dhcp_form_range_start": "Start-IP-adresse",
|
||||
"dhcp_form_range_title": "IP-adresseområde",
|
||||
"dhcp_form_subnet_input": "Undernetmaske",
|
||||
"dhcp_found": "DHCP-serveren kan ikke aktiveres",
|
||||
"dhcp_hardware_address_value": "<strong>Hardwareadresse:</strong> %value%",
|
||||
"dhcp_interface_select": "DHCP-grænseflade",
|
||||
"dhcp_ip_addresses_value": "<strong>IP-adresser:</strong> %value%",
|
||||
"dhcp_ipv4_settings": "DHCP IPv4-indstillinger",
|
||||
"dhcp_ipv6_settings": "DHCP IPv6-indstillinger",
|
||||
"dhcp_lease_added": "Statisk lejemål \"%key%\" er tilføjet",
|
||||
"dhcp_lease_deleted": "Statisk lejemål \"%key%\" er slettet",
|
||||
"dhcp_lease_updated": "Statisk lejemål \"%key%\" er opdateret",
|
||||
"dhcp_leases": "Dynamiske lejemål",
|
||||
"dhcp_leases_not_found": "Ingen DHCP-leases fundet",
|
||||
"dhcp_leases_title": "DHCP-leases",
|
||||
"dhcp_mac_address_already_added": "Denne MAC-adresse er allerede tilføjet",
|
||||
|
|
@ -189,80 +209,148 @@
|
|||
"dhcp_reset_leases_success": "DHCP- gyldighedsperioder nulstillet",
|
||||
"dhcp_settings": "DHCP-indstillinger",
|
||||
"dhcp_static_ip_error": "DHCP-serveren kræver en statisk IP-adresse. Det kunne ikke bekræfte, at denne netværksgrænseflade har én",
|
||||
"dhcp_static_leases": "Statiske lejemål",
|
||||
"dhcp_static_leases_not_found": "Intet DHCP statisk leases fundet",
|
||||
"dhcp_table_hostname": "Værtsnavn",
|
||||
"dhcp_table_ip_address": "IP-adresse",
|
||||
"dhcp_table_mac_address": "MAC-adresse",
|
||||
"dhcp_v6_unavailable": "Utilgængelig",
|
||||
"dhcp_warning": "For at aktivere denne DHCP-server, deaktivér alle øvrige DHCP-servere på lokalnetværket, da enheder ellers kan miste internetadgang",
|
||||
"disable": "Deaktivér",
|
||||
"disable_dns_rewrites": "Deaktivér DNS-omskrivninger?",
|
||||
"disable_protection_btn": "Deaktivér beskyttelse",
|
||||
"disabled_dhcp": "DHCP-server deaktiveret",
|
||||
"disallow_client_confirm_allowlist_note": "Denne handling undtager også reglen %ip% fra listen over tilladte klienter.",
|
||||
"disallow_client_confirm_text": "Dette dropper alle fremtidige DNS-forespørgsler fra klienten <strong>%ip%</strong> samt fjerner den fra listen over tilladte klienter",
|
||||
"disallow_client_confirm_title": "Afvis denne klient?",
|
||||
"disallow_this_client": "Afvis denne klient",
|
||||
"dns_access_settings_title": "Adgangsindstillinger",
|
||||
"dns_allowed_clients": "Tilladte klienter",
|
||||
"dns_allowed_clients_desc": "Accepterer kun forespørgsler fra klienter på denne liste",
|
||||
"dns_allowed_clients_desc_2": "For at tilføje en klient, angiv dens <a>CIDR, IP-adresse eller ClientID</a>",
|
||||
"dns_allowed_clients_label": "Tilladte klienter, én pr. linje",
|
||||
"dns_allowed_clients_placeholder": "Angiv tilladte klienter",
|
||||
"dns_blocking_mode": "Blokeringstilstand",
|
||||
"dns_blocking_mode_custom_ip": "Tilpasset IP-adresse",
|
||||
"dns_blocking_mode_custom_ip_desc": "Svarer med en manuelt angivet IP-adresse",
|
||||
"dns_blocking_mode_default": "Standard",
|
||||
"dns_blocking_mode_default_desc": "Svarer med 0.0.0.0 for A-poster og :: for AAAA-poster, hvis blokeret af en Adblock-lignende regel. Svarer med den IP-adresse, der er angivet i reglen, hvis blokeret af en hosts-lignende regel",
|
||||
"dns_blocking_mode_desc": "Indstiller svartypen til blokerede forespørgsler",
|
||||
"dns_blocking_mode_ipv4_label": "IPv4-blokering",
|
||||
"dns_blocking_mode_ipv4_placeholder": "Angiv IPv4-adresse",
|
||||
"dns_blocking_mode_ipv6_label": "IPv6-blokering",
|
||||
"dns_blocking_mode_ipv6_placeholder": "Angiv IPv6-adresse",
|
||||
"dns_blocking_mode_null_ip": "Null IP-adresse",
|
||||
"dns_blocking_mode_null_ip_desc": "Svarer med 0.0.0.0 for A og :: for AAAA",
|
||||
"dns_blocking_mode_nxdomain": "NXDOMAIN",
|
||||
"dns_blocking_mode_nxdomain_desc": "Svarer med NXDOMAIN-kode",
|
||||
"dns_blocking_mode_refused": "REFUSED",
|
||||
"dns_blocking_mode_refused_desc": "Returnerer REFUSED-kode",
|
||||
"dns_blocking_mode_title": "Blokeringstilstand",
|
||||
"dns_blocking_mode_ttl_label": "Blokeret svar-TTL i sekunder",
|
||||
"dns_blocking_mode_ttl_placeholder": "Angiv TTL",
|
||||
"dns_bootstrap_dns_desc": "Angiver de DNS-servere, som bruges til at opløse værtsnavne for DoH/DoT-upstream-opløsere",
|
||||
"dns_bootstrap_dns_desc_2": "Angiv kun IP-adresser. Kommentarer er ikke tilladt",
|
||||
"dns_bootstrap_dns_label": "Bootstrap-DNS-servere, én pr. linje",
|
||||
"dns_bootstrap_dns_placeholder": "IP-adresser",
|
||||
"dns_bootstrap_dns_title": "Bootstrap DNS-servere",
|
||||
"dns_bootstrap_servers": "Bootstrap DNS-servere",
|
||||
"dns_cache_config": "DNS-cacheopsætning",
|
||||
"dns_cache_configuration_saved_toast": "DNS-cacheopsætning gemt",
|
||||
"dns_cache_desc": "Gemmer DNS-svar lokalt",
|
||||
"dns_cache_size": "Cache-størrelse",
|
||||
"dns_cache_size_desc": "Angiver DNS-cachestørrelsen",
|
||||
"dns_cache_size_label": "DNS-cachestørrelse i bytes",
|
||||
"dns_cache_size_title": "Cache-størrelse",
|
||||
"dns_cache_title": "DNS-cache",
|
||||
"dns_clear_cache": "Ryd cache",
|
||||
"dns_clear_cache_cancel": "Afbryd",
|
||||
"dns_clear_cache_confirm": "Ja, ryd",
|
||||
"dns_clear_cache_desc": "Dette rydder enhedens DNS-cache og får den til at anmode om nye oplysninger fra DNS-serveren",
|
||||
"dns_clear_cache_title": "Ryd cache?",
|
||||
"dns_clear_cache_yes": "Ja, ryd",
|
||||
"dns_config": "DNS-serveropsætning",
|
||||
"dns_confirm_clear_cache": "Ryd DNS-cache?",
|
||||
"dns_confirm_clear_cache_desc": "Sikker på, at DNS-cache skal ryddes?",
|
||||
"dns_disallowed_clients": "Ikke tilladte klienter",
|
||||
"dns_disallowed_clients_desc": "Dropper forespørgsler fra klienter på denne liste. Indstillingen ignoreres, når der er poster i Tilladte klienter",
|
||||
"dns_disallowed_clients_label": "Ikke-tilladte klienter, én pr. linje",
|
||||
"dns_disallowed_clients_title": "Ikke tilladte klienter",
|
||||
"dns_disallowed_domains": "Ikke tilladte domæner",
|
||||
"dns_disallowed_domains_desc": "Dropper DNS-forespørgsler, som matcher disse domæner, og de vises ikke i Forespørgselslog",
|
||||
"dns_disallowed_domains_desc_2": "Angiv nøjagtige domænenavne, jokertegn eller URL-filterregler, såsom eksempel.org, *.eksempel.org eller ||eksempel.org^",
|
||||
"dns_disallowed_domains_label": "Ikke tilladte domæner, ét pr. linje",
|
||||
"dns_disallowed_domains_title": "Ikke tilladte domæner",
|
||||
"dns_dnssec": "DNSSEC",
|
||||
"dns_dnssec_desc": "Tilføjer DNSSEC-flaget i udgående DNS-forespørgsler og tjekker resultatet. Kræver en DNSSEC-aktiveret opløser",
|
||||
"dns_edns_client_subnet": "EDNS-klientundernet",
|
||||
"dns_edns_client_subnet_desc": "Tilføjer EDNS Client Subnet (ECS) data til upstream-forespørgsler og registrerer ECS-værdier i Forespørgselslog",
|
||||
"dns_edns_custom_label": "Tilpasset IP-adresse til EDNS",
|
||||
"dns_edns_custom_placeholder": "Indtast IP-adresse",
|
||||
"dns_edns_desc": "Tilføj EDNS Client Subnet (ECS) data til upstream-forespørgsler, og registrér ECS-værdier i Forespørgselslog. Der kan bruges standard klientundernettet eller angives en tilpasset IP-adresse til ECS",
|
||||
"dns_edns_option_custom": "Benyt tilasset IP-adresse",
|
||||
"dns_edns_option_default": "Benyt standard EDNS-klientundernet",
|
||||
"dns_edns_title": "EDNS-klientundernet",
|
||||
"dns_fallback_dns_desc": "Angiver reserve-DNS-serverne, som skal benyttes, når upstream-DNS-serverne er utilgængelige",
|
||||
"dns_fallback_dns_desc_2": "Brug samme syntaks som for <a>upstream-DNS-serverne</a>",
|
||||
"dns_fallback_dns_label": "Reserve-DNS-servere, én pr. linje",
|
||||
"dns_fallback_dns_placeholder": "IP-adresser, sdns://, quic://, https://, h3://, or tls://",
|
||||
"dns_fallback_dns_title": "Reserve DNS-servere",
|
||||
"dns_fallback_servers": "Reserve DNS-servere",
|
||||
"dns_ipv6_resolution": "IPv6-opløsning",
|
||||
"dns_ipv6_resolution_desc": "Aktiverer opløsning af DNS-forespørgsler for IPv6-adresser (AAAA-poster) og føjer IPv6-hints til HTTPS-svar",
|
||||
"dns_optimistic_caching": "Optimistisk caching",
|
||||
"dns_optimistic_caching_desc": "Svarer fra cachen, selv hvis posterne er udløbet, og forsøger at opdatere dem",
|
||||
"dns_over_https": "DNS-over-HTTPS",
|
||||
"dns_over_quic": "DNS-over-QUIC",
|
||||
"dns_over_tls": "DNS-over-TLS",
|
||||
"dns_override_max_ttl": "Tilsidesæt maksimal TTL",
|
||||
"dns_override_max_ttl_desc": "Angiver den maksimale tid, DNS-svar cachelagres. Højere TTL-værdier reduceres til denne værdi",
|
||||
"dns_override_max_ttl_label": "Maksimal levetid (TTL) i sekunder",
|
||||
"dns_override_max_ttl_placeholder": "Levetid (TTL)",
|
||||
"dns_override_max_ttl_title": "Tilsidesæt maksimal TTL",
|
||||
"dns_override_min_ttl": "Tilsidesæt minimum TTL",
|
||||
"dns_override_min_ttl_desc": "Angiver den minimale tid, DNS-svar cachelagres. Lavere TTL-værdier forøges til denne værdi",
|
||||
"dns_override_min_ttl_label": "Minimum levetid (TTL) i sekunder",
|
||||
"dns_override_min_ttl_placeholder": "Levetid (TTL)",
|
||||
"dns_override_min_ttl_title": "Tilsidesæt minimum TTL",
|
||||
"dns_privacy": "DNS-fortrolighed",
|
||||
"dns_private_reverse_page_desc": "Opløser PTR-, SOA- og NS-forespørgsler for ARPA-domæner med private IP-adresser via private upstream-servere, DHCP og /etc/hosts. Når deaktiveret, besvares alle sådanne forespørgsler med NXDOMAIN",
|
||||
"dns_private_reverse_resolve_clients_desc": "Opløser klienters IP-adresser til deres værtsnavne vha. PTR-forespørgsler. Bruger private DNS-servere til lokale klienter og upstream-servere til klienter med offentlige IP-adresser",
|
||||
"dns_private_reverse_resolve_clients_title": "Omvendt opslag af klienters IP-adresser",
|
||||
"dns_private_reverse_resolvers": "Private omvendt DNS-opløsere",
|
||||
"dns_private_reverse_resolvers_desc": "Opløser PTR-, SOA- og NS-forespørgsler for ARPA-domæner med private IP-adresser via private upstream-servere, DHCP og /etc/hosts",
|
||||
"dns_private_reverse_resolvers_disabled_desc": "Når deaktiveret, besvares alle sådanne forespørgsler med NXDOMAIN",
|
||||
"dns_private_reverse_servers_desc": "Angiver DNS-servere for private PTR-, SOA- og NS-forespørgsler. Disse forespørgsler kommer fra klienter med private IP-adresser og anmoder om ARPA-domæner indeholdende undernet fra private IP-adresseområder",
|
||||
"dns_private_reverse_servers_desc_2": "Er dette felt tomt, benyttes operativsystemets standard DNS-oplser, undtagen for AdGuard Home IP-adresser. Som standard benytter AdGuard Home flg. omvendt DNS-opløsere: %value_1% og %value_2%",
|
||||
"dns_private_reverse_servers_label": "Private reverse-DNS-servere, én pr. linje",
|
||||
"dns_private_reverse_servers_placeholder": "IP-adresser, sdns://, quic://, https://, h3://, or tls://",
|
||||
"dns_private_reverse_servers_title": "Private reverse DNS-servere",
|
||||
"dns_protocols_title": "DNS-protokoller",
|
||||
"dns_queries": "DNS-forespørgsler",
|
||||
"dns_queries_tooltip": "Antallet af DNS-forespørgsler behandlet i de seneste 24 timer",
|
||||
"dns_query": "DNS-forespørgsler",
|
||||
"dns_rate_limit": "Hyppighedsgrænse",
|
||||
"dns_rate_limit_allowlist": "Frekvensbegrænsningshvidliste",
|
||||
"dns_rate_limit_allowlist_desc": "Angiver IP-adresser undtaget fra hastighedsbegrænsning",
|
||||
"dns_rate_limit_allowlist_label": "IP-adresser, én pr. linje",
|
||||
"dns_rate_limit_allowlist_placeholder": "IP-adresser",
|
||||
"dns_rate_limit_allowlist_title": "Frekvensbegrænsningshvidliste",
|
||||
"dns_rate_limit_desc": "Angive tilladte antal forespørgsler pr. sekund pr. klient",
|
||||
"dns_rate_limit_desc_2": "En værdi på 0 betyder ingen begrænsning",
|
||||
"dns_rate_limit_no_limit": "Ingen begrænsning",
|
||||
"dns_rate_limit_placeholder": "Angiv antalsgrænse",
|
||||
"dns_rate_limit_title": "Hyppighedsgrænse",
|
||||
"dns_rate_limit_value": "%value% foresp./s",
|
||||
"dns_rewrite_exists": "Denne DNS-omskrivning findes allerede",
|
||||
"dns_rewrite_removed": "DNS-omskrivning fjernet",
|
||||
"dns_rewrite_same": "Omskrivning kan ikke ske til det samme domæne eller jokertegn",
|
||||
"dns_rewrites": "DNS-omskrivninger",
|
||||
"dns_rewrites_desc": "Lister de tilpassede DNS-svar for bestemte domænenavne",
|
||||
"dns_server_addresses": "Serveradresser",
|
||||
"dns_server_addresses_configured_in_file": "Serveradresser opsættes i filen <strong>%path%</strong>",
|
||||
"dns_server_addresses_desc": "Indstiller DNS-serveradresser, hvorigennem AdGuard Home vil være tilgængelig",
|
||||
"dns_server_addresses_desc_2": "Se tips til <a>opsætning af upstream-DNS-servere</a> og vores <b>liste over betroede DNS-udbydere</b>",
|
||||
"dns_server_addresses_label": "Serveradresser, én pr. linje",
|
||||
"dns_server_addresses_placeholder": "IP-adresser, sdns://, quic://, https://, h3://, or tls://",
|
||||
"dns_server_addresses_title": "Serveradresser",
|
||||
|
|
@ -270,19 +358,35 @@
|
|||
"dns_server_configuration_saved_toast": "DNS-serveropsætning gemt",
|
||||
"dns_settings": "DNS",
|
||||
"dns_subnet_placeholder": "Angiv præfikslængde",
|
||||
"dns_subnet_prefix": "Undernetpræfikslængde for %value%-adresser",
|
||||
"dns_subnet_prefix_desc": "Indstiller undernetpræfikslængden for %value%-adresser brugt til hastighedsbegrænsning",
|
||||
"dns_subnet_prefix_title": "Undernetpræfikslængde for %value%-adresser",
|
||||
"dns_test_not_ok_toast": "Upstream %key% bestod syntakskontrollen, men dumpede testen. Tjek upstream-indstillingerne",
|
||||
"dns_test_ok_toast": "Alle upstreams er operationelle",
|
||||
"dns_test_parsing_error_toast": "Mislykkedes at udtrække upstream-navnet fra linjen %number% i afsnittet %section%. Tjek linjeformatet",
|
||||
"dns_test_upstreams": "Test upstreams",
|
||||
"dns_test_warning_toast": "Upstream %key% svarede ikke på DNS-forespørgslen. Bekræft upstream-indstillingerne",
|
||||
"dns_ttl_value": "%value% s",
|
||||
"dns_upstream_mode": "Upstream-tilstand",
|
||||
"dns_upstream_mode_desc": "Bestemmer, hvordan lokalnetværkets DNS-forespørgsler skal rutes til flere upstream-servere",
|
||||
"dns_upstream_mode_title": "Upstream-tilstand",
|
||||
"dns_upstream_servers_title": "Upstream DNS-servere",
|
||||
"dns_upstream_timeout": "Upstream-timeout",
|
||||
"dns_upstream_timeout_desc": "Angiver, hvor længe der ventes på et svar fra upstream-servere",
|
||||
"dns_upstream_timeout_placeholder": "Angiv timeout i sekunder",
|
||||
"dns_upstream_timeout_title": "Upstream-timeout",
|
||||
"dns_upstream_validation_invalid": "Ugyldigt format",
|
||||
"dns_upstream_validation_invalid_lines": "Ugyldigt format på linjerne {lines}",
|
||||
"domain": "Domæne",
|
||||
"domain_or_client": "Domæne eller klient",
|
||||
"download_mobileconfig": "Download opsætningsfil",
|
||||
"dynamic_dhcp_leases_not_found": "Dynamiske DHCP-lejemål ikke fundet",
|
||||
"edit_table_action": "Redigér",
|
||||
"enable": "Aktivér",
|
||||
"enable_dns_rewrites": "Aktivér DNS-omskrivninger?",
|
||||
"enabled_dhcp": "DHCP-server aktiveret",
|
||||
"encrypted_dns_addresses": "Krypterede DNS-serveradresser",
|
||||
"encrypted_dns_settings": "Indstillinger for krypteret DNS-server",
|
||||
"encryption_certificate_has_issues": "Certifikatet har problemer",
|
||||
"encryption_certificate_path": "Certifikatsti",
|
||||
"encryption_certificates": "Certifikater",
|
||||
|
|
@ -298,8 +402,10 @@
|
|||
"encryption_disable_plain_dns": "Deaktivér almindelig DNS?",
|
||||
"encryption_disable_plain_dns_desc": "Enheder, som ikke understøtter krypteret DNS, fungerer muligvis ikke korrekt",
|
||||
"encryption_doq": "DNS-over-QUIC port",
|
||||
"encryption_doq_summary": "DNS-over-QUIC-port: %value%",
|
||||
"encryption_doq_tooltip": "Denne port bruges til at køre en DNS-over-QUIC-server",
|
||||
"encryption_dot": "DNS-over-TLS port",
|
||||
"encryption_dot_summary": "DNS-over-TLS-port: %value%",
|
||||
"encryption_dot_tooltip": "Denne port bruges til at køre en DNS-over-TLS-server",
|
||||
"encryption_encrypted_dns": "Krypteret DNS",
|
||||
"encryption_encrypted_dns_desc": "AdGuard Homes admin-grænseflade bruger HTTPS. DNS-serveren understøtter DNS-over-HTTPS, DNS-over-TLS og DNS-over-QUIC",
|
||||
|
|
@ -307,9 +413,12 @@
|
|||
"encryption_force_redirect": "Omdirigér automatisk fra HTTP til HTTPS",
|
||||
"encryption_hostnames": "Værtsnavne: %value%",
|
||||
"encryption_https": "HTTPS-port",
|
||||
"encryption_https_summary": "HTTPS-port: %value%",
|
||||
"encryption_https_tooltip": "Denne port giver HTTPS-adgang til AdGuard Homes admin-grænseflade. Den leverer også DNS-over-HTTPS på stien '/dns-query'",
|
||||
"encryption_invalid_data": "Ugyldige data",
|
||||
"encryption_issuer": "Udsteder: %value%",
|
||||
"encryption_key": "Privat nøgle",
|
||||
"encryption_key_cert_mismatch": "Privat nøgle matcher ikke certifikatet",
|
||||
"encryption_key_input": "Kopiér/indsæt dine PEM-kodede private nøgle til dit certifikat hér.",
|
||||
"encryption_key_invalid": "Privat nøgle er ugyldig",
|
||||
"encryption_key_source_content": "Indsæt indholdet af den private nøgle",
|
||||
|
|
@ -317,20 +426,25 @@
|
|||
"encryption_key_type": "Krypteringsalgoritme: %value%",
|
||||
"encryption_key_valid": "Privat nøgle er gyldig",
|
||||
"encryption_plain_dns": "Almindelig DNS",
|
||||
"encryption_plain_dns_desc": "Almindelig DNS er aktiveret som standard. Deaktivering tvinger alle enheder til at bruge krypteret DNS. For at gøre dette, angiv mindst én krypteret DNS-protokol",
|
||||
"encryption_plain_dns_error": "Aktivér mindst én krypteret DNS-protokol for at deaktivere almindelig DNS",
|
||||
"encryption_private_key_path": "Private nøgle-sti",
|
||||
"encryption_server": "Servernavn",
|
||||
"encryption_server_enter": "Angiv dit domænenavn",
|
||||
"encryption_server_summary": "Servernavn: %value%",
|
||||
"encryption_server_tooltip_1": "Hvis opsat, detekterer AdGuard Home KlientID'er, svarer på DDR-forespørgsler og udfører yderligere forbindelsesvalideringer. Hvis ikke opsat, er disse funktioner deaktiveret",
|
||||
"encryption_server_tooltip_2": "Skal matche et DNS-navn i certifikatet",
|
||||
"encryption_subject": "Emne: %value%",
|
||||
"encryption_title": "Kryptering",
|
||||
"encryption_unable_read_cert": "Kan ikke læse certifikatfil",
|
||||
"encryption_unable_read_key": "Kan ikke læse privat nøglefil",
|
||||
"enter_cache_size": "Angiv cache-størrelse (bytes)",
|
||||
"enter_ip_address_placeholder": "Indtast IP-adresse",
|
||||
"error": "Fejl",
|
||||
"fallback_dns": "Reserve DNS-servere",
|
||||
"filter_added_successfully": "Tilføjet sortliste: <strong>%value%</strong>",
|
||||
"filter_added_successfully_allowlist": "Tilføjet hvidliste: <strong>%value%</strong>",
|
||||
"filter_added_successfully_allowlist_noname": "Hvidliste er tilføjet",
|
||||
"filter_added_successfully_more": "Tilføjede sortlister: <strong>%value%</strong> og %more% flere",
|
||||
"filter_category_general": "Generalt",
|
||||
"filter_category_general_desc": "Lister, som blokerer sporing og reklamer på de fleste enheder",
|
||||
|
|
@ -342,6 +456,7 @@
|
|||
"filter_category_security_desc": "Lister designet specifikt til at blokere malware-, phishing- eller svindel-domæner",
|
||||
"filter_removed_successfully": "Fjernet sortliste: <strong>%value%</strong>",
|
||||
"filter_removed_successfully_allowlist": "Fjernet hvidliste: <strong>%value%</strong>",
|
||||
"filter_removed_successfully_allowlist_noname": "Hvidliste er fjernet",
|
||||
"filters": "Filtre",
|
||||
"forgot_password": "Glemt adgangskode?",
|
||||
"forgot_password_list_item_1": "Opret en ny adgangskode med htpasswd-værktøjet eller anden online BCrypt-generator.",
|
||||
|
|
@ -360,6 +475,8 @@
|
|||
"form_error_domain_format": "Ugyldigt domæneformat",
|
||||
"form_error_equal": "Må ikke svare til.",
|
||||
"form_error_format": "Ugyldigt format",
|
||||
"form_error_format_line": "Ugyldigt format på linje %line%",
|
||||
"form_error_format_lines": "Ugyldigt format på linjerne %lines%",
|
||||
"form_error_gateway_ip": "Lease kan ikke have gatewayens IP-adresse",
|
||||
"form_error_hostname_format": "Brug tal, engelske minuskler og/eller bindestreger",
|
||||
"form_error_ip4_format": "Ugyldig IPv4-adresse",
|
||||
|
|
@ -373,12 +490,17 @@
|
|||
"form_error_port_range": "Angiv portnummer i intervallet 80-65535",
|
||||
"form_error_port_unsafe": "Ikke-sikker port",
|
||||
"form_error_range": "Værdien skal være mellem %min% og %max%",
|
||||
"form_error_required": "Udfyld dette felt",
|
||||
"form_error_server_name": "Ugyldigt servernavn",
|
||||
"form_error_subnet": "IP-adressen skal være i undernettet %cidr%",
|
||||
"form_error_url_format": "Angiv en gyldig URL eller filsti",
|
||||
"form_value_value_from_error": "Angiv en værdi fra %min_value% til %max_value%",
|
||||
"form_value_value_min_error": "Angiv en værdi på %min_value% eller mere",
|
||||
"found_in_known_domains": "Fundet i de kendte domæner",
|
||||
"friday": "Fredag",
|
||||
"gateway_or_subnet_invalid": "Ugyldig undernetmaske",
|
||||
"general_statistics": "Generelle statistikker",
|
||||
"greater_range_start_error": "Slutningen af intervallet skal være højere end starten",
|
||||
"home_dns_addresses": "AdGuard Home DNS-adresser",
|
||||
"home_dns_addresses_desc": "AdGuard Home DNS-serveren lytter på flg. grænseflader/porte:",
|
||||
"ignore_domains_desc_log": "Forespørgsler matchende disse regler vises ikke i Forespørgselslog",
|
||||
|
|
@ -440,6 +562,7 @@
|
|||
"notify_undo": "Fortryd",
|
||||
"notify_updated": "Opdateret",
|
||||
"open_dashboard": "Åbn kontrolpanel",
|
||||
"out_of_range_error": "Adressen må ikke være i DHCP-intervallet %start%–%end%",
|
||||
"parental_control": "Forældrekontrol",
|
||||
"parental_group_ai": "AI",
|
||||
"parental_group_cdn": "CDN",
|
||||
|
|
@ -457,6 +580,7 @@
|
|||
"parental_group_software": "Softwareudvikling",
|
||||
"parental_group_streaming": "Streaming",
|
||||
"password_label": "Adgangskode",
|
||||
"password_login_error": "Brugernavnet og adgangskoden matcher ikke",
|
||||
"password_placeholder": "Angiv adgangskode",
|
||||
"password_requirements": "Adgangskodekrav",
|
||||
"password_requirements_characters": "Minimum 8 tegn",
|
||||
|
|
@ -464,6 +588,7 @@
|
|||
"password_requirements_match": "Adgangskoder matcher",
|
||||
"password_requirements_special": "Engelske bogstaver, tal og specialtegn",
|
||||
"password_requirements_uppercase": "Minimum 1 majuskel",
|
||||
"path_to_file_placeholder": "Sti til fil: Lokal- eller internetadresse",
|
||||
"pause_for_hour": "Pauseret i %count% time",
|
||||
"pause_for_minutes": "| Pauseret i %count% minut | Pauseret i %count% minutter",
|
||||
"pause_for_seconds": "| Pauseret i %count% sekund | Pauseret i %count% sekunder",
|
||||
|
|
@ -477,8 +602,9 @@
|
|||
"processing_time_ms": "%value% ms",
|
||||
"protection": "Beskyttelse",
|
||||
"protocol": "Protokol",
|
||||
"protocols": "Protokoller",
|
||||
"queries": "Forespørgsler",
|
||||
"queries_total": "| %count% forespørgsel i alt | %count% forespørgsler i alt",
|
||||
"queries_total": "| %value% forespørgsel i alt | %value% forespørgsler i alt",
|
||||
"query_details": "Forespørgselsdetaljer",
|
||||
"query_log": "Forespørgselslog",
|
||||
"query_log_all_reasons": "Alle årsager",
|
||||
|
|
@ -513,6 +639,7 @@
|
|||
"query_log_detail_time": "Tid: <span>%value%</span>",
|
||||
"query_log_detail_type": "Type: <span>%value%</span>",
|
||||
"query_log_nothing_available": "Intet blev fundet",
|
||||
"query_log_nothing_available_rotation": "Forespørgselslog er deaktiveret i Generelle indstillinger",
|
||||
"query_log_processed": "Behandlet",
|
||||
"query_log_retention": "Logrotation",
|
||||
"query_log_rewritten": "Omskrevet",
|
||||
|
|
@ -530,6 +657,10 @@
|
|||
"request_table_header": "Forespørgsel",
|
||||
"requests_table_header": "Forespørgsler",
|
||||
"reset": "Nulstil",
|
||||
"reset_dhcp_settings": "Nulstil DHCP-indstillinger",
|
||||
"reset_dns_confirm_text": "Alle DNS-protokolindstillinger nulstilles til standardværdierne",
|
||||
"reset_dns_confirm_title": "Nulstil DNS-protokoller?",
|
||||
"reset_dns_protocols": "Nulstil DNS-protokoller",
|
||||
"reset_settings": "Nulstil indstillinger",
|
||||
"reset_settings_confirm": "Ja, nulstil",
|
||||
"response_details": "Svardetaljer",
|
||||
|
|
@ -537,6 +668,7 @@
|
|||
"result": "Resultat",
|
||||
"resume_protection_timer": "Beskyttelsen genoptages om %time%",
|
||||
"rewrite_add": "Tilføj DNS-omskrivning",
|
||||
"rewrite_domain": "Domænenavn eller jokertegn",
|
||||
"rewrite_domain_input_placeholder": "Angiv domænenavnet eller et jokertegn",
|
||||
"rewrite_edit": "Redigér DNS-omskrivning",
|
||||
"rewrite_hosts_applied": "Omskrevet af værtsfilreglen",
|
||||
|
|
@ -602,6 +734,7 @@
|
|||
"server_config_subnet_len_ipv6_faq": "Længde på undernetpræfiks for IPv6-adresser til hastighedsbegrænsning. Standard er 56",
|
||||
"server_config_subnet_len_placeholder": "Angiv præfikslængde",
|
||||
"set_static_ip": "Opsæt en statisk IP-adresse",
|
||||
"set_static_ip_manually": "Angiv statisk IP-adresse",
|
||||
"settings": "Indstillinger",
|
||||
"settings_anonymize_client_ip": "Anonymisér klienters IP-adresser",
|
||||
"settings_anonymize_client_ip_desc": "Erstat en del af IP-adressen med nuller",
|
||||
|
|
@ -725,6 +858,7 @@
|
|||
"stats_adult": "Blokerede voksne websteder",
|
||||
"stats_query_domain": "Mest forespurgte domæner",
|
||||
"status_table_header": "Status",
|
||||
"subnet_error": "Intervaladresser skal være i samme undernet",
|
||||
"sunday": "Søndag",
|
||||
"system_host_files": "System hosts-filer",
|
||||
"system_theme": "Systemtema",
|
||||
|
|
@ -735,6 +869,14 @@
|
|||
"threats_blocked_tooltip": "Antallet af DNS-forespørgsler blokeret af AdGuard-browsingsikkerhedsmodulet",
|
||||
"thursday": "Torsdag",
|
||||
"time_table_header": "Tid",
|
||||
"tls_cert_modal_description": "For at bruge kryptering skal der angives en gyldig TLS-certifikatkæde for domænet. Et gratis TLS-certifikat kan hentes fra letsencrypt.org eller købes et via en betroet Certifikatmyndighed",
|
||||
"tls_cert_path_label": "Fuld sti til certifikatfilen",
|
||||
"tls_cert_path_option": "Angiv den fulde sti til certifikatfilen",
|
||||
"tls_certificate": "TLS-certifikat",
|
||||
"tls_certificate_expired": "TLS-certifikatet er udløbet",
|
||||
"tls_certificate_expiring": "TLS-certifikatet er ved at udløbe",
|
||||
"tls_key_path_label": "Fuld sti til den private nøglefil",
|
||||
"tls_key_path_option": "Anguv den fulde sti til den private nøglefil",
|
||||
"top_blocked_domains": "Hyppigst blokerede domæner",
|
||||
"top_clients": "Hyppigste klienter",
|
||||
"top_upstreams": "Top-upstreams",
|
||||
|
|
@ -747,14 +889,18 @@
|
|||
"unblock": "Afblokér",
|
||||
"unblock_client": "Afblokér klient",
|
||||
"unknown_filter": "Ukendt filter %filterId%",
|
||||
"update_available": "Version %version% er tilgængelig. <a>Versionsnoter</a>",
|
||||
"update_button": "Opdatér",
|
||||
"update_failed": "Auto-opdatering mislykkedes. Følg <a>disse trin</a> for at opdatere manuelt",
|
||||
"update_filters_custom_hours": "Tilpasset interval i timer",
|
||||
"update_filters_desc": "Angiver intervallet for auto-opdatering af filtre. Gælder for både sortlister og hvidlister tilføjet via eksterne links",
|
||||
"update_filters_disable": "Deaktivér",
|
||||
"update_filters_interval_custom": "Tilpasset",
|
||||
"update_filters_interval_daily": "Hver dag",
|
||||
"update_filters_interval_hourly": "Hver time",
|
||||
"update_filters_interval_weekly": "Hver uge",
|
||||
"update_filters_title": "Opdatér filtre automatisk",
|
||||
"update_how_to": "Sådan opdateres",
|
||||
"updated_upstream_dns_toast": "Upstream-servere gemt",
|
||||
"updates_checked": "En ny version af AdGuard Home er tilgængelig\n",
|
||||
"updates_version_equal": "AdGuard Home er opdateret",
|
||||
|
|
@ -766,7 +912,12 @@
|
|||
"upstream_dns_addresses_faq": "<a>Læs mere</a> om opsætning af upstream-DNS-servere. Her er en <b>liste over kendte DNS-udbydere</b> at vælge imellem.",
|
||||
"upstream_dns_configured_in_file": "Opsat i %path%",
|
||||
"upstream_dns_fastest_addr": "Hurtigste IP-adresse",
|
||||
"upstream_dns_fastest_addr_desc": "Afvent svar fra alle DNS-servere, mål TCP-forbindelseshastigheden for hver server og returnér IP-adressen på den server med den hurtigste forbindelseshastighed",
|
||||
"upstream_dns_fastest_addr_warning": "Denne tilstand kan forsinke DNS-forespørgsler betydeligt, hvis en eller flere upstream-servere ikke svarer. Sørg for, at upstream-serverne er stabile, og at upstream-timeouten er lav",
|
||||
"upstream_dns_load_balancing": "Belastningsfordeling",
|
||||
"upstream_dns_load_balancing_desc": "<p>Forespørg én upstream-server ad gangen</p><p>AdGuard Home bruger en vægtet randomiseringsalgoritme til at vælge servere med det laveste antal mislykkede opslag og den laveste gennemsnitlige opslagstid</p>",
|
||||
"upstream_dns_parallel_requests": "Parallelle forespørgsler",
|
||||
"upstream_dns_parallel_requests_desc": "Brug parallelle forespørgsler til at accelerere opløsing ved at forespørge alle upstream-servere samtidigt",
|
||||
"upstream_dns_placeholder": "IP-adresser, sdns://, quic://, https://, h3://, or tls://",
|
||||
"upstream_dns_servers_title": "Upstream DNS-servere",
|
||||
"upstream_enable_reverse_lookup_desc": "Omvendt opslag af klient-IP-adresser til deres værtsnavne ved at sende PTR-forespørgsler til passende opløsere: Private DNS-servere for lokale klienter eller upstream-servere for klienter med offentlige IP-adresser",
|
||||
|
|
@ -812,6 +963,7 @@
|
|||
"user_rules_check_hostname_placeholder": "Angiv mål",
|
||||
"user_rules_check_title": "Tjek domænefiltrering globalt eller for en klient",
|
||||
"user_rules_cname": "<strong>CNAME:</strong> %cname%",
|
||||
"user_rules_desc": "Angiv én regel pr. linje. Benyt Adblock-stil eller hosts-fil-syntaks",
|
||||
"user_rules_disable_browsing_security": "Deaktivér Browsingsikkerhed",
|
||||
"user_rules_disable_filter": "Deaktivér filter",
|
||||
"user_rules_disable_parental_control": "Deaktivér Forældrekontrol",
|
||||
|
|
@ -844,6 +996,7 @@
|
|||
"user_rules_rewrite_rule_is_applied": "Omskrivningsregel effektueret",
|
||||
"user_rules_rewritten_to": "<strong>Omskrevet til:</strong> %value%",
|
||||
"user_rules_rule": "<strong>Regel:</strong> %rule%",
|
||||
"user_rules_rule_added": "Brugerregel tilføjet: <strong>%rule%</strong>",
|
||||
"user_rules_rule_added_to_custom_filtering_rules": "Regel føjet til tilpassede filtreringsregler",
|
||||
"user_rules_rule_removed": "Regel fjernet fra tilpassede filtreringsregler",
|
||||
"user_rules_safe_search_disabled": "Safe search deaktiveret",
|
||||
|
|
@ -853,12 +1006,16 @@
|
|||
"user_rules_source_allowlist": "<strong>Hvidliste:</strong> %value%",
|
||||
"user_rules_source_blocklist": "<strong>Sortliste:</strong> %value%",
|
||||
"user_rules_status": "<strong>Status:</strong> %reason%",
|
||||
"user_rules_title": "Tilpassede regler",
|
||||
"username_label": "Brugernavn",
|
||||
"username_placeholder": "Angiv brugernavn",
|
||||
"validated_with_dnssec": "Valideret med DNSSEC",
|
||||
"version_number": "Version %value%",
|
||||
"wednesday": "Onsdag",
|
||||
"whois": "WHOIS",
|
||||
"yes": "Ja",
|
||||
"yes_disable": "Ja, deaktivér",
|
||||
"yes_disallow": "Ja, forbyd",
|
||||
"yes_remove": "Ja, fjern",
|
||||
"yes_reset": "Ja, nulstil"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"add_allowlist": "Zulassungsliste hinzufügen",
|
||||
"add_blocklist": "Blockliste hinzufügen",
|
||||
"add_persistent_client": "Als dauerhaften Client hinzufügen",
|
||||
"add_tls_certificate": "TLS-Zertifikat hinzufügen",
|
||||
"add_tls_certificate_private_key": "Privaten Schlüssel des TLS-Zertifikats hinzufügen",
|
||||
"add_to_allowlist": "Zur Positivliste hinzufügen",
|
||||
"ads_blocked": "Anzeigen blockiert",
|
||||
"ads_blocked_card": "Anzeigen blockiert",
|
||||
|
|
@ -113,8 +115,10 @@
|
|||
"client_settings": "Client-Einstellungen",
|
||||
"client_table_header": "Client",
|
||||
"client_unblocked_flash": "Client entsperrt",
|
||||
"clients": "Clients",
|
||||
"clients_add": "Client hinzufügen",
|
||||
"clients_add_identifier": "Kennung hinzufügen",
|
||||
"clients_add_name": "Client-Name",
|
||||
"clients_add_placeholder": "Clientnamen eingeben",
|
||||
"clients_desc": "Regeln für Geräte, die mit AdGuard Home verbunden sind",
|
||||
"clients_dns_cache_size": "Größe des DNS-Cache, in Bytes",
|
||||
|
|
@ -122,14 +126,21 @@
|
|||
"clients_dont_collect_stats": "Keine Statistiken für diesen Client sammeln",
|
||||
"clients_dont_log": "Diesen Client nicht protokollieren",
|
||||
"clients_edit": "Client bearbeiten",
|
||||
"clients_identifier_already_used": "Diese Kennung wird bereits verwendet",
|
||||
"clients_identifier_format_error": "Nur Zahlen, englische Kleinbuchstaben oder Bindestriche",
|
||||
"clients_identifiers": "Kennungen",
|
||||
"clients_logs_and_statistics": "Protokolle und Statistiken",
|
||||
"clients_not_found": "Keine Clients gefunden",
|
||||
"clients_protection": "Schutz",
|
||||
"clients_protection_desc": "Schutzfunktionen, Protokollierung und Statistiken aktivieren oder deaktivieren",
|
||||
"clients_remove_title": "Client entfernen?",
|
||||
"clients_tags": "Schlagwörter",
|
||||
"clients_tags_desc": "Du kannst Tags für diesen Client hinzufügen und sie in Filterregeln einbeziehen. <a>Mehr erfahren</a>",
|
||||
"clients_tags_placeholder": "Tags hinzufügen",
|
||||
"clients_upstreams_desc": "Upstream-DNS-Server, einer pro Zeile",
|
||||
"clients_use_dns_cache": "DNS-Cache verwenden",
|
||||
"clients_use_global_settings": "Allgemeine Einstellungen nutzen",
|
||||
"clients_use_global_settings_desc": "Schutzeinstellungen aus deinem AdGuard Home-Konto übernehmen",
|
||||
"close": "Schließen",
|
||||
"close_result": "Ergebnis schließen",
|
||||
"confirm": "Bestätigen",
|
||||
|
|
@ -144,30 +155,43 @@
|
|||
"dark_theme": "Dunkles Design",
|
||||
"dashboard": "Übersicht",
|
||||
"delete_btn": "Löschen",
|
||||
"delete_confirm": "Lease löschen?",
|
||||
"delete_confirm_desc": "Dadurch wird <b>%ip%</b> freigegeben",
|
||||
"delete_table_action": "Löschen",
|
||||
"device_type": "Gerätetyp",
|
||||
"dhcp": "DHCP",
|
||||
"dhcp_add_static_lease": "Statische Zuweisung hinzufügen",
|
||||
"dhcp_config_saved": "DHCP-Konfiguration erfolgreich gespeichert",
|
||||
"dhcp_edit_static_lease": "Statische Zuweisung bearbeiten",
|
||||
"dhcp_enable": "Den integrierten DHCP-Server von AdGuard nutzen",
|
||||
"dhcp_error": "Fehler bei der Überprüfung des DHCP-Servers. Bitte warte kurz und versuche es erneut",
|
||||
"dhcp_form_gateway_input": "Gateway-IP",
|
||||
"dhcp_form_subnet_input": "Subnetz-Maske",
|
||||
"dhcp_found": "DHCP-Server kann nicht aktiviert werden",
|
||||
"dhcp_ipv4_settings": "DHCP-IPv4-Einstellungen",
|
||||
"dhcp_ipv6_settings": "DHCP-IPv6-Einstellungen",
|
||||
"dhcp_lease_added": "Statische Lease „%key%“ hinzugefügt",
|
||||
"dhcp_lease_deleted": "Statische Lease „%key%“ gelöscht",
|
||||
"dhcp_lease_updated": "Statische Lease „%key%“ aktualisiert",
|
||||
"dhcp_leases": "Dynamische Leases",
|
||||
"dhcp_leases_not_found": "Keine DHCP-Zuweisungen gefunden",
|
||||
"dhcp_leases_title": "DHCP-Zuweisungen",
|
||||
"dhcp_mac_address_already_added": "Diese MAC-Adresse wurde bereits hinzugefügt",
|
||||
"dhcp_new_static_lease": "Neue statische Zuweisung",
|
||||
"dhcp_not_found": "DHCP-Server ist aktiv",
|
||||
"dhcp_reset": "Dies setzt die DHCP-Konfiguration zurück. Alle Änderungen gehen verloren",
|
||||
"dhcp_reset_leases": "Alle Leases zurücksetzen",
|
||||
"dhcp_reset_leases_confirm": "Dies setzt alle Leases zurück",
|
||||
"dhcp_reset_leases_success": "DHCP-Zuweisungen erfolgreich zurückgesetzt",
|
||||
"dhcp_settings": "DHCP-Einstellungen",
|
||||
"dhcp_static_ip_error": "Der DHCP-Server benötigt eine statische IP-Adresse. Wir konnten nicht bestätigen, dass diese Netzwerkschnittstelle über eine solche verfügt",
|
||||
"dhcp_static_leases": "Statische Leases",
|
||||
"dhcp_static_leases_not_found": "Keine statischen DHCP-Zuweisungen gefunden",
|
||||
"dhcp_table_hostname": "Hostname",
|
||||
"dhcp_table_ip_address": "IP-Adresse",
|
||||
"dhcp_v6_unavailable": "Nicht verfügbar",
|
||||
"disable": "Deaktivieren",
|
||||
"disable_dns_rewrites": "DNS-Rewrites deaktivieren?",
|
||||
"disable_protection_btn": "Schutz deaktivieren",
|
||||
"disabled_dhcp": "DHCP-Server deaktiviert",
|
||||
"disallow_this_client": "Diesen Client sperren",
|
||||
|
|
@ -177,14 +201,19 @@
|
|||
"dns_allowed_clients_placeholder": "Zugelassene Clients eingeben",
|
||||
"dns_blocking_mode": "Sperrmodus",
|
||||
"dns_blocking_mode_custom_ip": "Benutzerdefinierte IP-Adresse",
|
||||
"dns_blocking_mode_custom_ip_desc": "Antwortet mit einer manuell festgelegten IP-Adresse",
|
||||
"dns_blocking_mode_default": "Standard",
|
||||
"dns_blocking_mode_desc": "Legt den Antworttyp für blockierte Anfragen fest",
|
||||
"dns_blocking_mode_ipv4_label": "IPv4-Sperren",
|
||||
"dns_blocking_mode_ipv4_placeholder": "IPv4-Adresse eingeben",
|
||||
"dns_blocking_mode_ipv6_label": "IPv6-Sperren",
|
||||
"dns_blocking_mode_ipv6_placeholder": "IPv6-Adresse eingeben",
|
||||
"dns_blocking_mode_null_ip": "Null-IP-Adresse",
|
||||
"dns_blocking_mode_null_ip_desc": "Antwortet mit 0.0.0.0 für A und :: für AAAA",
|
||||
"dns_blocking_mode_nxdomain": "NXDomain",
|
||||
"dns_blocking_mode_nxdomain_desc": "Antwortet mit NXDOMAIN-Code",
|
||||
"dns_blocking_mode_refused": "REFUSED",
|
||||
"dns_blocking_mode_refused_desc": "Antwortet mit REFUSED-Code",
|
||||
"dns_blocking_mode_title": "Sperrmodus",
|
||||
"dns_blocking_mode_ttl_label": "TTL für blockierte Antworten (in Sekunden)",
|
||||
"dns_bootstrap_dns_label": "Bootstrap-DNS-Server, einer pro Zeile",
|
||||
|
|
@ -196,6 +225,7 @@
|
|||
"dns_cache_size": "Größe des Cache",
|
||||
"dns_cache_size_label": "Größe des DNS-Cache, in Bytes",
|
||||
"dns_cache_size_title": "Größe des Cache",
|
||||
"dns_cache_title": "DNS-Cache",
|
||||
"dns_clear_cache": "Cache leeren",
|
||||
"dns_clear_cache_cancel": "Abbrechen",
|
||||
"dns_clear_cache_confirm": "Ja, bitte",
|
||||
|
|
@ -207,7 +237,10 @@
|
|||
"dns_disallowed_domains": "Nicht zugelassene Domains",
|
||||
"dns_disallowed_domains_label": "Nicht zugelassene Domains (eine pro Zeile)",
|
||||
"dns_disallowed_domains_title": "Nicht zugelassene Domains",
|
||||
"dns_dnssec": "DNSSEC",
|
||||
"dns_edns_client_subnet": "EDNS-Client-Subnetz",
|
||||
"dns_edns_custom_placeholder": "IP-Adresse eingeben",
|
||||
"dns_edns_title": "EDNS-Client-Subnetz",
|
||||
"dns_fallback_dns_label": "Fallback-DNS-Server, einer pro Zeile",
|
||||
"dns_fallback_dns_placeholder": "IP-Adressen, sdns://, quic://, https://, h3:// oder tls://",
|
||||
"dns_fallback_dns_title": "Fallback-DNS-Server",
|
||||
|
|
@ -224,15 +257,14 @@
|
|||
"dns_private_reverse_servers_label": "Private Reverse-DNS-Server, einer pro Zeile",
|
||||
"dns_private_reverse_servers_placeholder": "IP-Adressen, sdns://, quic://, https://, h3:// oder tls://",
|
||||
"dns_private_reverse_servers_title": "Private inverse DNS-Server",
|
||||
"dns_protocols_title": "DNS-Protokolle",
|
||||
"dns_queries": "DNS-Anfragen",
|
||||
"dns_queries_tooltip": "Die Anzahl der in den letzten 24 Stunden verarbeiteten DNS-Anfragen",
|
||||
"dns_query": "DNS-Anfragen",
|
||||
"dns_rate_limit": "Begrenzungswert",
|
||||
"dns_rate_limit_allowlist": "Ratenbegrenzung: Zulassungsliste",
|
||||
"dns_rate_limit": "Ratenlimit",
|
||||
"dns_rate_limit_allowlist_placeholder": "IP-Adressen",
|
||||
"dns_rate_limit_allowlist_title": "Ratenbegrenzung: Zulassungsliste",
|
||||
"dns_rate_limit_placeholder": "Begrenzungswert eingeben",
|
||||
"dns_rate_limit_title": "Begrenzungswert",
|
||||
"dns_rate_limit_placeholder": "Ratenlimit eingeben",
|
||||
"dns_rate_limit_title": "Ratenlimit",
|
||||
"dns_rewrites": "DNS-Umschreibungen",
|
||||
"dns_server_addresses": "Serveradressen",
|
||||
"dns_server_addresses_label": "Serveradressen (eine pro Zeile)",
|
||||
|
|
@ -241,19 +273,26 @@
|
|||
"dns_server_config_title": "DNS-Serverkonfiguration",
|
||||
"dns_server_configuration_saved_toast": "DNS-Server-Konfiguration gespeichert",
|
||||
"dns_settings": "DNS",
|
||||
"dns_test_ok_toast": "Alle Upstreams funktionieren",
|
||||
"dns_test_upstreams": "Upstreams testen",
|
||||
"dns_ttl_value": "%value% Sek.",
|
||||
"dns_upstream_mode": "Upstream-Modus",
|
||||
"dns_upstream_mode_title": "Upstream-Modus",
|
||||
"dns_upstream_servers_title": "Upstream-DNS-Server",
|
||||
"dns_upstream_timeout": "Upstream-Timeout",
|
||||
"dns_upstream_timeout_title": "Upstream-Timeout",
|
||||
"dns_upstream_validation_invalid": "Ungültiges Format",
|
||||
"dns_upstream_validation_invalid_lines": "Ungültiges Format in den Zeilen {lines}",
|
||||
"domain": "Domain",
|
||||
"domain_or_client": "Domain oder Client",
|
||||
"download_mobileconfig": "Konfigurationsdatei herunterladen",
|
||||
"dynamic_dhcp_leases_not_found": "Dynamische DHCP-Leases nicht gefunden",
|
||||
"edit_table_action": "Bearbeiten",
|
||||
"enable": "Aktivieren",
|
||||
"enable_dns_rewrites": "DNS-Rewrites aktivieren?",
|
||||
"enabled_dhcp": "DHCP-Server aktiviert",
|
||||
"encrypted_dns_addresses": "Verschlüsselte DNS-Serveradressen",
|
||||
"encrypted_dns_settings": "Einstellungen für verschlüsselten DNS-Server",
|
||||
"encryption_certificate_path": "Zertifikatspfad",
|
||||
"encryption_certificates": "Zertifikate",
|
||||
"encryption_certificates_desc": "Um Verschlüsselung zu nutzen, musst du eine gültige SSL-Zertifikatskette für deine Domain angeben. Du kannst ein kostenloses SSL-Zertifikat von <a>letsencrypt.org</a> erhalten oder eines von einer vertrauenswürdigen Zertifizierungsstelle kaufen",
|
||||
|
|
@ -266,21 +305,27 @@
|
|||
"encryption_disable_plain_dns": "Unverschlüsseltes DNS deaktivieren?",
|
||||
"encryption_disable_plain_dns_desc": "Geräte, die kein verschlüsseltes DNS unterstützen, funktionieren möglicherweise nicht ordnungsgemäß",
|
||||
"encryption_doq": "Port für DNS-over-QUIC",
|
||||
"encryption_doq_summary": "DNS-over-QUIC-Port: %value%",
|
||||
"encryption_doq_tooltip": "Dieser Port wird verwendet, um einen DNS-over-QUIC-Server zu betreiben",
|
||||
"encryption_dot": "DNS-over-TLS",
|
||||
"encryption_dot_summary": "DNS-over-TLS-Port: %value%",
|
||||
"encryption_dot_tooltip": "Dieser Port wird verwendet, um einen DNS-over-TLS-Server zu betreiben",
|
||||
"encryption_encrypted_dns": "Verschlüsseltes DNS",
|
||||
"encryption_encrypted_dns_desc": "Die Verwaltungsoberfläche von AdGuard Home verwendet HTTPS. Der DNS-Server unterstützt DNS-over-HTTPS, DNS-over-TLS und DNS-over-QUIC",
|
||||
"encryption_force_redirect": "Automatisch von HTTP auf HTTPS umleiten",
|
||||
"encryption_https": "HTTPS-Port",
|
||||
"encryption_https_summary": "HTTPS-Port: %value%",
|
||||
"encryption_key": "Privater Schlüssel",
|
||||
"encryption_key_input": "Kopieren Sie Ihren PEM-codierten privaten Schlüssel für Ihr Zertifikat und fügen Sie ihn hier ein.",
|
||||
"encryption_key_invalid": "Privater Schlüssel ist ungültig",
|
||||
"encryption_key_source_content": "Inhalt des privaten Schlüssels einfügen",
|
||||
"encryption_key_valid": "Privater Schlüssel ist gültig",
|
||||
"encryption_plain_dns": "Einfaches DNS",
|
||||
"encryption_plain_dns_error": "Um einfaches DNS zu deaktivieren, aktivieren Sie mindestens ein verschlüsseltes DNS-Protokoll",
|
||||
"encryption_private_key_path": "Pfad des privaten Schlüssels",
|
||||
"encryption_server": "Servername",
|
||||
"encryption_server_enter": "Domain-Namen eingeben",
|
||||
"encryption_server_summary": "Servername: %value%",
|
||||
"encryption_server_tooltip_1": "Wenn diese Option aktiviert ist, erkennt AdGuard Home Client-IDs, antwortet auf DDR-Anfragen und führt zusätzliche Verbindungsprüfungen durch. Andernfalls sind diese Funktionen deaktiviert",
|
||||
"encryption_server_tooltip_2": "Muss einem DNS-Namen im Zertifikat entsprechen",
|
||||
"encryption_title": "Verschlüsselung",
|
||||
|
|
@ -309,15 +354,22 @@
|
|||
"forgot_password_list_title": "Um dein Passwort zu ändern:",
|
||||
"forgot_password_page_desc": "AdGuard Home speichert Passwörter als BCrypt-verschlüsselte Hashes",
|
||||
"form_enter_blocked_response_ttl": "TTL für blockierte Antworten eingeben (Sekunden)",
|
||||
"form_enter_hostname": "Hostname eingeben",
|
||||
"form_enter_ip": "IP-Adresse eingeben",
|
||||
"form_enter_mac": "MAC-Adresse eingeben",
|
||||
"form_error_answer_format": "Ungültiges Ergebnisformat",
|
||||
"form_error_client_id_format": "Client-ID muss nur Zahlen, Kleinbuchstaben und Bindestriche enthalten",
|
||||
"form_error_domain_format": "Ungültiges Domainformat",
|
||||
"form_error_equal": "Sollten nicht übereinstimmen",
|
||||
"form_error_format": "Ungültiges Format",
|
||||
"form_error_format_line": "Ungültiges Format in der Zeile %line%",
|
||||
"form_error_format_lines": "Ungültige Formate in den Zeilen %lines%",
|
||||
"form_error_gateway_ip": "Lease kann nicht die IP-Adresse des Gateways haben",
|
||||
"form_error_ip4_format": "Ungültige IPv4-Adresse",
|
||||
"form_error_ip6_format": "Ungültige IPv6-Adresse",
|
||||
"form_error_ip_already_added": "Diese IP-Adresse wurde bereits hinzugefügt",
|
||||
"form_error_ip_format": "Ungültige IP-Adresse",
|
||||
"form_error_mac_already_added": "Diese MAC-Adresse wurde bereits hinzugefügt",
|
||||
"form_error_mac_format": "Ungültige MAC-Adresse",
|
||||
"form_error_password": "Passwörter stimmen nicht überein",
|
||||
"form_error_port": "Gültige Portnummer eingeben",
|
||||
|
|
@ -325,6 +377,7 @@
|
|||
"form_error_port_unsafe": "Unsicherer Port",
|
||||
"form_error_range": "Der Wert muss zwischen %min% und %max% liegen",
|
||||
"form_error_server_name": "Ungültiger Servername",
|
||||
"form_error_subnet": "Die IP-Adresse sollte im Subnetz %cidr% sein",
|
||||
"form_error_url_format": "Geben Sie eine gültige URL oder einen Dateipfad ein",
|
||||
"found_in_known_domains": "In den bekannten Domains gefunden",
|
||||
"friday": "Freitag",
|
||||
|
|
@ -359,6 +412,7 @@
|
|||
"install_step": "Schritt",
|
||||
"install_submit_title": "Gratulation!",
|
||||
"instructions": "Anleitungen",
|
||||
"invalid": "Ungültig",
|
||||
"ip_address": "IP-Adresse",
|
||||
"ip_addresses_placeholder": "IP-Adressen eingeben, eine pro Zeile",
|
||||
"known_tracker": "Bekannte Tracker",
|
||||
|
|
@ -409,6 +463,7 @@
|
|||
"password_requirements_match": "Passwörter stimmen überein",
|
||||
"password_requirements_special": "Englische Buchstaben, Zahlen und Sonderzeichen",
|
||||
"password_requirements_uppercase": "Mindestens 1 Großbuchstabe",
|
||||
"path_to_file_placeholder": "Pfad zur Datei: lokale oder Internetadresse",
|
||||
"pause_for_hour": "Pause für %count% Stunde",
|
||||
"pause_until_tomorrow": "Pause bis morgen, %time%",
|
||||
"period_notify": "Um die angezeigten Zeiträume zu ändern, bearbeite die <i>Aufbewahrung der Statistiken</i> in den <a>Allgemeinen Einstellungen</a>",
|
||||
|
|
@ -419,9 +474,12 @@
|
|||
"processing_time_ms": "%value% ms",
|
||||
"protection": "Schutz",
|
||||
"protocol": "Protokoll",
|
||||
"protocols": "Protokolle",
|
||||
"queries": "Anfragen",
|
||||
"query_details": "Anfragedetails",
|
||||
"query_log": "Anfragenprotokoll",
|
||||
"query_log_all_reasons": "Alle Gründe",
|
||||
"query_log_all_statuses": "Alle Status",
|
||||
"query_log_allowed": "Auf Freigabeliste",
|
||||
"query_log_blocked": "Blockiert",
|
||||
"query_log_blocked_by_filter": "Durch Filter blockiert",
|
||||
|
|
@ -444,7 +502,9 @@
|
|||
"query_log_detail_reason": "Grund:",
|
||||
"query_log_detail_response": "Antwort:",
|
||||
"query_log_detail_response_code": "Antwortcode: <span>%value%</span>",
|
||||
"query_log_detail_rules": "Regel(n):",
|
||||
"query_log_detail_service_name": "Dienstname: <span>%value%</span>",
|
||||
"query_log_detail_source": "Quelle: <span>%value%</span>",
|
||||
"query_log_detail_status": "Status: <span>%value%</span>",
|
||||
"query_log_detail_time": "Zeit: <span>%value%</span>",
|
||||
"query_log_detail_type": "Typ: <span>%value%</span>",
|
||||
|
|
@ -466,6 +526,10 @@
|
|||
"request_table_header": "Anfrage",
|
||||
"requests_table_header": "Anfragen",
|
||||
"reset": "Zurücksetzen",
|
||||
"reset_dhcp_settings": "DHCP-Einstellungen zurücksetzen",
|
||||
"reset_dns_confirm_text": "Alle DNS-Protokolleinstellungen werden auf ihre Standardwerte zurückgesetzt",
|
||||
"reset_dns_confirm_title": "DNS-Protokolle zurücksetzen?",
|
||||
"reset_dns_protocols": "DNS-Protokolle zurücksetzen",
|
||||
"reset_settings": "Einstellungen zurücksetzen",
|
||||
"reset_settings_confirm": "Ja, zurücksetzen",
|
||||
"response_details": "Einzelheiten der Antwort",
|
||||
|
|
@ -499,6 +563,7 @@
|
|||
"save": "Speichern",
|
||||
"save_btn": "Speichern",
|
||||
"search_placeholder": "Suchen",
|
||||
"selected": "%value% ausgewählt",
|
||||
"server_config_blocking_mode": "Sperrmodus",
|
||||
"server_config_blocking_mode_custom_ip_desc": "<strong>Benutzerdefinierte IP:</strong> Mit einer manuell eingestellten IP-Adresse antworten",
|
||||
"server_config_blocking_mode_ipv4": "IPv4-Sperren",
|
||||
|
|
@ -519,20 +584,20 @@
|
|||
"server_config_edns_enable": "EDNS-Client-Subnetz aktivieren",
|
||||
"server_config_null_ip": "Null-IP-Adresse",
|
||||
"server_config_nxdomain": "NXDomain",
|
||||
"server_config_rate_limit": "Ratenbegrenzung, RPS",
|
||||
"server_config_rate_limit_placeholder": "Begrenzungswert eingeben",
|
||||
"server_config_rate_limit_whitelist": "Ratenbegrenzung: Zulassungsliste",
|
||||
"server_config_rate_limit_whitelist_faq": "IP-Adressen, die von der Ratebegrenzung ausgeschlossen sind",
|
||||
"server_config_rate_limit_placeholder": "Ratenlimit eingeben",
|
||||
"server_config_rate_limit_whitelist_faq": "IP-Adressen, die vom Ratenlimit ausgenommen sind",
|
||||
"server_config_refused": "REFUSED",
|
||||
"server_config_subnet_len_ipv4": "Länge des Subnetzpräfixes für IPv4-Adressen",
|
||||
"server_config_subnet_len_ipv4_faq": "Subnetpräfixlänge für IPv4-Adressen, die für die Ratebegrenzung verwendet werden. Der Standardwert ist 24",
|
||||
"server_config_subnet_len_ipv4_faq": "Subnetz-Präfixlänge für IPv4-Adressen, die für das Ratenlimit verwendet wird. Der Standardwert ist 24",
|
||||
"server_config_subnet_len_ipv6": "Subnetzpräfixlänge für IPv6-Adressen",
|
||||
"server_config_subnet_len_ipv6_faq": "Subnetpräfixlänge für IPv6-Adressen, die für die Ratebegrenzung verwendet werden. Der Standardwert ist 56",
|
||||
"server_config_subnet_len_ipv6_faq": "Subnetz-Präfixlänge für IPv6-Adressen, die für das Ratenlimit verwendet wird. Der Standardwert ist 56",
|
||||
"set_static_ip": "Feste IP-Adresse festlegen",
|
||||
"set_static_ip_manually": "Statische IP-Adresse festlegen",
|
||||
"settings": "Einstellungen",
|
||||
"settings_anonymize_client_ip": "Client-IP-Adressen anonymisieren",
|
||||
"settings_anonymize_client_ip_desc": "Einen Teil der IP-Adresse durch Nullen ersetzen",
|
||||
"settings_browsing_security": "Internetsicherheit",
|
||||
"settings_browsing_security_desc": "Zugriff auf Domains blockieren, die in der Datenbank für bösartige und Phishing-Domains gelistet sind",
|
||||
"settings_confirm_clear_query_log": "Anfragenprotokoll leeren?",
|
||||
"settings_confirm_clear_query_log_desc": "Dadurch werden alle Anfragen aus dem <strong>Anfragenprotokoll</strong> gelöscht. Die Statistiken bleiben weiterhin verfügbar",
|
||||
"settings_confirm_clear_statistics": "Statistiken leeren?",
|
||||
|
|
@ -653,12 +718,14 @@
|
|||
"threats_blocked_tooltip": "Anzahl der DNS-Anfragen, die vom AdGuard-Modul „Internetsicherheit“ blockiert wurden",
|
||||
"thursday": "Donnerstag",
|
||||
"time_table_header": "Zeit",
|
||||
"tls_certificate": "TLS-Zertifikat",
|
||||
"top_blocked_domains": "Am häufigsten gesperrte Domains",
|
||||
"top_clients": "Top Clients",
|
||||
"top_upstreams": "Top Upstreams",
|
||||
"total": "Insgesamt",
|
||||
"try_again": "Erneut versuchen",
|
||||
"tuesday": "Dienstag",
|
||||
"type_value": "Typ: %value%",
|
||||
"unavailable_dhcp": "DHCP ist nicht verfügbar",
|
||||
"unavailable_dhcp_desc": "AdGuard Home konnte keinen DHCP-Server auf Ihrem Betriebssystem ausführen",
|
||||
"unblock": "Entsperren",
|
||||
|
|
@ -682,6 +749,7 @@
|
|||
"upstream_dns_addresses": "Serveradressen (eine pro Zeile)",
|
||||
"upstream_dns_configured_in_file": "Konfiguriert in %path%",
|
||||
"upstream_dns_fastest_addr": "Schnellste IP-Adresse",
|
||||
"upstream_dns_load_balancing": "Lastverteilung",
|
||||
"upstream_dns_parallel_requests": "Paralleles Abfragen",
|
||||
"upstream_dns_placeholder": "IP-Adressen, sdns://, quic://, https://, h3:// oder tls://",
|
||||
"upstream_dns_servers_title": "Upstream-DNS-Server",
|
||||
|
|
@ -704,6 +772,7 @@
|
|||
"user_rules": "Benutzerregeln",
|
||||
"user_rules_add_to_allowlist": "Zur Positivliste hinzufügen",
|
||||
"user_rules_allow_service": "Service zulassen",
|
||||
"user_rules_browsing_security_disabled": "Internetsicherheit deaktiviert",
|
||||
"user_rules_check_button": "Prüfen",
|
||||
"user_rules_check_client_label": "Client-Kennung (Name, Client-ID oder IP-Adresse)",
|
||||
"user_rules_check_client_placeholder": "Client-Kennung eingeben",
|
||||
|
|
@ -711,8 +780,10 @@
|
|||
"user_rules_check_hostname_label": "Hostname oder Domainname",
|
||||
"user_rules_check_hostname_placeholder": "Ziel eingeben",
|
||||
"user_rules_cname": "<strong>CNAME:</strong> %cname%",
|
||||
"user_rules_disable_browsing_security": "Internetsicherheit deaktivieren",
|
||||
"user_rules_disable_filter": "Filter deaktivieren",
|
||||
"user_rules_disable_parental_control": "Kindersicherung deaktivieren",
|
||||
"user_rules_disable_safe_search": "Sichere Suche deaktivieren",
|
||||
"user_rules_dns_record_type_placeholder": "DNS-Datensatztyp auswählen",
|
||||
"user_rules_dns_rewrite_removed": "Regel aus DNS-Rewrite entfernt",
|
||||
"user_rules_domain": "<strong>Domain:</strong> %value%",
|
||||
|
|
@ -727,10 +798,12 @@
|
|||
"user_rules_reason": "<strong>Grund:</strong> %reason%",
|
||||
"user_rules_reason_blocked_by": "Blockiert durch %source%",
|
||||
"user_rules_redirected_to": "<strong>Weitergeleitet zu:</strong> %value%",
|
||||
"user_rules_remove_dns_rewrite": "DNS-Rewrite entfernen",
|
||||
"user_rules_rewrite_rule_is_applied": "Umschreibungsregel ist angewendet",
|
||||
"user_rules_rewritten_to": "<strong>Umgeschrieben zu:</strong> %value%",
|
||||
"user_rules_rule": "<strong>Regel:</strong> %rule%",
|
||||
"user_rules_rule_added_to_custom_filtering_rules": "Regel zu benutzerdefinierten Filterregeln hinzugefügt",
|
||||
"user_rules_safe_search_disabled": "Sichere Suche deaktiviert",
|
||||
"user_rules_service": "<strong>Service:</strong> %service%",
|
||||
"user_rules_service_allowed": "Der Dienst <strong>%value%</strong> zugelassen",
|
||||
"user_rules_source": "<strong>Quelle:</strong> %value%",
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -478,7 +478,7 @@
|
|||
"protection": "Protección",
|
||||
"protocol": "Protocolo",
|
||||
"queries": "Consultas",
|
||||
"queries_total": "| %count% consulta en total | %count% consultas en total",
|
||||
"queries_total": "| %value% consulta en total | %value% consultas en total",
|
||||
"query_details": "Detalles de la consulta",
|
||||
"query_log": "Registro de consultas",
|
||||
"query_log_all_reasons": "Todos los motivos",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@
|
|||
"add_allowlist": "Ajouter liste d’autorisation",
|
||||
"add_blocklist": "Ajouter liste de blocage",
|
||||
"add_persistent_client": "Ajouter comme client persistant",
|
||||
"add_tls_certificate": "Ajouter un certificat TLS",
|
||||
"add_tls_certificate_private_key": "Ajouter la clé privée du certificat TLS",
|
||||
"add_to_allowlist": "Ajouter à la liste blanche",
|
||||
"ads_blocked": "Annonces bloquées",
|
||||
"ads_blocked_card": "Annonces bloquées",
|
||||
|
|
@ -28,6 +30,10 @@
|
|||
"allowlists_title": "Listes d'autorisation",
|
||||
"apply": "Appliquer",
|
||||
"aria_clear_input": "Effacer la saisie",
|
||||
"aria_next_page": "Page suivante",
|
||||
"aria_previous_page": "Page précédente",
|
||||
"auto_clients_desc": "Informations sur les adresses IP des appareils pouvant utiliser AdGuard Home",
|
||||
"auto_clients_title": "Durée d'exécution",
|
||||
"autofix_warning_result": "Par conséquent, toutes les demandes DNS de votre système seront traitées par AdGuardHome par défaut.",
|
||||
"autofix_warning_text": "Si vous cliquez sur « Réparer », AdGuard Home configurera votre système pour utiliser le serveur DNS AdGuard Home.",
|
||||
"average_time_processing": "Temps moyen de traitement",
|
||||
|
|
@ -41,6 +47,7 @@
|
|||
"blocked_queries": "Requêtes bloquées",
|
||||
"blocked_service": "Service bloqué",
|
||||
"blocked_services": "Services bloqués",
|
||||
"blocked_services_desc": "Sélectionnez les sites web et services populaires qui doivent être bloqués",
|
||||
"blocked_threats": "Menaces bloquées",
|
||||
"blocked_threats_chart": "Menaces bloquées",
|
||||
"blocklist_add_from_list": "Ajouter à partir de la liste",
|
||||
|
|
@ -76,6 +83,7 @@
|
|||
"check_not_found": "Introuvable dans vos listes de filtres",
|
||||
"check_updates_btn": "Vérifier les mises à jour",
|
||||
"clear_btn": "Effacer",
|
||||
"clear_query_log": "Vider le Journal des requêtes",
|
||||
"client_add": "Ajouter un client",
|
||||
"client_added": "Client ajouté",
|
||||
"client_blocked_flash": "Client bloqué",
|
||||
|
|
@ -86,6 +94,7 @@
|
|||
"client_identifier": "Identifiant",
|
||||
"client_ip": "IP du client",
|
||||
"client_location": "Localisation du client",
|
||||
"client_name_already_exists": "Ce nom de client existe déjà",
|
||||
"client_removed": "Client supprimé",
|
||||
"client_settings": "Paramètres du client",
|
||||
"client_table_header": "Client",
|
||||
|
|
@ -112,6 +121,7 @@
|
|||
"clients_remove_title": "Supprimer le client ?",
|
||||
"clients_tags": "Mots clés",
|
||||
"clients_tags_desc": "Vous pouvez ajouter des balises pour ce client et les inclure dans vos règles de filtrage. <a>En savoir plus</a>",
|
||||
"clients_title": "Persistant",
|
||||
"clients_upstreams_desc": "Serveurs DNS en amont, un par ligne",
|
||||
"clients_use_dns_cache": "Utiliser le cache DNS",
|
||||
"clients_use_global_settings": "Utiliser les paramètres généraux",
|
||||
|
|
@ -125,39 +135,80 @@
|
|||
"copy": "Copier",
|
||||
"copy_to_clipboard": "Copier dans le clipboard",
|
||||
"custom_filtering_rules": "Règles de filtrage d'utilisateur",
|
||||
"custom_rules": "Règles personnalisées",
|
||||
"dark_theme": "Thème sombre",
|
||||
"dashboard": "Tableau de bord",
|
||||
"delete_btn": "Supprimer",
|
||||
"delete_confirm": "Supprimer le bail ?",
|
||||
"delete_confirm_desc": "Cela libérera <b>%ip%</b>",
|
||||
"delete_table_action": "Supprimer",
|
||||
"delete_table_action_confirm": "Oui, supprimer",
|
||||
"device_type": "Type d'appareil",
|
||||
"dhcp": "DHCP",
|
||||
"dhcp_add_static_lease": "Ajoutez un bail statique",
|
||||
"dhcp_config_saved": "Configuration du serveur DHCP sauvegardée",
|
||||
"dhcp_dynamic_ip_found": "Votre système utilise une configuration d'adresse IP dynamique pour l'interface %interface_name%. Pour pouvoir utiliser le serveur DHCP, vous devez définir une adresse IP statique. Votre adresse IP actuelle est %ip%. AdGuard Home définira automatiquement cette adresse IP comme statique si vous cliquez sur le bouton « Activer le serveur DHCP ».",
|
||||
"dhcp_edit_static_lease": "Modifier le bail statique",
|
||||
"dhcp_enable": "Utiliser le serveur DHCP intégré à AdGuard",
|
||||
"dhcp_error": "Échec de la vérification du serveur DHCP. Veuillez patienter un instant avant de réessayer",
|
||||
"dhcp_form_gateway_address": "Adresse IP de la passerelle",
|
||||
"dhcp_form_gateway_address_value": "<strong>Adresse IP de la passerelle :</strong> %value%",
|
||||
"dhcp_form_gateway_input": "IP de la passerelle",
|
||||
"dhcp_form_lease_title": "Durée de bail DHCP, en secondes",
|
||||
"dhcp_form_range_start": "Adresse IP de début",
|
||||
"dhcp_form_range_title": "Plage d'adresses IP",
|
||||
"dhcp_form_subnet_input": "Masque de sous-réseau",
|
||||
"dhcp_found": "Le serveur DHCP ne peut pas être activé",
|
||||
"dhcp_hardware_address_value": "<strong>Adresse matérielle :</strong> %value%",
|
||||
"dhcp_interface_select": "Interface DHCP",
|
||||
"dhcp_ip_addresses_value": "<strong>Adresses IP :</strong> %value%",
|
||||
"dhcp_ipv4_settings": "Paramètres DHCP IPv4",
|
||||
"dhcp_ipv6_settings": "Paramètres DHCP IPv6",
|
||||
"dhcp_lease_added": "Bail statique \"%key%\" ajouté avec succès",
|
||||
"dhcp_lease_deleted": "Bail statique \"%key%\" supprimé avec succès",
|
||||
"dhcp_lease_updated": "Bail statique \"%key%\" mis à jour avec succès",
|
||||
"dhcp_leases": "Baux dynamiques",
|
||||
"dhcp_leases_not_found": "Aucun bail DHCP trouvé",
|
||||
"dhcp_leases_title": "Locations des serveurs DHCP",
|
||||
"dhcp_mac_address_already_added": "Cette adresse MAC est déjà ajoutée",
|
||||
"dhcp_new_static_lease": "Nouveau bail statique",
|
||||
"dhcp_not_found": "Le serveur DHCP est actif",
|
||||
"dhcp_reset": "Cela réinitialisera la configuration DHCP. Toutes les modifications seront perdues",
|
||||
"dhcp_reset_leases": "Réinitialiser tous les baux",
|
||||
"dhcp_reset_leases_confirm": "Cela réinitialisera tous les baux",
|
||||
"dhcp_reset_leases_success": "Les baux DHCP ont été réinitialisés avec succès",
|
||||
"dhcp_settings": "Paramètres DHCP",
|
||||
"dhcp_static_ip_error": "Le serveur DHCP nécessite une adresse IP statique. Nous n'avons pas pu confirmer que cette interface réseau en possède une",
|
||||
"dhcp_static_leases_not_found": "Aucun bail statique DHCP trouvé",
|
||||
"dhcp_table_hostname": "Nom d'hôte",
|
||||
"dhcp_table_ip_address": "Adresse IP",
|
||||
"dhcp_table_mac_address": "Adresse MAC",
|
||||
"dhcp_v6_unavailable": "Indisponible",
|
||||
"dhcp_warning": "Pour activer ce serveur DHCP, désactivez tous les autres serveurs DHCP de votre réseau. Sinon, certains appareils risquent de perdre accès à Internet",
|
||||
"disable": "Désactiver",
|
||||
"disable_protection_btn": "Désactiver la protection",
|
||||
"disabled_dhcp": "Serveur DHCP désactivé",
|
||||
"disallow_client_confirm_allowlist_note": "Cette action exclura également la règle %ip% de la liste des clients autorisés.",
|
||||
"disallow_client_confirm_text": "Cela bloquera toutes les requêtes DNS futures provenant du client <strong>%ip%</strong> et le supprimera de la liste des clients autorisés",
|
||||
"disallow_client_confirm_title": "Bloquer ce client ?",
|
||||
"disallow_this_client": "Interdire ce client",
|
||||
"dns_access_settings_title": "Paramètres d'accès",
|
||||
"dns_allowed_clients": "Clients autorisés",
|
||||
"dns_allowed_clients_desc": "N'accepte que les demandes provenant des clients figurant sur cette liste",
|
||||
"dns_allowed_clients_desc_2": "Pour ajouter un client, saisissez son <a>CIDR, son adresse IP ou son ClientID</a>",
|
||||
"dns_allowed_clients_label": "Clients autorisés, un par ligne",
|
||||
"dns_allowed_clients_placeholder": "Saisir les clients autorisés",
|
||||
"dns_blocking_mode": "Mode du blocage",
|
||||
"dns_blocking_mode_custom_ip": "Adresse IP personnalisée",
|
||||
"dns_blocking_mode_custom_ip_desc": "Répond avec une adresse IP configurée manuellement",
|
||||
"dns_blocking_mode_default": "Défaut",
|
||||
"dns_blocking_mode_default_desc": "Renvoie 0.0.0.0 pour les enregistrements A et :: pour les enregistrements AAAA s'il est bloqué par une règle de type Adblock. Renvoie l'adresse IP spécifiée dans la règle s'il est bloqué par une règle de type « hosts ».",
|
||||
"dns_blocking_mode_desc": "Définit le type de réponse pour les requêtes bloquées",
|
||||
"dns_blocking_mode_ipv4_label": "Blocage IPv4",
|
||||
"dns_blocking_mode_ipv4_placeholder": "Saisissez l'adresse IPv4",
|
||||
"dns_blocking_mode_ipv6_label": "Blocage IPv6",
|
||||
"dns_blocking_mode_ipv6_placeholder": "Saisissez l'adresse IPv6",
|
||||
"dns_blocking_mode_null_ip": "Adresse IP nulle",
|
||||
"dns_blocking_mode_nxdomain": "NXDOMAIN",
|
||||
"dns_blocking_mode_refused": "REFUSED",
|
||||
"dns_blocking_mode_title": "Mode du blocage",
|
||||
|
|
@ -195,6 +246,7 @@
|
|||
"dns_privacy": "Confidentialité DNS",
|
||||
"dns_private_reverse_servers_label": "Serveurs DNS inversés privés, un par ligne",
|
||||
"dns_private_reverse_servers_title": "Serveurs DNS privés inverses",
|
||||
"dns_protocols_title": "Protocoles DNS",
|
||||
"dns_queries": "Requêtes DNS",
|
||||
"dns_query": "Requêtes DNS",
|
||||
"dns_rate_limit": "Limite de taux",
|
||||
|
|
@ -208,7 +260,11 @@
|
|||
"dns_server_config_title": "Configuration du serveur DNS",
|
||||
"dns_server_configuration_saved_toast": "Configuration du serveur DNS enregistrée",
|
||||
"dns_settings": "DNS",
|
||||
"dns_test_not_ok_toast": "Le serveur amont %key% a passé la vérification de syntaxe mais a échoué au test. Vérifiez les paramètres du serveur en amont",
|
||||
"dns_test_ok_toast": "Toutes les amonts sont opérationnels",
|
||||
"dns_test_parsing_error_toast": "Impossible d'extraire le nom de l'amont de la ligne %number% dans la section %section%. Vérifiez le format de la ligne",
|
||||
"dns_test_upstreams": "Tester les upstreams",
|
||||
"dns_test_warning_toast": "Le serveur en amont %key% n’a pas répondu à la requête DNS. Vérifiez les réglages en amont",
|
||||
"dns_upstream_servers_title": "Serveurs DNS upstream",
|
||||
"dns_upstream_timeout": "Délai d'attente en amont",
|
||||
"dns_upstream_timeout_title": "Délai d'attente en amont",
|
||||
|
|
@ -220,6 +276,7 @@
|
|||
"edit_table_action": "Éditer",
|
||||
"enable": "Activer",
|
||||
"enabled_dhcp": "Serveur DHCP activé",
|
||||
"encrypted_dns_settings": "Paramètres du serveur DNS chiffré",
|
||||
"encryption_certificate_has_issues": "Le certificat présente des problèmes",
|
||||
"encryption_certificate_path": "Emplacement du certificat",
|
||||
"encryption_certificates": "Certificats",
|
||||
|
|
@ -233,35 +290,51 @@
|
|||
"encryption_confirm_clear": "Réinitialiser les paramètres de chiffrement",
|
||||
"encryption_confirm_clear_desc": "Tous les paramètres de chiffrement seront réinitialisés à leurs valeurs par défaut",
|
||||
"encryption_disable_plain_dns": "Désactiver le DNS simple ?",
|
||||
"encryption_disable_plain_dns_desc": "Les appareils qui ne prennent pas en charge le DNS chiffré peuvent ne pas fonctionner correctement",
|
||||
"encryption_doq": "Port DNS sur QUIC",
|
||||
"encryption_doq_summary": "Port DNS-over-QUIC : %value%",
|
||||
"encryption_doq_tooltip": "Ce port sert à faire fonctionner un serveur DNS-over-QUIC",
|
||||
"encryption_dot": "Port DNS-over-TLS",
|
||||
"encryption_dot_summary": "Port DNS-over-TLS : %value%",
|
||||
"encryption_dot_tooltip": "Ce port sert à faire fonctionner un serveur DNS-over-TLS",
|
||||
"encryption_encrypted_dns": "DNS chiffré",
|
||||
"encryption_encrypted_dns_desc": "L'interface d'administration d'AdGuard Home utilise HTTPS. Le serveur DNS prend en charge DNS-over-HTTPS, DNS-over-TLS et DNS-over-QUIC",
|
||||
"encryption_expire": "Expire : %value%",
|
||||
"encryption_force_redirect": "Rediriger automatiquement de HTTP vers HTTPS",
|
||||
"encryption_hostnames": "Noms d'hôtes : %value%",
|
||||
"encryption_https": "Port HTTPS",
|
||||
"encryption_https_summary": "Port HTTPS : %value%",
|
||||
"encryption_https_tooltip": "Ce port fournit l'accès HTTPS à l'interface d'administration d'AdGuard Home. Il fournit également le DNS-over-HTTPS à l'emplacement '/dns-query'",
|
||||
"encryption_invalid_data": "Données non valides",
|
||||
"encryption_issuer": "Émetteur : %value%",
|
||||
"encryption_key": "Clé privée",
|
||||
"encryption_key_cert_mismatch": "La clé privée ne correspond pas au certificat",
|
||||
"encryption_key_input": "Copiez/coller votre clé privée PEM encodée pour votre certificat ici.",
|
||||
"encryption_key_invalid": "La clé privée n'est pas valide",
|
||||
"encryption_key_source_content": "Coller les contenus de la clef privée",
|
||||
"encryption_key_source_path": "Définir un fichier de clé privée",
|
||||
"encryption_key_type": "Algorithme de chiffrement : %value%",
|
||||
"encryption_key_valid": "La clé privée est valide",
|
||||
"encryption_plain_dns": "DNS brut",
|
||||
"encryption_plain_dns_desc": "Le DNS standard est activé par défaut. Sa désactivation oblige tous les appareils à utiliser un DNS chiffré. Pour ça, indiquez au moins un protocole DNS chiffré",
|
||||
"encryption_plain_dns_error": "Pour désactiver le DNS simple, activez au moins un protocole DNS crypté",
|
||||
"encryption_private_key_path": "Emplacement de la clef privée",
|
||||
"encryption_server": "Nom du serveur",
|
||||
"encryption_server_enter": "Entrez votre nom de domaine",
|
||||
"encryption_server_summary": "Nom du serveur : %value%",
|
||||
"encryption_server_tooltip_1": "Si cette option est activée, AdGuard Home détectera les ClientID, répondra aux requêtes DDR et effectuera des validations supplémentaires de la connexion. Si elle n'est pas activée, ces fonctionnalités sont désactivées",
|
||||
"encryption_server_tooltip_2": "Doit correspondre à un nom DNS figurant dans le certificat",
|
||||
"encryption_subject": "Objet : %value%",
|
||||
"encryption_title": "Chiffrement",
|
||||
"encryption_unable_read_cert": "Impossible de lire le fichier de certificat",
|
||||
"encryption_unable_read_key": "Impossible de lire le fichier de clé privée",
|
||||
"enter_cache_size": "Entrer la taille du cache (octets)",
|
||||
"enter_ip_address_placeholder": "Saisissez l'adresse IP",
|
||||
"error": "Erreur",
|
||||
"fallback_dns": "Serveurs DNS de repli",
|
||||
"filter_added_successfully": "Liste de blocage ajoutée : <strong>%value%</strong>",
|
||||
"filter_added_successfully_allowlist": "Liste d’autorisation ajoutée : <strong>%value%</strong>",
|
||||
"filter_added_successfully_allowlist_noname": "La liste blanche a été ajoutée avec succès",
|
||||
"filter_added_successfully_more": "Listes de blocage ajoutées : <strong>%value%</strong> et %more% plus",
|
||||
"filter_category_general": "General",
|
||||
"filter_category_general_desc": "Listes qui bloquent le suivi et la publicité sur la plupart des appareils",
|
||||
|
|
@ -273,35 +346,63 @@
|
|||
"filter_category_security_desc": "Listes créées exprès pour bloquer les logiciels malveillants, des domaines hameçonneurs ou frauduleux",
|
||||
"filter_removed_successfully": "Liste de blocage supprimée : <strong>%value%</strong>",
|
||||
"filter_removed_successfully_allowlist": "Liste d'autorisation supprimée : <strong>%value%</strong>",
|
||||
"filter_removed_successfully_allowlist_noname": "La liste autorisée a été supprimée",
|
||||
"filters": "Filtres",
|
||||
"forgot_password": "Mot de passe oublié ?",
|
||||
"forgot_password_list_item_1": "Créez un nouveau mot de passe à l'aide de l'outil htpasswd ou de n'importe quel générateur BCrypt en ligne.",
|
||||
"forgot_password_list_item_2": "Arrêter AdGuard Home.",
|
||||
"forgot_password_list_item_3": "Ouvrez AdGuardHome.yaml, repérez le champ du mot de passe et remplacez son contenu par la nouvelle valeur.",
|
||||
"forgot_password_list_item_4": "Démarrer AdGuard Home.",
|
||||
"forgot_password_list_item_5": "Connectez-vous à l'interface web avec votre nouveau mot de passe.",
|
||||
"forgot_password_list_title": "Pour changer votre mot de passe :",
|
||||
"forgot_password_page_desc": "AdGuard Home stocke les mots de passe sous forme de hachages chiffrés avec BCrypt",
|
||||
"form_enter_blocked_response_ttl": "Saisir le TTL de la réponse bloquée (secondes)",
|
||||
"form_enter_hostname": "Saisissez un nom d'hôte",
|
||||
"form_enter_ip": "Saisissez une adresse IP",
|
||||
"form_enter_mac": "Saisissez une adresse MAC",
|
||||
"form_error_answer_format": "Format de résultat invalide",
|
||||
"form_error_client_id_format": "L'ID du client ne doit contenir que des chiffres, des lettres minuscules et des traits d'union",
|
||||
"form_error_domain_format": "Format de domaine invalide",
|
||||
"form_error_equal": "Ne doit pas être égal",
|
||||
"form_error_format": "Format invalide",
|
||||
"form_error_format_line": "Format invalide à la ligne %line%",
|
||||
"form_error_format_lines": "Format invalide aux lignes %lines%",
|
||||
"form_error_gateway_ip": "Le bail ne peut pas avoir d'adresse IP de la passerelle",
|
||||
"form_error_hostname_format": "Utilisez des chiffres, des lettres anglaises minuscules ou des traits d'union",
|
||||
"form_error_ip4_format": "Adresse IPv4 invalide",
|
||||
"form_error_ip6_format": "Adresse IPv6 invalide",
|
||||
"form_error_ip_already_added": "Cette adresse IP est déjà ajoutée",
|
||||
"form_error_ip_format": "Adresse e-mail non valide",
|
||||
"form_error_mac_already_added": "Cette adresse MAC est déjà ajoutée",
|
||||
"form_error_mac_format": "Adresse MAC invalide",
|
||||
"form_error_password": "Mots de passe différents",
|
||||
"form_error_port": "Saisissez un numéro de port valide",
|
||||
"form_error_port_range": "Saisissez une valeur de port entre 80 et 65535",
|
||||
"form_error_port_unsafe": "Port non fiable",
|
||||
"form_error_required": "Remplissez ce champ",
|
||||
"form_error_server_name": "Nom de serveur invalide",
|
||||
"form_error_subnet": "L'adresse IP doit appartenir au sous-réseau %cidr%",
|
||||
"form_error_url_format": "Saisissez une URL ou un chemin d\\'accès au fichier valide",
|
||||
"form_value_value_from_error": "Saisissez une valeur comprise entre %min_value% et %max_value%",
|
||||
"form_value_value_min_error": "Saisissez une valeur égale ou supérieure à %min_value%",
|
||||
"friday": "Vendredi",
|
||||
"gateway_or_subnet_invalid": "Masque de sous-réseau invalide.",
|
||||
"general_statistics": "Statistiques générales",
|
||||
"greater_range_start_error": "La fin de la plage doit être supérieure au début",
|
||||
"ignore_domains_desc_log": "Les requêtes qui correspondent à ces règles ne sont pas affichées dans le Journal des requêtes",
|
||||
"ignore_domains_desc_stats": "Les requêtes correspondant à ces règles ne sont pas incluses dans les statistiques",
|
||||
"ignore_domains_title": "Domaines ignorés",
|
||||
"inactivity_schedule": "Horaire d'inactivité",
|
||||
"inactivity_schedule_add": "Ajouter un horaire",
|
||||
"inactivity_schedule_all_day": "Toute la journée",
|
||||
"inactivity_schedule_delete": "Supprimer l'horaire",
|
||||
"inactivity_schedule_delete_desc": "Le blocage des services sera actif toute la journée le <strong>%value%</strong>",
|
||||
"inactivity_schedule_desc": "Définir des périodes d'inactivité pour le blocage des services",
|
||||
"inactivity_schedule_edit": "Modifier l'horaire",
|
||||
"inactivity_schedule_no_schedules": "Aucun horaire",
|
||||
"inactivity_schedule_replace_desc": "Cet horaire remplacera tout autre planning existant pour le même jour de la semaine. Chaque jour ne peut avoir qu'une seule période d'inactivité",
|
||||
"inactivity_schedule_time_from": "Heure de début",
|
||||
"inactivity_schedule_time_to": "Heure de fin",
|
||||
"inactivity_schedule_timezone": "Fuseau horaire",
|
||||
"install_auth_confirm": "Confirmer le mot de passe",
|
||||
"install_auth_password": "Mot de passe",
|
||||
|
|
@ -316,15 +417,20 @@
|
|||
"install_step": "Étape",
|
||||
"install_submit_title": "Félicitations !",
|
||||
"instructions": "Instructions",
|
||||
"invalid": "Invalide",
|
||||
"ip_address": "Adresse IP",
|
||||
"ip_addresses_placeholder": "Saisissez les adresses IP, une par ligne",
|
||||
"known_tracker": "Traqueur connu",
|
||||
"language": "Langue",
|
||||
"last_days": "| %count% dernier jour | %count% derniers jours",
|
||||
"last_hour": "Dernière heure",
|
||||
"last_hours": "| %count% dernière heure | %count% dernières heures",
|
||||
"last_updated_label": "Dernière mise à jour",
|
||||
"light_theme": "Thème clair",
|
||||
"login": "Connexion",
|
||||
"logout": "Déconnexion",
|
||||
"make_static": "Rendre statique",
|
||||
"make_static_desc": "Cette action convertit une adresse dynamique en adresse statique",
|
||||
"milliseconds_abbreviation": "ms",
|
||||
"monday": "Lundi",
|
||||
"name": "Nom",
|
||||
|
|
@ -339,11 +445,17 @@
|
|||
"notify_undo": "Annuler",
|
||||
"notify_updated": "Mis à jour",
|
||||
"open_dashboard": "Ouvrir le tableau de bord",
|
||||
"out_of_range_error": "L'adresse ne doit pas se situer dans la plage DHCP %start%–%end%",
|
||||
"parental_control": "Contrôle Parental",
|
||||
"parental_group_ai": "IA",
|
||||
"parental_group_cdn": "CDN",
|
||||
"parental_group_dating": "Rencontres",
|
||||
"parental_group_development": "Développement de logiciels",
|
||||
"parental_group_gambling": "Jeux d'argent, paris sportifs",
|
||||
"parental_group_gaming": "Jeux vidéo",
|
||||
"parental_group_hosting": "Hébergement Web",
|
||||
"parental_group_messaging": "Messagerie",
|
||||
"parental_group_messenger": "Messagerie",
|
||||
"parental_group_privacy": "Outils de confidentialité",
|
||||
"parental_group_shopping": "Shopping",
|
||||
"parental_group_social": "Réseaux sociaux",
|
||||
|
|
@ -351,15 +463,27 @@
|
|||
"parental_group_software": "Développement de logiciels",
|
||||
"parental_group_streaming": "Streaming",
|
||||
"password_label": "Mot de passe",
|
||||
"password_login_error": "Votre nom d'utilisateur et votre mot de passe ne correspondent pas",
|
||||
"password_placeholder": "Entrer le mot de passe",
|
||||
"password_requirements": "Exigences du mot de passe",
|
||||
"password_requirements_characters": "Au moins 8 caractères",
|
||||
"password_requirements_lowercase": "Au moins 1 lettre minuscule",
|
||||
"password_requirements_match": "Les mots de passe correspondent",
|
||||
"password_requirements_special": "Lettres anglaises, chiffres et caractères spéciaux",
|
||||
"password_requirements_uppercase": "Au moins 1 lettre majuscule",
|
||||
"path_to_file_placeholder": "Chemin d'accès au fichier : adresse locale ou Internet",
|
||||
"pause_for_hour": "Suspendre pour %count% heure",
|
||||
"pause_until_tomorrow": "Mettre en pause jusqu'à demain, %time%",
|
||||
"period_notify": "Pour modifier les périodes affichées, modifiez <i>Conservation des statistiques</i> dans <a>Paramètres généraux</a>",
|
||||
"plain_dns": "DNS brut",
|
||||
"port_in_use": "Ce port est déjà utilisé",
|
||||
"privacy_policy": "Politique de confidentialité",
|
||||
"protection": "Protection",
|
||||
"protocol": "Protocole",
|
||||
"protocols": "Protocoles",
|
||||
"queries": "Requêtes",
|
||||
"query_details": "Détails de la requête",
|
||||
"query_log": "Journal des requêtes",
|
||||
"query_log_all_reasons": "Toutes les raisons",
|
||||
"query_log_all_statuses": "Tous les états",
|
||||
"query_log_allowed": "En Liste blanche",
|
||||
|
|
@ -392,6 +516,7 @@
|
|||
"query_log_detail_time": "Heure : <span>%value%</span>",
|
||||
"query_log_detail_type": "Type : <span>%value%</span>",
|
||||
"query_log_nothing_available": "Rien n'a été trouvé",
|
||||
"query_log_nothing_available_rotation": "Le journal des requêtes est désactivé dans les paramètres généraux",
|
||||
"query_log_processed": "Traités",
|
||||
"query_log_retention": "Rotation des journaux",
|
||||
"query_log_rewritten": "Réécrit",
|
||||
|
|
@ -409,6 +534,10 @@
|
|||
"request_table_header": "Requête",
|
||||
"requests_table_header": "Requêtes",
|
||||
"reset": "Réinitialiser",
|
||||
"reset_dhcp_settings": "Réinitialiser les paramètres DHCP",
|
||||
"reset_dns_confirm_text": "Tous les paramètres du protocole DNS seront réinitialisés à leurs valeurs par défaut",
|
||||
"reset_dns_confirm_title": "Réinitialiser les protocoles DNS ?",
|
||||
"reset_dns_protocols": "Réinitialiser les protocoles DNS",
|
||||
"reset_settings": "Réinitialiser les paramètres",
|
||||
"reset_settings_confirm": "Oui, réinitialiser",
|
||||
"response_details": "Détails de la réponse",
|
||||
|
|
@ -440,6 +569,7 @@
|
|||
"server_config_disable_ipv6": "Désactiver la résolution des adresses IPv6",
|
||||
"server_config_dnssec_enable": "Activer DNSSEC",
|
||||
"server_config_edns_enable": "Activer le sous-réseau du client EDNS",
|
||||
"server_config_null_ip": "Adresse IP nulle",
|
||||
"server_config_nxdomain": "NXDOMAIN",
|
||||
"server_config_rate_limit_placeholder": "Entrez la limite de taux",
|
||||
"server_config_rate_limit_whitelist_faq": "Adresses IP exclues de la limitation du débit",
|
||||
|
|
@ -449,24 +579,38 @@
|
|||
"server_config_subnet_len_ipv6": "Longueur du préfixe de sous-réseau pour les adresses IPv6",
|
||||
"server_config_subnet_len_ipv6_faq": "Longueur du préfixe de sous-réseau pour les adresses IPv6 utilisé pour la limitation de débit. La valeur par défaut est 56",
|
||||
"set_static_ip": "Définir une adresse IP statique",
|
||||
"set_static_ip_manually": "Configurer une adresse IP statique",
|
||||
"settings": "Paramètres",
|
||||
"settings_browsing_security": "Navigation sécurisée",
|
||||
"settings_confirm_clear_query_log": "Vider le Journal des requêtes ?",
|
||||
"settings_confirm_clear_query_log_desc": "Ceci supprimera toutes les requêtes du <strong>Journal des requêtes</strong>. Les statistiques resteront disponibles",
|
||||
"settings_confirm_clear_statistics": "Effacer les statistiques?",
|
||||
"settings_confirm_clear_statistics_desc": "Ceci effacera toutes les statistiques. Le <strong>Journal des requêtes</strong> restera disponible",
|
||||
"settings_confirm_decrease_log_rotation_interval": "Réduire l’intervalle de rotation des journaux ?",
|
||||
"settings_confirm_decrease_log_rotation_interval_desc": "Cette opération supprimera tous les journaux plus anciens que %value%",
|
||||
"settings_confirm_decrease_stats_rotation_interval": "Réduire l’intervalle de rétention des statistiques ?",
|
||||
"settings_confirm_decrease_stats_rotation_interval_desc": "Ceci supprimera toutes les statistiques antérieures à %value%",
|
||||
"settings_custom": "Personnalisé",
|
||||
"settings_domain_names": "Noms de domaine, un par ligne",
|
||||
"settings_general_short": "General",
|
||||
"settings_global": "Général",
|
||||
"settings_notify_changes_saved": "Modifications enregistrées",
|
||||
"settings_notify_query_log_cleared": "Journal des requêtes vidé",
|
||||
"settings_notify_statistics_cleared": "Statistiques effacées",
|
||||
"settings_parental_control": "Contrôle parental",
|
||||
"settings_rotation_placeholder": "Utilisez des chiffres",
|
||||
"settings_rule_syntax": "Syntaxe des règles",
|
||||
"settings_safe_search": "Recherche sécurisée",
|
||||
"settings_statistics": "Statistiques",
|
||||
"settings_statistics_clear": "Réinitialiser les statistiques",
|
||||
"settings_statistics_desc": "Afficher les statistiques de requêtes et de clients sur le tableau de bord",
|
||||
"settings_statistics_retention": "Maintien des statistiques",
|
||||
"settings_statistics_retention_hours": "Durée de conservation des statistiques, en heures",
|
||||
"settings_tooltip_domain_names": "Noms de domaine, caractères génériques ou règles de filtrage",
|
||||
"settings_tooltip_examples": "Exemples :",
|
||||
"settings_yes_clear": "Oui, effacer",
|
||||
"settings_yes_decrease": "Oui, diminuer",
|
||||
"setup_complete": "La configuration est terminée et AdGuard Home est prêt à l'emploi",
|
||||
"setup_devices_android_title": "Android",
|
||||
"setup_devices_dns_android_list_1": "Android 9 prend en charge nativement <strong>DNS-over-TLS</strong>. Pour le configurer, rendez-vous dans <i>Paramètres → Réseau et Internet → Avancé → DNS privé</i> et saisissez votre nom de domaine.",
|
||||
"setup_devices_dns_list_devices": "Voici une liste des logiciels que vous pouvez utiliser.",
|
||||
|
|
@ -480,8 +624,16 @@
|
|||
"setup_dns_desc": "Configurez votre routeur ou vos autres appareils pour qu'ils utilisent les adresses de serveurs DNS suivantes :",
|
||||
"setup_dns_privacy_ioc_mac": "Configuration sur iOS et macOS",
|
||||
"setup_dns_privacy_other_title": "Autres implémentations",
|
||||
"setup_dns_quote_desc": "Pour fonctionner correctement, le serveur DNS AdGuard Home nécessite une adresse IP statique sur votre réseau local. Sinon, votre routeur pourrait finir par lui attribuer une adresse IP différente et perturber la protection DNS",
|
||||
"setup_dns_quote_title": "Attribuez une adresse IP statique au serveur DNS d'AdGuard Home sur votre réseau local",
|
||||
"setup_dns_title": "Définir l'adresse du serveur DNS AdGuard Home",
|
||||
"setup_dns_title_banner": "Paramètres du serveur DNS",
|
||||
"setup_guide_title": "Configurez vos appareils pour utiliser AdGuard Home",
|
||||
"setup_ui_desc": "Utilisez ces adresses pour accéder à l'interface utilisateur Web d'AdGuard Home. Si nécessaire, modifiez le port",
|
||||
"setup_ui_title": "Définissez l'adresse de l'interface web pour AdGuard Home",
|
||||
"setup_ui_title_banner": "Paramètres de l’interface Web",
|
||||
"show_blocked_responses": "Bloqué",
|
||||
"show_more_count": "+ %count% de plus",
|
||||
"show_processed_responses": "Traités",
|
||||
"source_label": "Source",
|
||||
"source_tooltip": "Source",
|
||||
|
|
@ -489,6 +641,7 @@
|
|||
"stats_adult": "Sites à contenu adulte bloqués",
|
||||
"stats_query_domain": "Domaines les plus recherchés",
|
||||
"status_table_header": "État",
|
||||
"subnet_error": "Les adresses de la plage doivent appartenir au même sous-réseau",
|
||||
"sunday": "Dimanche",
|
||||
"system_host_files": "Fichier d'hôtes système",
|
||||
"table_client": "Client",
|
||||
|
|
@ -497,6 +650,14 @@
|
|||
"threats_blocked": "Menaces bloquées",
|
||||
"thursday": "Jeudi",
|
||||
"time_table_header": "Temps",
|
||||
"tls_cert_modal_description": "Pour utiliser le chiffrement, vous devez fournir une chaîne de certificats TLS valide pour votre domaine. Vous pouvez obtenir un certificat TLS gratuit sur letsencrypt.org ou en acheter un auprès d'une autorité de certification de confiance",
|
||||
"tls_cert_path_label": "Chemin d'accès complet au fichier de certificat",
|
||||
"tls_cert_path_option": "Saisissez le chemin d'accès complet au fichier de certificat",
|
||||
"tls_certificate": "Certificat TLS",
|
||||
"tls_certificate_expired": "Votre certificat TLS a expiré",
|
||||
"tls_certificate_expiring": "Votre certificat TLS est sur le point d'expirer",
|
||||
"tls_key_path_label": "Chemin d'accès complet au fichier de clé privée",
|
||||
"tls_key_path_option": "Saisissez le chemin d'accès complet vers le fichier de clé privée",
|
||||
"top_blocked_domains": "Les domaines les plus fréquemment bloqués",
|
||||
"top_clients": "Meilleurs clients",
|
||||
"top_upstreams": "Top amonts",
|
||||
|
|
@ -509,6 +670,7 @@
|
|||
"unblock": "Débloquer",
|
||||
"unblock_client": "Débloquer le client",
|
||||
"unknown_filter": "Filtre inconnu %filterId%",
|
||||
"update_available": "La version %version% est disponible. <a>Notes de mise à jour</a>",
|
||||
"update_button": "Mettre à jour",
|
||||
"update_filters_custom_hours": "Intervalle personnalisé, en heures",
|
||||
"update_filters_disable": "Désactiver",
|
||||
|
|
@ -517,6 +679,7 @@
|
|||
"update_filters_interval_hourly": "Chaque heure",
|
||||
"update_filters_interval_weekly": "Chaque semaine",
|
||||
"update_filters_title": "Mettre à jour automatiquement les filtres",
|
||||
"update_how_to": "Comment effectuer la mise à jour",
|
||||
"updated_upstream_dns_toast": "Serveurs en amont enregistrés",
|
||||
"updates_checked": "Une nouvelle version de AdGuard Home est disponible",
|
||||
"updates_version_equal": "AdGuard Home est à jour",
|
||||
|
|
@ -584,6 +747,7 @@
|
|||
"user_rules_rewrite_rule_is_applied": "Règle de réécriture appliquée",
|
||||
"user_rules_rewritten_to": "<strong>Réécrit en :</strong> %value%",
|
||||
"user_rules_rule": "<strong>Règle :</strong> %rule%",
|
||||
"user_rules_rule_added": "Règle utilisateur ajoutée : <strong>%rule%</strong>",
|
||||
"user_rules_rule_added_to_custom_filtering_rules": "Règle ajoutée aux règles de filtrage personnalisées",
|
||||
"user_rules_rule_removed": "Règle supprimée des règles de filtrage personnalisées",
|
||||
"user_rules_safe_search_disabled": "Recherche sécurisée désactivée",
|
||||
|
|
@ -593,12 +757,15 @@
|
|||
"user_rules_source_allowlist": "<strong>Liste d’autorisation :</strong> %value%",
|
||||
"user_rules_source_blocklist": "<strong>Liste de blocage :</strong> %value%",
|
||||
"user_rules_status": "<strong>État :</strong> %reason%",
|
||||
"user_rules_title": "Règles personnalisées",
|
||||
"username_label": "Nom d'utilisateur",
|
||||
"username_placeholder": "Saisir un nom d'utilisateur",
|
||||
"validated_with_dnssec": "Validé avec DNSSEC",
|
||||
"version_number": "Version %value%",
|
||||
"wednesday": "Mercredi",
|
||||
"whois": "WHOIS",
|
||||
"yes": "Oui",
|
||||
"yes_disable": "Oui, désactiver",
|
||||
"yes_disallow": "Oui, refuser",
|
||||
"yes_reset": "Oui, réinitialiser"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@
|
|||
"protection": "Protezione",
|
||||
"protocol": "Protocollo",
|
||||
"queries": "Richieste",
|
||||
"queries_total": "| %count% richiesta totale | %count% richieste totali",
|
||||
"queries_total": "| %value% richiesta totale | %value% richieste totali",
|
||||
"query_details": "Dettagli della richiesta",
|
||||
"query_log": "Registro richieste",
|
||||
"query_log_all_reasons": "Tutti i motivi",
|
||||
|
|
|
|||
|
|
@ -604,7 +604,7 @@
|
|||
"protocol": "Protocol",
|
||||
"protocols": "Protocollen",
|
||||
"queries": "Zoekopdrachten",
|
||||
"queries_total": "| %count% zoekopdracht totaal | %count% zoekopdrachten totaal",
|
||||
"queries_total": "| %value% zoekopdracht totaal | %value% zoekopdrachten totaal",
|
||||
"query_details": "Details van de zoekopdracht",
|
||||
"query_log": "Zoekopdrachtlogboek",
|
||||
"query_log_all_reasons": "Alle redenen",
|
||||
|
|
|
|||
|
|
@ -16,6 +16,8 @@
|
|||
"add_allowlist": "Adicionar lista de permissões",
|
||||
"add_blocklist": "Adicionar lista de bloqueio",
|
||||
"add_persistent_client": "Adicionar como cliente persistente",
|
||||
"add_tls_certificate": "Adicionar certificado TLS",
|
||||
"add_tls_certificate_private_key": "Adicionar a chave privada do certificado TLS",
|
||||
"add_to_allowlist": "Adicionar na lista de permissões",
|
||||
"ads_blocked": "anúncios bloqueados",
|
||||
"ads_blocked_card": "anúncios bloqueados",
|
||||
|
|
@ -38,6 +40,10 @@
|
|||
"allowlists_title": "Lista de permissões",
|
||||
"apply": "Aplicar",
|
||||
"aria_clear_input": "Limpar entrada",
|
||||
"aria_next_page": "Próxima página",
|
||||
"aria_previous_page": "Página anterior",
|
||||
"auto_clients_desc": "Informações sobre os endereços IP dos dispositivos que podem usar o AdGuard Home",
|
||||
"auto_clients_title": "Tempo de execução",
|
||||
"autofix_warning_list": "<p>Ele irá realizar estas tarefas: </p><p>Desativar sistema DNSStubListener</p><p>Definir endereço do servidor DNS para 127.0.0.1</p><p>Substituir o alvo simbólico do link /etc/resolv.conf para /run/systemd/resolv.conf</p><p>Parar DNSStubListener (recarregar serviço resolvido pelo sistema)</p>",
|
||||
"autofix_warning_result": "Como resultado, todos as solicitações DNS do seu sistema serão processadas pelo AdGuard Home por padrão.",
|
||||
"autofix_warning_text": "Se clicar em \"Corrigir\", o AdGuardHome irá configurar o seu sistema para utilizar o servidor DNS do AdGuardHome.",
|
||||
|
|
@ -139,6 +145,8 @@
|
|||
"clients_remove_title": "Remover cliente?",
|
||||
"clients_tags": "Marcadores",
|
||||
"clients_tags_desc": "Você pode adicionar etiquetas para este cliente e incluí-las nas regras de filtragem. <a>Saiba mais</a>",
|
||||
"clients_tags_placeholder": "Adicionar etiquetas",
|
||||
"clients_title": "Persistente",
|
||||
"clients_upstreams_desc": "Servidores DNS upstream, um por linha",
|
||||
"clients_use_dns_cache": "Usar cache de DNS",
|
||||
"clients_use_global_settings": "Usar configurações global",
|
||||
|
|
@ -155,6 +163,7 @@
|
|||
"copy": "Copiar",
|
||||
"copy_to_clipboard": "Copiar para área de transferência",
|
||||
"custom_filtering_rules": "Regras de filtragem personalizadas",
|
||||
"custom_rules": "Regras personalizadas",
|
||||
"dark_theme": "Tema escuro",
|
||||
"dashboard": "Painel",
|
||||
"delete_btn": "Excluir",
|
||||
|
|
@ -163,21 +172,27 @@
|
|||
"delete_table_action": "Excluir",
|
||||
"delete_table_action_confirm": "Sim, excluir",
|
||||
"device_type": "Tipo de dispositivo",
|
||||
"dhcp": "DHCP",
|
||||
"dhcp_add_static_lease": "Adicionar nova concessão estática",
|
||||
"dhcp_config_saved": "Configurações DHCP salvas com sucesso",
|
||||
"dhcp_edit_static_lease": "Editar concessão estática",
|
||||
"dhcp_enable": "Usar o servidor DHCP integrado do AdGuard",
|
||||
"dhcp_error": "Falha ao verificar o servidor DHCP. Aguarde um pouco antes de tentar novamente",
|
||||
"dhcp_form_gateway_address": "Endereço IP do gateway",
|
||||
"dhcp_form_gateway_input": "IP do gateway",
|
||||
"dhcp_form_lease_title": "Tempo de concessão do DHCP, em segundos",
|
||||
"dhcp_form_range_start": "Endereço IP inicial",
|
||||
"dhcp_form_range_title": "Intervalo de endereços IP",
|
||||
"dhcp_form_subnet_input": "Máscara de sub-rede",
|
||||
"dhcp_found": "O servidor DHCP não pode ser ativado",
|
||||
"dhcp_interface_select": "Interface DHCP",
|
||||
"dhcp_ip_addresses_value": "<strong>Endereço de IP:</strong> %value%",
|
||||
"dhcp_ipv4_settings": "Configurações DHCP IPv4",
|
||||
"dhcp_ipv6_settings": "Configurações DHCP IPv6",
|
||||
"dhcp_lease_added": "Concessão estática \"%key%\" adicionada com sucesso",
|
||||
"dhcp_lease_deleted": "Concessão estática \"%key%\" excluída com sucesso",
|
||||
"dhcp_lease_updated": "Concessão estática \"%key%\" adicionada com sucesso",
|
||||
"dhcp_leases": "Concessões dinâmicas",
|
||||
"dhcp_leases_not_found": "Nenhuma concessão DHCP encontrada",
|
||||
"dhcp_leases_title": "Concessões DHCP",
|
||||
"dhcp_mac_address_already_added": "Este endereço MAC já foi adicionado",
|
||||
|
|
@ -189,11 +204,14 @@
|
|||
"dhcp_reset_leases_success": "Concessões de DHCP redefinidas com sucesso",
|
||||
"dhcp_settings": "Configurações de DHCP",
|
||||
"dhcp_static_ip_error": "O servidor DHCP requer um endereço IP estático. Não foi possível confirmar que esta interface de rede possui um endereços IP estático",
|
||||
"dhcp_static_leases": "Concessões estáticas",
|
||||
"dhcp_static_leases_not_found": "Nenhuma concessão DHCP estática foi encontrada",
|
||||
"dhcp_table_hostname": "Nome do servidor",
|
||||
"dhcp_table_ip_address": "Endereço de IP",
|
||||
"dhcp_table_mac_address": "Endereço MAC",
|
||||
"dhcp_v6_unavailable": "Indisponível",
|
||||
"disable": "Desativar",
|
||||
"disable_dns_rewrites": "Desativar reescritas de DNS?",
|
||||
"disable_protection_btn": "Desativar proteção",
|
||||
"disabled_dhcp": "Servidor DHCP desativado",
|
||||
"disallow_client_confirm_allowlist_note": "Esta ação também excluirá a regra %ip% da lista de clientes permitidos.",
|
||||
|
|
@ -201,68 +219,96 @@
|
|||
"disallow_this_client": "Não permitir este cliente",
|
||||
"dns_access_settings_title": "Configurações de acessos",
|
||||
"dns_allowed_clients": "Clientes permitidos",
|
||||
"dns_allowed_clients_desc": "Aceita apenas solicitações de clientes desta lista",
|
||||
"dns_allowed_clients_desc_2": "Para adicionar um cliente, insira seu <a>CIDR, endereço IP ou ID do cliente</a>",
|
||||
"dns_allowed_clients_label": "Clientes permitidos, um por linha",
|
||||
"dns_allowed_clients_placeholder": "Insira os clientes permitidos",
|
||||
"dns_blocking_mode": "Modo de bloqueio",
|
||||
"dns_blocking_mode_custom_ip": "Endereço de IP personalizado",
|
||||
"dns_blocking_mode_custom_ip_desc": "Responde com um endereço IP definido manualmente",
|
||||
"dns_blocking_mode_default": "Padrão",
|
||||
"dns_blocking_mode_default_desc": "Responde com 0.0.0.0 para registros A e com :: para registros AAAA se for bloqueado por uma regra do tipo Adblock. Responde com o endereço IP especificado na regra se for bloqueado por uma regra do tipo hosts",
|
||||
"dns_blocking_mode_desc": "Define o tipo de resposta para solicitações bloqueadas",
|
||||
"dns_blocking_mode_ipv4_label": "Bloqueando IPv4",
|
||||
"dns_blocking_mode_ipv4_placeholder": "Digite o endereço de IPv4",
|
||||
"dns_blocking_mode_ipv6_label": "Bloqueando IPv6",
|
||||
"dns_blocking_mode_ipv6_placeholder": "Digite o endereço de IPv6",
|
||||
"dns_blocking_mode_null_ip": "Endereço IP nulo",
|
||||
"dns_blocking_mode_null_ip_desc": "Responde com 0.0.0.0 para A e :: para AAAA",
|
||||
"dns_blocking_mode_nxdomain": "NXDOMAIN",
|
||||
"dns_blocking_mode_nxdomain_desc": "Responde com o código NXDOMAIN",
|
||||
"dns_blocking_mode_refused": "REFUSED",
|
||||
"dns_blocking_mode_refused_desc": "Responde com o código REFUSED",
|
||||
"dns_blocking_mode_title": "Modo de bloqueio",
|
||||
"dns_blocking_mode_ttl_label": "TTL da resposta bloqueada, em segundos",
|
||||
"dns_blocking_mode_ttl_placeholder": "Digite o TTL",
|
||||
"dns_bootstrap_dns_label": "Servidores DNS de bootstrap, um por linha",
|
||||
"dns_bootstrap_dns_placeholder": "Endereço de IP",
|
||||
"dns_bootstrap_dns_title": "Servidores DNS de inicialização",
|
||||
"dns_bootstrap_servers": "Servidores DNS de inicialização",
|
||||
"dns_cache_config": "Configuração de cache DNS",
|
||||
"dns_cache_configuration_saved_toast": "Configuração do cache DNS salva",
|
||||
"dns_cache_desc": "Armazena as respostas do DNS localmente",
|
||||
"dns_cache_size": "Tamanho do cache",
|
||||
"dns_cache_size_desc": "Define o tamanho do cache de DNS",
|
||||
"dns_cache_size_label": "Tamanho do cache do DNS, em bytes",
|
||||
"dns_cache_size_title": "Tamanho do cache",
|
||||
"dns_cache_title": "Cache DNS",
|
||||
"dns_clear_cache": "Limpar cache",
|
||||
"dns_clear_cache_cancel": "Cancelar",
|
||||
"dns_clear_cache_confirm": "Sim, limpar",
|
||||
"dns_clear_cache_title": "Limpar cache?",
|
||||
"dns_clear_cache_yes": "Sim, limpar",
|
||||
"dns_config": "Configuração do servidor DNS",
|
||||
"dns_confirm_clear_cache": "Limpar cache DNS?",
|
||||
"dns_confirm_clear_cache_desc": "Tem certeza de que deseja limpar o cache DNS?",
|
||||
"dns_disallowed_clients": "Clientes não permitidos",
|
||||
"dns_disallowed_clients_label": "Clientes não permitidos, um por linha",
|
||||
"dns_disallowed_clients_title": "Clientes não permitidos",
|
||||
"dns_disallowed_domains": "Domínios bloqueados",
|
||||
"dns_disallowed_domains_label": "Domínios não permitidos, um por linha",
|
||||
"dns_disallowed_domains_title": "Domínios bloqueados",
|
||||
"dns_dnssec": "DNSSEC",
|
||||
"dns_edns_client_subnet": "Sub-rede do cliente EDNS",
|
||||
"dns_edns_custom_label": "Endereço IP personalizado para EDNS",
|
||||
"dns_edns_custom_placeholder": "Digite o endereço de IP",
|
||||
"dns_edns_option_custom": "Usar endereço de IP personalizado",
|
||||
"dns_edns_option_default": "Ativar a sub-rede do cliente EDNS padrão",
|
||||
"dns_edns_title": "Sub-rede do cliente EDNS",
|
||||
"dns_fallback_dns_label": "Servidores DNS de fallback, um por linha",
|
||||
"dns_fallback_dns_placeholder": "Endereços IP, sdns://, quic://, https://, h3://, ou tls://",
|
||||
"dns_fallback_dns_title": "Servidores DNS Fallback",
|
||||
"dns_fallback_servers": "Servidores DNS Fallback",
|
||||
"dns_ipv6_resolution": "Resolução IPv6",
|
||||
"dns_optimistic_caching": "Cache otimista",
|
||||
"dns_over_https": "DNS-sobre-HTTPS",
|
||||
"dns_over_quic": "DNS-sobre-QUIC",
|
||||
"dns_over_tls": "DNS-sobre-TLS",
|
||||
"dns_override_max_ttl": "Sobrepor o TTL máximo",
|
||||
"dns_override_max_ttl_placeholder": "Tempo de vida (TTL)",
|
||||
"dns_override_max_ttl_title": "Sobrepor o TTL máximo",
|
||||
"dns_override_min_ttl": "Sobrepor o TTL mínimo",
|
||||
"dns_override_min_ttl_placeholder": "Tempo de vida (TTL)",
|
||||
"dns_override_min_ttl_title": "Sobrepor o TTL mínimo",
|
||||
"dns_privacy": "Privacidade de DNS",
|
||||
"dns_private_reverse_servers_label": "Servidores de DNS reverso privados, um por linha",
|
||||
"dns_private_reverse_servers_placeholder": "Endereços IP, sdns://, quic://, https://, h3://, ou tls://",
|
||||
"dns_private_reverse_servers_title": "Servidores DNS reversos privados",
|
||||
"dns_protocols_title": "Protocolos DNS",
|
||||
"dns_queries": "Consultas de DNS",
|
||||
"dns_queries_tooltip": "O número de consultas DNS processadas nas últimas 24 horas",
|
||||
"dns_query": "Consultas de DNS",
|
||||
"dns_rate_limit": "Velocidade limite",
|
||||
"dns_rate_limit_allowlist": "Lista de permissões de limitação de taxa",
|
||||
"dns_rate_limit_allowlist_label": "Endereço IP, um por linha",
|
||||
"dns_rate_limit_allowlist_placeholder": "Endereço de IP",
|
||||
"dns_rate_limit_allowlist_title": "Lista de permissões de limitação de taxa",
|
||||
"dns_rate_limit_no_limit": "Sem limite",
|
||||
"dns_rate_limit_placeholder": "Insira a velocidade limite",
|
||||
"dns_rate_limit_title": "Velocidade limite",
|
||||
"dns_rewrites": "Reescritas de DNS",
|
||||
"dns_server_addresses": "Endereços de servidor",
|
||||
"dns_server_addresses_desc": "Define os endereços dos servidores DNS pelos quais o AdGuard Home poderá ser acessado",
|
||||
"dns_server_addresses_label": "Endereços de servidor, um por linha",
|
||||
"dns_server_addresses_placeholder": "Endereços IP, sdns://, quic://, https://, h3://, ou tls://",
|
||||
"dns_server_addresses_title": "Endereços de servidor",
|
||||
|
|
@ -271,6 +317,8 @@
|
|||
"dns_settings": "DNS",
|
||||
"dns_subnet_placeholder": "Insira o comprimento do prefixo",
|
||||
"dns_test_upstreams": "Testar DNS primário",
|
||||
"dns_ttl_value": "%value% s",
|
||||
"dns_upstream_mode_desc": "Determina como encaminhar as consultas DNS da sua rede para vários servidores primários",
|
||||
"dns_upstream_servers_title": "Servidores DNS primário",
|
||||
"dns_upstream_timeout": "Tempo limite de upstream",
|
||||
"dns_upstream_timeout_title": "Tempo limite de upstream",
|
||||
|
|
@ -281,8 +329,10 @@
|
|||
"dynamic_dhcp_leases_not_found": "Concessões DHCP dinâmicas não encontradas",
|
||||
"edit_table_action": "Editar",
|
||||
"enable": "Ativar",
|
||||
"enable_dns_rewrites": "Ativar reescritas de DNS?",
|
||||
"enabled_dhcp": "Servidor DHCP ativado",
|
||||
"encrypted_dns_addresses": "Endereços de servidor DNS criptografados",
|
||||
"encrypted_dns_settings": "Configurações do servidor DNS",
|
||||
"encryption_certificate_has_issues": "O certificado apresenta problemas",
|
||||
"encryption_certificate_path": "Caminho do certificado",
|
||||
"encryption_certificates": "Certificados",
|
||||
|
|
@ -298,8 +348,10 @@
|
|||
"encryption_disable_plain_dns": "Desativar DNS simples?",
|
||||
"encryption_disable_plain_dns_desc": "Dispositivos não compatíveis com DNS criptografado podem não funcionar corretamente",
|
||||
"encryption_doq": "Porta DNS-sobre-QUIC",
|
||||
"encryption_doq_summary": "Porta DNS-sobre-QUIC: %value%",
|
||||
"encryption_doq_tooltip": "Esta porta é usada para executar um servidor DNS-sobre-QUIC",
|
||||
"encryption_dot": "Porta DNS-sobre-TLS",
|
||||
"encryption_dot_summary": "Porta DNS-sobre-TLS: %value%",
|
||||
"encryption_dot_tooltip": "Esta porta é usada para executar um servidor DNS-sobre-TLS",
|
||||
"encryption_encrypted_dns": "DNS criptografado",
|
||||
"encryption_encrypted_dns_desc": "A interface administrativa do AdGuard Home utiliza HTTPS. O servidor DNS é compatível com DNS-sobre-HTTPS, DNS-sobre-TLS e DNS-sobre-QUIC",
|
||||
|
|
@ -307,9 +359,12 @@
|
|||
"encryption_force_redirect": "Redirecionar automaticamente de HTTP para HTTPS",
|
||||
"encryption_hostnames": "Nomes de host: %value%",
|
||||
"encryption_https": "Porta HTTPS",
|
||||
"encryption_https_summary": "Porta HTTPS: %value%",
|
||||
"encryption_https_tooltip": "Esta porta fornece acesso HTTPS para a interface de administração do AdGuard Home. Ela também fornece DNS-sobre-HTTPS no local '/dns-query'",
|
||||
"encryption_invalid_data": "Dados inválidos",
|
||||
"encryption_issuer": "Emissor: %value%",
|
||||
"encryption_key": "Chave privada",
|
||||
"encryption_key_cert_mismatch": "A chave privada não corresponde ao certificado",
|
||||
"encryption_key_input": "Copie/cole aqui a chave privada codificada em PEM para seu certificado.",
|
||||
"encryption_key_invalid": "A chave privada é inválida",
|
||||
"encryption_key_source_content": "Colar o conteúdo da chave privada",
|
||||
|
|
@ -317,10 +372,12 @@
|
|||
"encryption_key_type": "Algoritmo de criptografia: %value%",
|
||||
"encryption_key_valid": "A chave privada é válida",
|
||||
"encryption_plain_dns": "DNS simples",
|
||||
"encryption_plain_dns_desc": "O DNS simples está ativado por padrão. Desativa-lo faz com que todos os dispositivos usem o DNS criptografado. Para isso, especifique pelo menos um protocolo de DNS criptografado",
|
||||
"encryption_plain_dns_error": "Para desativar o DNS simples, ative pelo menos um protocolo DNS criptografado",
|
||||
"encryption_private_key_path": "Caminho da chave privada",
|
||||
"encryption_server": "Nome do servidor",
|
||||
"encryption_server_enter": "Digite seu nome de domínio",
|
||||
"encryption_server_summary": "Nome do servidor: %value%",
|
||||
"encryption_server_tooltip_1": "Se definido, AdGuard Home detectará ClientIDs, responderá a inquéritos DDR e executará validações de conexão adicionais. Se não estiver definido, esses recursos estão desativados",
|
||||
"encryption_server_tooltip_2": "Deve corresponder a um nome DNS no certificado",
|
||||
"encryption_subject": "Assunto: %value%",
|
||||
|
|
@ -360,6 +417,8 @@
|
|||
"form_error_domain_format": "Formato de domínio inválido",
|
||||
"form_error_equal": "Não deve ser igual",
|
||||
"form_error_format": "Formato inválido",
|
||||
"form_error_format_line": "Formato inválido na linha %line%",
|
||||
"form_error_format_lines": "Formato inválido nas linhas %lines%",
|
||||
"form_error_gateway_ip": "A concessão não pode ter o endereço IP do gateway",
|
||||
"form_error_hostname_format": "Use números, letras minúsculas ou hífens",
|
||||
"form_error_ip4_format": "Endereço de IPv4 inválido",
|
||||
|
|
@ -379,6 +438,7 @@
|
|||
"friday": "Sexta-feira",
|
||||
"gateway_or_subnet_invalid": "Máscara de sub-rede inválida",
|
||||
"general_statistics": "Estatísticas gerais",
|
||||
"greater_range_start_error": "O final do intervalo deve ser maior que o início",
|
||||
"home_dns_addresses": "Endereços DNS do AdGuard Home",
|
||||
"home_dns_addresses_desc": "O servidor DNS do AdGuard Home escuta nas seguintes interfaces/portas:",
|
||||
"ignore_domains_desc_log": "As solicitações que correspondem a essas regras não são exibidas no registro de consultas",
|
||||
|
|
@ -477,8 +537,9 @@
|
|||
"processing_time_ms": "%value% ms",
|
||||
"protection": "Proteção",
|
||||
"protocol": "Protocolo",
|
||||
"protocols": "Protocolos",
|
||||
"queries": "Consultas",
|
||||
"queries_total": "| %count% consulta no total | %count% consultas no total",
|
||||
"queries_total": "| %value% consulta no total | %value% consultas no total",
|
||||
"query_details": "Detalhes da consulta",
|
||||
"query_log": "Registro de consultas",
|
||||
"query_log_all_reasons": "Todos os motivos",
|
||||
|
|
@ -530,6 +591,10 @@
|
|||
"request_table_header": "Solicitação",
|
||||
"requests_table_header": "Solicitações",
|
||||
"reset": "Redefinir",
|
||||
"reset_dhcp_settings": "Redefinir configurações de DHCP",
|
||||
"reset_dns_confirm_text": "Todas as configurações de protocolos serão redefinidas para seus valores padrão",
|
||||
"reset_dns_confirm_title": "Redefinir protocolos DNS?",
|
||||
"reset_dns_protocols": "Redefinir protocolos DNS",
|
||||
"reset_settings": "Redefinir configurações",
|
||||
"reset_settings_confirm": "Sim, redefinir",
|
||||
"response_details": "Detalhes da resposta",
|
||||
|
|
@ -602,6 +667,7 @@
|
|||
"server_config_subnet_len_ipv6_faq": "Comprimento do prefixo de sub-rede para endereços IPv6 usados para limitação de velocidade. O padrão é 56",
|
||||
"server_config_subnet_len_placeholder": "Insira o comprimento do prefixo",
|
||||
"set_static_ip": "Definir um endereço de IP estático",
|
||||
"set_static_ip_manually": "Definir endereço de IP estático",
|
||||
"settings": "Configurações",
|
||||
"settings_anonymize_client_ip": "Anonimizar endereços IP de clientes",
|
||||
"settings_anonymize_client_ip_desc": "Substituir parte do endereço IP por zeros",
|
||||
|
|
@ -611,7 +677,7 @@
|
|||
"settings_confirm_clear_query_log_desc": "Isso excluirá todas as solicitações do <strong>log de consultas</strong>. As estatísticas ainda estarão disponíveis",
|
||||
"settings_confirm_clear_statistics": "Limpar estatísticas?",
|
||||
"settings_confirm_clear_statistics_desc": "Isso limpará todas as estatísticas. <strong>O registro de consultas</strong> ainda estará disponível",
|
||||
"settings_confirm_decrease_log_rotation_interval": "Diminuir o intervalo de rotação de logs?",
|
||||
"settings_confirm_decrease_log_rotation_interval": "Diminuir o intervalo de rotação de registros?",
|
||||
"settings_confirm_decrease_log_rotation_interval_desc": "Isso excluirá todos os registros mais antigos que %value%",
|
||||
"settings_confirm_decrease_stats_rotation_interval": "Diminuir o intervalo de retenção de estatísticas?",
|
||||
"settings_confirm_decrease_stats_rotation_interval_desc": "Isso excluirá todas as estatísticas mais antigas que %value%",
|
||||
|
|
@ -725,6 +791,7 @@
|
|||
"stats_adult": "Bloqueado sites adultos",
|
||||
"stats_query_domain": "Principais domínios consultados",
|
||||
"status_table_header": "Status",
|
||||
"subnet_error": "Os endereços do intervalo devem estar na mesma sub-rede",
|
||||
"sunday": "Domingo",
|
||||
"system_host_files": "Arquivos hosts do sistema",
|
||||
"system_theme": "Tema do sistema",
|
||||
|
|
@ -735,6 +802,9 @@
|
|||
"threats_blocked_tooltip": "O número de consultas DNS bloqueadas pelo módulo de segurança de navegação do AdGuard",
|
||||
"thursday": "Quinta-feira",
|
||||
"time_table_header": "Data",
|
||||
"tls_cert_path_label": "Caminho completo para o arquivo do certificado",
|
||||
"tls_cert_path_option": "Insira o caminho completo para o arquivo do certificado",
|
||||
"tls_certificate": "Certificado TLS",
|
||||
"top_blocked_domains": "Principais domínios bloqueados",
|
||||
"top_clients": "Principais clientes",
|
||||
"top_upstreams": "Melhores servidores DNS primários",
|
||||
|
|
@ -755,6 +825,7 @@
|
|||
"update_filters_interval_hourly": "A cada hora",
|
||||
"update_filters_interval_weekly": "Toda semana",
|
||||
"update_filters_title": "Atualiza os filtros automaticamente",
|
||||
"update_how_to": "Como atualizar",
|
||||
"updated_upstream_dns_toast": "Servidores upstream salvos",
|
||||
"updates_checked": "Uma nova versão do AdGuard Home está disponível\n",
|
||||
"updates_version_equal": "O AdGuard Home está atualizado.",
|
||||
|
|
@ -766,7 +837,12 @@
|
|||
"upstream_dns_addresses_faq": "<a>Saiba mais</a> sobre a configuração de servidores DNS upstream. Aqui está uma <b>lista de provedores de DNS conhecidos</b> para escolher.",
|
||||
"upstream_dns_configured_in_file": "Configurado em %path%",
|
||||
"upstream_dns_fastest_addr": "Endereço de IP mais rápido",
|
||||
"upstream_dns_fastest_addr_desc": "Aguarde as respostas de todos os servidores DNS, meça a velocidade de conexão TCP de cada servidor e retorne o endereço IP do servidor com a maior velocidade de conexão",
|
||||
"upstream_dns_fastest_addr_warning": "Este modo pode desacelerar significativamente as consultas DNS se um ou mais servidores upstream não estiverem respondendo. Certifique-se de que seus servidores upstream sejam estáveis e que o tempo limite upstream seja baixo",
|
||||
"upstream_dns_load_balancing": "Balanceamento de carga",
|
||||
"upstream_dns_load_balancing_desc": "<p>Consulte um servidor upstream por vez.</p><p>>O AdGuard Home usa um algoritmo aleatório ponderado para selecionar servidores com o menor número de falhas e o menor tempo médio de consulta.</p>",
|
||||
"upstream_dns_parallel_requests": "Solicitações paralelas",
|
||||
"upstream_dns_parallel_requests_desc": "Use consultas paralelas para acelerar a resolução consultando todos os servidores upstream simultaneamente",
|
||||
"upstream_dns_placeholder": "Endereços IP, sdns://, quic://, https://, h3://, ou tls://",
|
||||
"upstream_dns_servers_title": "Servidores DNS primário",
|
||||
"upstream_enable_reverse_lookup_desc": "Resolver reversamente os endereços IP dos clientes para seus nomes de host enviando consultas PTR aos resolvedores apropriados: servidores DNS privados para clientes locais ou servidores upstream para clientes com endereços IP públicos",
|
||||
|
|
@ -853,12 +929,15 @@
|
|||
"user_rules_source_allowlist": "<strong>Lista de permissões:</strong> %value%",
|
||||
"user_rules_source_blocklist": "<strong>Listas de bloqueio:</strong> %value%",
|
||||
"user_rules_status": "<strong>Status:</strong> %reason%",
|
||||
"user_rules_title": "Regras personalizadas",
|
||||
"username_label": "Nome de usuário",
|
||||
"username_placeholder": "Digite o nome de usuário",
|
||||
"validated_with_dnssec": "Validado com DNSSEC",
|
||||
"version_number": "Versão %value%",
|
||||
"wednesday": "Quarta-feira",
|
||||
"whois": "WHOIS",
|
||||
"yes": "Sim",
|
||||
"yes_disable": "Sim, desativar",
|
||||
"yes_remove": "Sim, remover",
|
||||
"yes_reset": "Sim, redefinir"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"add_allowlist": "Adicionar lista de permissões",
|
||||
"add_blocklist": "Adicionar lista de bloqueio",
|
||||
"add_persistent_client": "Adicionar como cliente persistente",
|
||||
"add_tls_certificate": "Adicionar certificado TLS",
|
||||
"add_to_allowlist": "Adicionar na lista de permissões",
|
||||
"ads_blocked": "Anúncios bloqueados",
|
||||
"ads_blocked_card": "Anúncios bloqueados",
|
||||
|
|
@ -38,6 +39,10 @@
|
|||
"allowlists_title": "Lista de permissões",
|
||||
"apply": "Aplicar",
|
||||
"aria_clear_input": "Limpar entrada",
|
||||
"aria_next_page": "Próxima página",
|
||||
"aria_previous_page": "Página anterior",
|
||||
"auto_clients_desc": "Informações sobre os endereços IP dos dispositivos que podem usar o AdGuard Home",
|
||||
"auto_clients_title": "Tempo de execução",
|
||||
"autofix_warning_list": "<p>Irá executar as seguintes tarefas:</p><p>Desativar o DNSStubListener do sistema</p><p>Definir o endereço do servidor DNS como 127.0.0.1</p><p>Substituir o destino do link simbólico de /etc/resolv.conf por /run/systemd/resolve/resolv.conf</p><p>Parar o DNSStubListener (recarregar o serviço systemd-resolved)</p>",
|
||||
"autofix_warning_result": "Como resultado, todos as solicitações DNS do seu sistema serão processadas pelo AdGuard Home por predefinição.",
|
||||
"autofix_warning_text": "Se clicar em \"Corrigir\", o AdGuardHome irá configurar o seu sistema para utilizar o servidor DNS do AdGuardHome.",
|
||||
|
|
@ -139,6 +144,8 @@
|
|||
"clients_remove_title": "Remover cliente?",
|
||||
"clients_tags": "Etiquetas",
|
||||
"clients_tags_desc": "Você pode adicionar etiquetas para este cliente e incluí-las nas regras de filtragem. <a>Saiba mais</a>",
|
||||
"clients_tags_placeholder": "Adicionar etiquetas",
|
||||
"clients_title": "Persistente",
|
||||
"clients_upstreams_desc": "Servidores DNS upstream, um por linha",
|
||||
"clients_use_dns_cache": "Utilizar cache de DNS",
|
||||
"clients_use_global_settings": "Usar definições globais",
|
||||
|
|
@ -155,6 +162,7 @@
|
|||
"copy": "Copiar",
|
||||
"copy_to_clipboard": "Copiar para a área de transferência",
|
||||
"custom_filtering_rules": "Regras de filtragem personalizadas",
|
||||
"custom_rules": "Regras personalizadas",
|
||||
"dark_theme": "Tema escuro",
|
||||
"dashboard": "Painel",
|
||||
"delete_btn": "Apagar",
|
||||
|
|
@ -163,9 +171,11 @@
|
|||
"delete_table_action": "Apagar",
|
||||
"delete_table_action_confirm": "Sim, apagar",
|
||||
"device_type": "Tipo de dispositivo",
|
||||
"dhcp": "DHCP",
|
||||
"dhcp_add_static_lease": "Adicionar nova concessão estática",
|
||||
"dhcp_config_saved": "Definições DHCP guardadas com sucesso",
|
||||
"dhcp_edit_static_lease": "Editar concessão estática",
|
||||
"dhcp_enable": "Utilizar o servidor DHCP integrado do AdGuard",
|
||||
"dhcp_error": "Falha ao verificar o servidor DHCP. Aguarde um pouco e tente novamente",
|
||||
"dhcp_form_gateway_address": "Endereço IP do gateway",
|
||||
"dhcp_form_gateway_input": "IP do gateway",
|
||||
|
|
@ -173,11 +183,13 @@
|
|||
"dhcp_form_range_title": "Intervalo de endereços IP",
|
||||
"dhcp_form_subnet_input": "Máscara de sub-rede",
|
||||
"dhcp_found": "O servidor DHCP não pode ser ativado",
|
||||
"dhcp_interface_select": "Interface DHCP",
|
||||
"dhcp_ipv4_settings": "Definições DHCP IPv4",
|
||||
"dhcp_ipv6_settings": "Definições DHCP IPv6",
|
||||
"dhcp_lease_added": "Concessão estática \"%key%\" adicionada com sucesso",
|
||||
"dhcp_lease_deleted": "Concessão estática \"%key%\" apagada com sucesso",
|
||||
"dhcp_lease_updated": "Concessão estática \"%key%\" atualizada com sucesso",
|
||||
"dhcp_leases": "Concessões dinâmicas",
|
||||
"dhcp_leases_not_found": "Nenhuma concessão DHCP encontrada",
|
||||
"dhcp_leases_title": "Concessões DHCP",
|
||||
"dhcp_mac_address_already_added": "Este endereço MAC já foi adicionado",
|
||||
|
|
@ -189,11 +201,14 @@
|
|||
"dhcp_reset_leases_success": "Concessões de DHCP repostas com sucesso",
|
||||
"dhcp_settings": "Definições de DHCP",
|
||||
"dhcp_static_ip_error": "O servidor DHCP requer um endereço IP estático. Não foi possível confirmar que esta interface de rede o possua",
|
||||
"dhcp_static_leases": "Concessões estáticas",
|
||||
"dhcp_static_leases_not_found": "Nenhuma concessão DHCP estática foi encontrada",
|
||||
"dhcp_table_hostname": "Nome do servidor",
|
||||
"dhcp_table_ip_address": "Endereço de IP",
|
||||
"dhcp_table_mac_address": "Endereço MAC",
|
||||
"dhcp_v6_unavailable": "Indisponível",
|
||||
"disable": "Desativar",
|
||||
"disable_dns_rewrites": "Desativar reescritas de DNS?",
|
||||
"disable_protection_btn": "Desativar proteção",
|
||||
"disabled_dhcp": "Servidor DHCP desativado",
|
||||
"disallow_client_confirm_allowlist_note": "Esta ação irá também excluir a regra %ip% da lista de clientes autorizados.",
|
||||
|
|
@ -212,9 +227,12 @@
|
|||
"dns_blocking_mode_ipv6_placeholder": "Digite o endereço de IPv6",
|
||||
"dns_blocking_mode_null_ip": "Endereço IP nulo",
|
||||
"dns_blocking_mode_nxdomain": "NXDOMAIN",
|
||||
"dns_blocking_mode_nxdomain_desc": "Responde com o código NXDOMAIN",
|
||||
"dns_blocking_mode_refused": "REFUSED",
|
||||
"dns_blocking_mode_refused_desc": "Responde com o código REFUSED",
|
||||
"dns_blocking_mode_title": "Modo de bloqueio",
|
||||
"dns_blocking_mode_ttl_label": "TTL da resposta bloqueada, em segundos",
|
||||
"dns_blocking_mode_ttl_placeholder": "Insira TTL",
|
||||
"dns_bootstrap_dns_label": "Servidores DNS de arranque, um por linha",
|
||||
"dns_bootstrap_dns_placeholder": "Endereços de IP",
|
||||
"dns_bootstrap_dns_title": "Servidores DNS de arranque",
|
||||
|
|
@ -227,42 +245,57 @@
|
|||
"dns_clear_cache": "Limpar cache",
|
||||
"dns_clear_cache_cancel": "Cancelar",
|
||||
"dns_clear_cache_confirm": "Sim, limpar",
|
||||
"dns_clear_cache_title": "Limpar cache?",
|
||||
"dns_clear_cache_yes": "Sim, limpar",
|
||||
"dns_config": "Definição do servidor DNS",
|
||||
"dns_confirm_clear_cache": "Limpar cache DNS?",
|
||||
"dns_disallowed_clients": "Clientes não permitidos",
|
||||
"dns_disallowed_clients_label": "Clientes não permitidos, um por linha",
|
||||
"dns_disallowed_clients_title": "Clientes não permitidos",
|
||||
"dns_disallowed_domains": "Domínios bloqueados",
|
||||
"dns_disallowed_domains_label": "Domínios não permitidos, um por linha",
|
||||
"dns_disallowed_domains_title": "Domínios bloqueados",
|
||||
"dns_dnssec": "DNSSEC",
|
||||
"dns_edns_client_subnet": "Sub-rede do cliente EDNS",
|
||||
"dns_edns_custom_label": "Endereço IP personalizado para o EDNS",
|
||||
"dns_edns_custom_placeholder": "Digite o endereço de IP",
|
||||
"dns_edns_option_custom": "Usar endereço IP personalizado",
|
||||
"dns_edns_option_default": "Ativar a sub-rede do cliente EDNS padrão",
|
||||
"dns_edns_title": "Sub-rede do cliente EDNS",
|
||||
"dns_fallback_dns_label": "Servidores DNS de fallback, um por linha",
|
||||
"dns_fallback_dns_placeholder": "Endereços IP, sdns://, quic://, https://, h3:// ou tls://",
|
||||
"dns_fallback_dns_title": "Servidores DNS de fallback",
|
||||
"dns_fallback_servers": "Servidores DNS de fallback",
|
||||
"dns_ipv6_resolution": "Resolução IPv6",
|
||||
"dns_optimistic_caching": "Cache otimista",
|
||||
"dns_over_https": "DNS-sobre-HTTPS",
|
||||
"dns_over_quic": "DNS-sobre-QUIC",
|
||||
"dns_over_tls": "DNS-sobre-TLS",
|
||||
"dns_override_max_ttl": "Sobrepor o TTL máximo",
|
||||
"dns_override_max_ttl_placeholder": "Tempo de vida (TTL)",
|
||||
"dns_override_max_ttl_title": "Sobrepor o TTL máximo",
|
||||
"dns_override_min_ttl": "Sobrepor o TTL mínimo",
|
||||
"dns_override_min_ttl_placeholder": "Tempo de vida (TTL)",
|
||||
"dns_override_min_ttl_title": "Sobrepor o TTL mínimo",
|
||||
"dns_privacy": "Privacidade de DNS",
|
||||
"dns_private_reverse_servers_label": "Servidores DNS reversos privados, um por linha",
|
||||
"dns_private_reverse_servers_placeholder": "Endereços IP, sdns://, quic://, https://, h3:// ou tls://",
|
||||
"dns_private_reverse_servers_title": "Servidores DNS reversos privados",
|
||||
"dns_protocols_title": "Protocolos DNS",
|
||||
"dns_queries": "Consultas de DNS",
|
||||
"dns_queries_tooltip": "O número de consultas DNS processadas nas últimas 24 horas",
|
||||
"dns_query": "Consultas de DNS",
|
||||
"dns_rate_limit": "Limite de velocidade",
|
||||
"dns_rate_limit_allowlist": "Lista de permissões de limitação de taxa",
|
||||
"dns_rate_limit_allowlist_label": "Endereço IP, um por linha",
|
||||
"dns_rate_limit_allowlist_placeholder": "Endereços de IP",
|
||||
"dns_rate_limit_allowlist_title": "Lista de permissões de limitação de taxa",
|
||||
"dns_rate_limit_no_limit": "Sem limite",
|
||||
"dns_rate_limit_placeholder": "Insira o limite de velocidade",
|
||||
"dns_rate_limit_title": "Limite de velocidade",
|
||||
"dns_rewrites": "Reescritas de DNS",
|
||||
"dns_server_addresses": "Endereços de servidor",
|
||||
"dns_server_addresses_desc": "Define os endereços dos servidores DNS pelos quais o AdGuard Home poderá ser acessado",
|
||||
"dns_server_addresses_label": "Endereços de servidor, um por linha",
|
||||
"dns_server_addresses_placeholder": "Endereços IP, sdns://, quic://, https://, h3:// ou tls://",
|
||||
"dns_server_addresses_title": "Endereços de servidor",
|
||||
|
|
@ -271,18 +304,24 @@
|
|||
"dns_settings": "DNS",
|
||||
"dns_subnet_placeholder": "Introduza o comprimento do prefixo",
|
||||
"dns_test_upstreams": "Testar DNS primário",
|
||||
"dns_ttl_value": "%value% s",
|
||||
"dns_upstream_mode_desc": "Determina como encaminhar as consultas DNS da sua rede para vários servidores primários",
|
||||
"dns_upstream_servers_title": "Servidores DNS primário",
|
||||
"dns_upstream_timeout": "Tempo esgotado de upstream",
|
||||
"dns_upstream_timeout_placeholder": "Insira o tempo limite em segundos",
|
||||
"dns_upstream_timeout_title": "Tempo esgotado de upstream",
|
||||
"dns_upstream_validation_invalid": "Formato inválido",
|
||||
"dns_upstream_validation_invalid_lines": "Formato de upstream nas linhas {lines}",
|
||||
"domain": "Domínio",
|
||||
"domain_or_client": "Domínio ou cliente",
|
||||
"download_mobileconfig": "Transferir ficheiro de configuração",
|
||||
"dynamic_dhcp_leases_not_found": "Concessões DHCP dinâmicas não encontradas",
|
||||
"edit_table_action": "Editar",
|
||||
"enable": "Ativar",
|
||||
"enable_dns_rewrites": "Ativar reescritas de DNS?",
|
||||
"enabled_dhcp": "Servidor DHCP ativado",
|
||||
"encrypted_dns_addresses": "Endereços de servidor DNS criptografados",
|
||||
"encrypted_dns_settings": "Configurações do servidor DNS",
|
||||
"encryption_certificate_has_issues": "O certificado apresenta problemas",
|
||||
"encryption_certificate_path": "Caminho do certificado",
|
||||
"encryption_certificates": "Certificados",
|
||||
|
|
@ -308,8 +347,10 @@
|
|||
"encryption_hostnames": "Nomes de anfitrião: %value%",
|
||||
"encryption_https": "Porta HTTPS",
|
||||
"encryption_https_tooltip": "Esta porta fornece acesso HTTPS para a interface de administração do AdGuard Home. Ela também fornece DNS-sobre-HTTPS no local '/dns-query'",
|
||||
"encryption_invalid_data": "Dados inválidos",
|
||||
"encryption_issuer": "Emissor: %value%",
|
||||
"encryption_key": "Chave privada",
|
||||
"encryption_key_cert_mismatch": "A chave privada não corresponde ao certificado",
|
||||
"encryption_key_input": "Copie/cole aqui a chave privada codificada em PEM para o seu certificado.",
|
||||
"encryption_key_invalid": "A chave privada não é válida",
|
||||
"encryption_key_source_content": "Colar o conteúdo da chave privada",
|
||||
|
|
@ -317,10 +358,12 @@
|
|||
"encryption_key_type": "Algoritmo de encriptação: %value%",
|
||||
"encryption_key_valid": "A chave privada é válida",
|
||||
"encryption_plain_dns": "DNS simples",
|
||||
"encryption_plain_dns_desc": "O DNS simples está ativado por predefinição. Desativá-lo obriga todos os dispositivos a utilizarem o DNS encriptado. Para tal, especifique pelo menos um protocolo DNS encriptado",
|
||||
"encryption_plain_dns_error": "Para desabilitar o DNS simples, habilite pelo menos um protocolo DNS criptografado",
|
||||
"encryption_private_key_path": "Caminho da chave privada",
|
||||
"encryption_server": "Nome do servidor",
|
||||
"encryption_server_enter": "Insira o seu nome de domínio",
|
||||
"encryption_server_summary": "Nome do servidor: %value%",
|
||||
"encryption_server_tooltip_1": "Se definido, AdGuard Home detetará ClientIDs, responderá a inquéritos DDR e executará validações de conexão adicionais. Se não estiver definido, esses recursos estão desativados",
|
||||
"encryption_server_tooltip_2": "Deve corresponder a um nome DNS no certificado",
|
||||
"encryption_subject": "Assunto: %value%",
|
||||
|
|
@ -360,6 +403,8 @@
|
|||
"form_error_domain_format": "Formato de domínio inválido",
|
||||
"form_error_equal": "Não deve ser igual",
|
||||
"form_error_format": "Formato inválido",
|
||||
"form_error_format_line": "Formato de upstream na linha %line%",
|
||||
"form_error_format_lines": "Formato de upstream nas linhas %lines%",
|
||||
"form_error_gateway_ip": "A concessão não pode ter o endereço IP do gateway",
|
||||
"form_error_hostname_format": "Utilize números, letras minúsculas do alfabeto ou hífens",
|
||||
"form_error_ip4_format": "Endereço de IPv4 inválido",
|
||||
|
|
@ -457,6 +502,7 @@
|
|||
"parental_group_software": "Desenvolvimento de software",
|
||||
"parental_group_streaming": "Streaming",
|
||||
"password_label": "Palavra-passe",
|
||||
"password_login_error": "O seu nome de utilizador e a sua palavra-passe não coincidem",
|
||||
"password_placeholder": "Insira palavra-passe",
|
||||
"password_requirements": "Requisitos de palavra-passe",
|
||||
"password_requirements_characters": "Mínimo de 8 caracteres",
|
||||
|
|
@ -477,8 +523,9 @@
|
|||
"processing_time_ms": "%value% ms",
|
||||
"protection": "Proteção",
|
||||
"protocol": "Protocolo",
|
||||
"protocols": "Protocolos",
|
||||
"queries": "Consultas",
|
||||
"queries_total": "| %count% consulta total | %count% consultas totais",
|
||||
"queries_total": "| %value% consulta total | %value% consultas totais",
|
||||
"query_details": "Detalhes da consulta",
|
||||
"query_log": "Registo de consultas",
|
||||
"query_log_all_reasons": "Todos os motivos",
|
||||
|
|
@ -530,6 +577,9 @@
|
|||
"request_table_header": "Solicitação",
|
||||
"requests_table_header": "Solicitações",
|
||||
"reset": "Repor",
|
||||
"reset_dhcp_settings": "Repor definições de DHCP",
|
||||
"reset_dns_confirm_title": "Redefinir protocolos DNS?",
|
||||
"reset_dns_protocols": "Redefinir protocolos DNS",
|
||||
"reset_settings": "Repor definições",
|
||||
"reset_settings_confirm": "Sim, repor",
|
||||
"response_details": "Detalhes da resposta",
|
||||
|
|
@ -602,6 +652,7 @@
|
|||
"server_config_subnet_len_ipv6_faq": "Comprimento do prefixo de sub-rede para endereços IPv6 usados para limitação de velocidade. O padrão é 56",
|
||||
"server_config_subnet_len_placeholder": "Introduza o comprimento do prefixo",
|
||||
"set_static_ip": "Definir um endereço de IP estático",
|
||||
"set_static_ip_manually": "Definir endereço de IP estático",
|
||||
"settings": "Definições",
|
||||
"settings_anonymize_client_ip": "Anonimizar os endereços IP dos clientes",
|
||||
"settings_anonymize_client_ip_desc": "Substituir parte do endereço IP por zeros",
|
||||
|
|
@ -611,7 +662,7 @@
|
|||
"settings_confirm_clear_query_log_desc": "Isto irá eliminar todos os pedidos do <strong>registo de consultas</strong>. As estatísticas continuarão disponíveis",
|
||||
"settings_confirm_clear_statistics": "Limpar estatísticas?",
|
||||
"settings_confirm_clear_statistics_desc": "Isto irá apagar todas as estatísticas. <strong>O registo de consultas</strong> continuará disponível",
|
||||
"settings_confirm_decrease_log_rotation_interval": "Reduzir o intervalo de rotação das toras?",
|
||||
"settings_confirm_decrease_log_rotation_interval": "Reduzir o intervalo de rotação dos registos?",
|
||||
"settings_confirm_decrease_log_rotation_interval_desc": "Isto irá eliminar todos os registos com data anterior a %value%",
|
||||
"settings_confirm_decrease_stats_rotation_interval": "Reduzir o intervalo de retenção das estatísticas?",
|
||||
"settings_confirm_decrease_stats_rotation_interval_desc": "Isto irá eliminar todas as estatísticas anteriores a %value%",
|
||||
|
|
@ -735,6 +786,9 @@
|
|||
"threats_blocked_tooltip": "O número de consultas DNS bloqueadas pelo módulo de segurança de navegação do AdGuard",
|
||||
"thursday": "Quinta-feira",
|
||||
"time_table_header": "Data",
|
||||
"tls_cert_path_label": "Caminho completo para o ficheiro do certificado",
|
||||
"tls_cert_path_option": "Insira o caminho completo para o ficheiro do certificado",
|
||||
"tls_certificate": "Certificado TLS",
|
||||
"top_blocked_domains": "Principais domínios bloqueados",
|
||||
"top_clients": "Principais clientes",
|
||||
"top_upstreams": "Melhores servidores DNS primários",
|
||||
|
|
@ -748,6 +802,7 @@
|
|||
"unblock_client": "Desbloquear cliente",
|
||||
"unknown_filter": "Filtro desconhecido %filterId%",
|
||||
"update_button": "Atualizar",
|
||||
"update_failed": "A atualização automática falhou. Por favor, <a>siga estes passos</a> para atualizar manualmente",
|
||||
"update_filters_custom_hours": "Intervalo personalizado, em horas",
|
||||
"update_filters_disable": "Desativar",
|
||||
"update_filters_interval_custom": "Personalizado",
|
||||
|
|
@ -755,6 +810,7 @@
|
|||
"update_filters_interval_hourly": "Cada %s hora",
|
||||
"update_filters_interval_weekly": "Toda semana",
|
||||
"update_filters_title": "Atualizar automaticamente os filtros",
|
||||
"update_how_to": "Como atualizar",
|
||||
"updated_upstream_dns_toast": "Servidores upstream guardados",
|
||||
"updates_checked": "Uma nova versão do AdGuard Home está disponível\n",
|
||||
"updates_version_equal": "O AdGuard Home está atualizado",
|
||||
|
|
@ -766,7 +822,12 @@
|
|||
"upstream_dns_addresses_faq": "<a>Saiba mais</a>sobre como configurar servidores DNS a montante. Aqui está uma<b>lista de fornecedores de DNS conhecidos</b>à sua escolha.",
|
||||
"upstream_dns_configured_in_file": "Configurado em %path%",
|
||||
"upstream_dns_fastest_addr": "Endereço de IP mais rápido",
|
||||
"upstream_dns_fastest_addr_desc": "Aguardar as respostas de todos os servidores DNS, medir a velocidade da ligação TCP de cada servidor e devolver o endereço IP do servidor com a velocidade de ligação mais rápida",
|
||||
"upstream_dns_fastest_addr_warning": "Este modo pode tornar as consultas DNS significativamente mais lentas, caso um ou mais servidores upstream não estejam a responder. Certifique-se de que os seus servidores a upstream estão estáveis e de que o tempo de espera para os servidores upstream é baixo",
|
||||
"upstream_dns_load_balancing": "Balanceamento de carga",
|
||||
"upstream_dns_load_balancing_desc": "<p>Consulta um servidor upstream de cada vez.</p><p>O AdGuard Home usa um algoritmo aleatório ponderado para selecionar servidores com o menor número de pesquisas falhadas e o menor tempo médio de pesquisa.</p>",
|
||||
"upstream_dns_parallel_requests": "Solicitações paralelas",
|
||||
"upstream_dns_parallel_requests_desc": "Utilize consultas paralelas para acelerar a resolução, consultando todos os servidores a montante simultaneamente",
|
||||
"upstream_dns_placeholder": "Endereços IP, sdns://, quic://, https://, h3:// ou tls://",
|
||||
"upstream_dns_servers_title": "Servidores DNS primário",
|
||||
"upstream_enable_reverse_lookup_desc": "Efetuar a resolução inversa dos endereços IP dos clientes para os respetivos nomes de anfitrião, enviando consultas PTR aos resolvedores adequados: servidores DNS privados para clientes locais ou servidores a montante para clientes com endereços IP públicos",
|
||||
|
|
@ -853,12 +914,15 @@
|
|||
"user_rules_source_allowlist": "<strong>Lista de permissões:</strong> %value%",
|
||||
"user_rules_source_blocklist": "<strong>Listas de bloqueio:</strong> %value%",
|
||||
"user_rules_status": "<strong>Estado:</strong> %reason%",
|
||||
"user_rules_title": "Regras personalizadas",
|
||||
"username_label": "Nome do utilizador",
|
||||
"username_placeholder": "Insira o nome de utilizador",
|
||||
"validated_with_dnssec": "Validado com DNSSEC",
|
||||
"version_number": "Versão %value%",
|
||||
"wednesday": "Quarta-feira",
|
||||
"whois": "WHOIS",
|
||||
"yes": "Sim",
|
||||
"yes_disable": "Sim, desativar",
|
||||
"yes_remove": "Sim, remover",
|
||||
"yes_reset": "Sim, repor"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@
|
|||
"blocked_services": "Заблокированные сервисы",
|
||||
"blocked_threats": "Заблокировано угроз",
|
||||
"blocked_threats_chart": "Заблокировано угроз",
|
||||
"blocklist_add_from_list": "Добавить из списка",
|
||||
"blocklist_add_manual": "Добавить",
|
||||
"blocklist_edit": "Редактировать чёрный список",
|
||||
"blocklist_homepage": "Сайт",
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@
|
|||
"protection": "Ochrana",
|
||||
"protocol": "Protokol",
|
||||
"queries": "Dopyty",
|
||||
"queries_total": "| %count% celkový počet dopytov | %count% celkový počet dopytov",
|
||||
"queries_total": "| %value% celkový počet dopytov | %value% celkový počet dopytov",
|
||||
"query_details": "Podrobnosti dopytu",
|
||||
"query_log": "Denník dopytov",
|
||||
"query_log_all_reasons": "Všetky dôvody",
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@
|
|||
"allowlist_add": "İzin listesi ekle",
|
||||
"allowlist_edit": "İzin listesini düzenle",
|
||||
"apply": "Uygula",
|
||||
"aria_next_page": "Sonraki sayfa",
|
||||
"aria_previous_page": "Önceki sayfa",
|
||||
"autofix_warning_result": "Sonuç olarak, sisteminizden gelen tüm DNS istekleri varsayılan olarak AdGuard Home tarafından işlenecektir.",
|
||||
"autofix_warning_text": "\"Düzelt\" öğesine tıklarsanız, AdGuard Home, sisteminizi AdGuard Home DNS sunucusunu kullanacak şekilde yapılandıracaktır.",
|
||||
"average_time_processing": "Ortalama işlem süresi",
|
||||
|
|
@ -66,6 +68,7 @@
|
|||
"clients_not_found": "İstemci bulunamadı",
|
||||
"clients_protection": "Koruma",
|
||||
"clients_tags": "Etiketler",
|
||||
"clients_tags_placeholder": "Etiket ekle",
|
||||
"clients_use_global_settings": "Genel ayarları kullan",
|
||||
"close": "Kapat",
|
||||
"confirm": "Doğrula",
|
||||
|
|
@ -73,15 +76,20 @@
|
|||
"copy": "Kopyala",
|
||||
"copy_to_clipboard": "Panoya kopyala",
|
||||
"custom_filtering_rules": "Özel filtreleme kuralları",
|
||||
"custom_rules": "Özel kurallar",
|
||||
"dark_theme": "Koyu tema",
|
||||
"dashboard": "Pano",
|
||||
"delete_btn": "Sil",
|
||||
"delete_table_action": "Sil",
|
||||
"dhcp": "DHCP",
|
||||
"dhcp_add_static_lease": "Sabit atama ekle",
|
||||
"dhcp_config_saved": "DHCP yapılandırması başarıyla kaydedildi",
|
||||
"dhcp_edit_static_lease": "Sabit atamayı düzenle",
|
||||
"dhcp_form_gateway_input": "Ağ geçidi IP",
|
||||
"dhcp_form_subnet_input": "Alt ağ maskesi",
|
||||
"dhcp_hardware_address_value": "<strong>Donanım adresi:</strong> %value%",
|
||||
"dhcp_interface_select": "DHCP arayüzü",
|
||||
"dhcp_ip_addresses_value": "<strong>IP adresleri:</strong> %value%",
|
||||
"dhcp_leases_not_found": "DHCP ataması bulunamadı",
|
||||
"dhcp_leases_title": "DHCP atamaları",
|
||||
"dhcp_new_static_lease": "Yeni sabit atama",
|
||||
|
|
@ -118,11 +126,15 @@
|
|||
"dns_clear_cache_confirm": "Evet, temizle",
|
||||
"dns_clear_cache_yes": "Evet, temizle",
|
||||
"dns_config": "DNS sunucu yapılandırması",
|
||||
"dns_confirm_clear_cache_desc": "DNS önbelleğini temizlemek istediğinizden emin misiniz?",
|
||||
"dns_disallowed_clients": "İzin verilmeyen istemciler",
|
||||
"dns_disallowed_clients_title": "İzin verilmeyen istemciler",
|
||||
"dns_disallowed_domains": "İzin verilmeyen alan adları",
|
||||
"dns_disallowed_domains_title": "İzin verilmeyen alan adları",
|
||||
"dns_dnssec": "DNSSEC",
|
||||
"dns_edns_custom_label": "EDNS için özel IP adresi",
|
||||
"dns_edns_custom_placeholder": "IP adresi girin",
|
||||
"dns_edns_option_custom": "Özel IP adresi kullan",
|
||||
"dns_fallback_dns_title": "Yedek DNS sunucuları",
|
||||
"dns_fallback_servers": "Yedek DNS sunucuları",
|
||||
"dns_optimistic_caching": "Kesintisiz önbellek",
|
||||
|
|
@ -135,6 +147,7 @@
|
|||
"dns_override_min_ttl_title": "Kullanım değeri alt sınırını belirle",
|
||||
"dns_privacy": "DNS Gizliliği",
|
||||
"dns_private_reverse_servers_title": "Özel ters DNS sunucuları",
|
||||
"dns_protocols_title": "DNS protokolleri",
|
||||
"dns_queries": "DNS sorguları",
|
||||
"dns_query": "DNS sorguları",
|
||||
"dns_rate_limit": "Hız sınırı",
|
||||
|
|
@ -158,6 +171,7 @@
|
|||
"edit_table_action": "Düzenle",
|
||||
"enable": "Etkinleştir",
|
||||
"enabled_dhcp": "DHCP sunucusu etkinleştirildi",
|
||||
"encrypted_dns_settings": "Şifrelenmiş DNS sunucu ayarları",
|
||||
"encryption_certificate_path": "Sertifika dosya yolu",
|
||||
"encryption_certificates": "Sertifikalar",
|
||||
"encryption_certificates_input": "PEM biçimindeki sertifikalarınızı kopyalayıp buraya yapıştırın.",
|
||||
|
|
@ -167,8 +181,11 @@
|
|||
"encryption_chain_valid": "Sertifika zinciri geçerli",
|
||||
"encryption_config_saved": "Şifreleme yapılandırması kaydedildi",
|
||||
"encryption_doq": "DNS-over-QUIC bağlantı noktası",
|
||||
"encryption_doq_summary": "DNS-over-QUIC bağlantı noktası: %value%",
|
||||
"encryption_dot": "DNS-over-TLS bağlantı noktası",
|
||||
"encryption_dot_summary": "DNS-over-TLS bağlantı noktası: %value%",
|
||||
"encryption_https": "HTTPS bağlantı noktası",
|
||||
"encryption_https_summary": "HTTPS bağlantı noktası: %value%",
|
||||
"encryption_key": "Özel anahtar",
|
||||
"encryption_key_input": "Sertifikanızın PEM biçimli özel anahtarını kopyalayıp buraya yapıştırın.",
|
||||
"encryption_key_source_content": "Özel anahtar içeriğini yapıştır",
|
||||
|
|
@ -177,6 +194,7 @@
|
|||
"encryption_private_key_path": "Özel anahtar dosya yolu",
|
||||
"encryption_server": "Sunucu adı",
|
||||
"encryption_server_enter": "Alan adınızı girin",
|
||||
"encryption_server_summary": "Sunucu adı: %value%",
|
||||
"encryption_title": "Şifreleme",
|
||||
"enter_cache_size": "Önbellek boyutunu bayt türünden girin",
|
||||
"enter_ip_address_placeholder": "IP adresi girin",
|
||||
|
|
@ -207,6 +225,7 @@
|
|||
"form_error_port": "Geçerli bir bağlantı noktası değeri girin",
|
||||
"form_error_port_range": "80-65535 aralığında geçerli bir bağlantı noktası değeri girin",
|
||||
"form_error_port_unsafe": "Güvenli olmayan bağlantı noktası",
|
||||
"form_error_required": "Bu alanı doldurun",
|
||||
"form_error_server_name": "Sunucu adı geçersiz",
|
||||
"form_error_url_format": "Geçerli bir URL veya dosya yolu girin",
|
||||
"friday": "Cuma",
|
||||
|
|
@ -266,6 +285,7 @@
|
|||
"processing_time_ms": "%value% ms",
|
||||
"protection": "Koruma",
|
||||
"protocol": "Protokol",
|
||||
"protocols": "Protokoller",
|
||||
"query_log_allowed": "İzin verilen",
|
||||
"query_log_blocked": "Engellendi",
|
||||
"query_log_blocked_services": "Engellenen hizmetler",
|
||||
|
|
@ -286,12 +306,16 @@
|
|||
"request_table_header": "İstek",
|
||||
"requests_table_header": "İstekler",
|
||||
"reset": "Sıfırla",
|
||||
"reset_dhcp_settings": "DHCP ayarlarını sıfırla",
|
||||
"reset_dns_confirm_title": "DNS protokolleri sıfırlansın mı?",
|
||||
"reset_dns_protocols": "DNS protokollerini sıfırla",
|
||||
"reset_settings": "Ayarları sıfırla",
|
||||
"reset_settings_confirm": "Evet, sıfırla",
|
||||
"response_details": "Yanıt ayrıntıları",
|
||||
"response_time": "Yanıt süresi",
|
||||
"result": "Sonuç",
|
||||
"rewrite_add": "DNS yeniden yazımı ekle",
|
||||
"rewrite_domain": "Alan adı veya joker karakter",
|
||||
"rewrite_edit": "DNS yeniden yazımı düzenle",
|
||||
"rewrite_hosts_applied": "Hosts dosyası kuralı tarafından yeniden yazıldı",
|
||||
"rewrite_settings_updated": "DNS yeniden yazım ayarları başarıyla güncellendi",
|
||||
|
|
@ -370,6 +394,7 @@
|
|||
"update_filters_interval_daily": "Her gün",
|
||||
"update_filters_interval_hourly": "Her saat",
|
||||
"update_filters_title": "Filtreleri otomatik günceller",
|
||||
"update_how_to": "Nasıl güncellenir",
|
||||
"updates_checked": "AdGuard Home'un yeni bir sürümü mevcut",
|
||||
"updates_version_equal": "AdGuard Home güncel",
|
||||
"upstream": "Üst kaynak",
|
||||
|
|
@ -393,12 +418,16 @@
|
|||
"user_rules_domain_blocked": "Alan adı engellendi",
|
||||
"user_rules_edit_dns_rewrite": "DNS yeniden yazımı düzenle",
|
||||
"user_rules_rewrite_rule_is_applied": "Yeniden yazım kuralı uygulandı",
|
||||
"user_rules_title": "Özel kurallar",
|
||||
"username_label": "Kullanıcı adı",
|
||||
"username_placeholder": "Kullanıcı adı girin",
|
||||
"validated_with_dnssec": "DNSSEC ile doğrulandı",
|
||||
"version_number": "Sürüm %value%",
|
||||
"wednesday": "Çarşamba",
|
||||
"whois": "WHOIS",
|
||||
"yes": "Evet",
|
||||
"yes_disable": "Evet, devre dışı bırak",
|
||||
"yes_disallow": "Evet, izin verme",
|
||||
"yes_remove": "Evet, kaldır",
|
||||
"yes_reset": "Evet, sıfırla"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"add_allowlist": "添加白名单",
|
||||
"add_blocklist": "添加黑名单",
|
||||
"add_persistent_client": "添加为持久客户端",
|
||||
"add_tls_certificate": "添加 TLS 证书",
|
||||
"add_to_allowlist": "添加到白名单",
|
||||
"ads_blocked": "广告已拦截",
|
||||
"ads_blocked_card": "广告已拦截",
|
||||
|
|
@ -155,6 +156,7 @@
|
|||
"copy": "复制",
|
||||
"copy_to_clipboard": "复制到剪贴板",
|
||||
"custom_filtering_rules": "自定义过滤器规则",
|
||||
"custom_rules": "自定义规则",
|
||||
"dark_theme": "暗黑主题",
|
||||
"dashboard": "仪表盘",
|
||||
"delete_btn": "删除",
|
||||
|
|
@ -252,6 +254,7 @@
|
|||
"dns_private_reverse_servers_label": "私有反向 DNS 服务器,每行一个",
|
||||
"dns_private_reverse_servers_placeholder": "IP 地址、sdns://、quic://、https://、h3:// 或 tls://",
|
||||
"dns_private_reverse_servers_title": "私人反向 DNS 服务器",
|
||||
"dns_protocols_title": "DNS 协议",
|
||||
"dns_queries": "DNS 查询",
|
||||
"dns_queries_tooltip": "过去 24 小时内处理的 DNS 查询数量",
|
||||
"dns_query": "DNS 查询",
|
||||
|
|
@ -283,6 +286,7 @@
|
|||
"enable": "启用",
|
||||
"enabled_dhcp": "DHCP 服务器已启用",
|
||||
"encrypted_dns_addresses": "加密 DNS 服务器地址",
|
||||
"encrypted_dns_settings": "加密 DNS 服务器设置",
|
||||
"encryption_certificate_has_issues": "证书存在问题",
|
||||
"encryption_certificate_path": "证书路径",
|
||||
"encryption_certificates": "证书",
|
||||
|
|
@ -298,8 +302,10 @@
|
|||
"encryption_disable_plain_dns": "禁用纯 DNS?",
|
||||
"encryption_disable_plain_dns_desc": "不支持加密 DNS 的设备可能无法正常工作",
|
||||
"encryption_doq": "DNS-over-QUIC 端口",
|
||||
"encryption_doq_summary": "DNS-over-QUIC 端口:%value%",
|
||||
"encryption_doq_tooltip": "此端口用于运行 DNS-over-QUIC 服务器",
|
||||
"encryption_dot": "DNS-over-TLS 端口",
|
||||
"encryption_dot_summary": "DNS-over-TLS 端口:%value%",
|
||||
"encryption_dot_tooltip": "此端口用于运行 DNS-over-TLS 服务器",
|
||||
"encryption_encrypted_dns": "加密 DNS",
|
||||
"encryption_encrypted_dns_desc": "AdGuard Home 管理界面使用 HTTPS。DNS 服务器支持 DNS-over-HTTPS、DNS-over-TLS 和 DNS-over-QUIC。",
|
||||
|
|
@ -307,6 +313,7 @@
|
|||
"encryption_force_redirect": "自动从 HTTP 重定向到 HTTPS",
|
||||
"encryption_hostnames": "主机名:%value%",
|
||||
"encryption_https": "HTTPS 端口",
|
||||
"encryption_https_summary": "HTTPS 端口:%value%",
|
||||
"encryption_https_tooltip": "此端口提供 AdGuard Home 管理界面的 HTTPS 访问。同时在 '/dns-query' 路径上提供 DNS-over-HTTPS。",
|
||||
"encryption_issuer": "颁发者:%value%",
|
||||
"encryption_key": "私钥",
|
||||
|
|
@ -321,6 +328,7 @@
|
|||
"encryption_private_key_path": "私钥路径",
|
||||
"encryption_server": "服务器名称",
|
||||
"encryption_server_enter": "输入您的域名",
|
||||
"encryption_server_summary": "服务器名称:%value%",
|
||||
"encryption_server_tooltip_1": "如果已设置,AdGuard Home 将检测 ClientID、响应 DDR 查询并执行额外的连接验证。如果未设置,这些功能将被禁用。",
|
||||
"encryption_server_tooltip_2": "必须与证书中的 DNS 名称匹配",
|
||||
"encryption_subject": "主题:%value%",
|
||||
|
|
@ -477,8 +485,9 @@
|
|||
"processing_time_ms": "%value% ms",
|
||||
"protection": "防护",
|
||||
"protocol": "协议",
|
||||
"protocols": "协议",
|
||||
"queries": "查询",
|
||||
"queries_total": "共 %count% 条查询 | 共 %count% 条查询",
|
||||
"queries_total": "共 %value% 条查询 | 共 %value% 条查询",
|
||||
"query_details": "查询详情",
|
||||
"query_log": "查询日志",
|
||||
"query_log_all_reasons": "全部原因",
|
||||
|
|
@ -530,6 +539,8 @@
|
|||
"request_table_header": "请求",
|
||||
"requests_table_header": "请求",
|
||||
"reset": "重置",
|
||||
"reset_dns_confirm_title": "重置 DNS 协议?",
|
||||
"reset_dns_protocols": "重置 DNS 协议",
|
||||
"reset_settings": "重置设置",
|
||||
"reset_settings_confirm": "重置",
|
||||
"response_details": "响应细节",
|
||||
|
|
@ -735,6 +746,7 @@
|
|||
"threats_blocked_tooltip": "被 AdGuard 浏览安全模块拦截的 DNS 查询数量",
|
||||
"thursday": "星期四",
|
||||
"time_table_header": "时间",
|
||||
"tls_certificate": "TLS 证书",
|
||||
"top_blocked_domains": "被拦截域名排行",
|
||||
"top_clients": "客户端排行",
|
||||
"top_upstreams": "经常请求的上游服务器",
|
||||
|
|
@ -853,6 +865,7 @@
|
|||
"user_rules_source_allowlist": "<strong>允许列表</strong>:%value%",
|
||||
"user_rules_source_blocklist": "<strong>拦截列表</strong>:%value%",
|
||||
"user_rules_status": "<strong>状态</strong>:%reason%",
|
||||
"user_rules_title": "自定义规则",
|
||||
"username_label": "用户名",
|
||||
"username_placeholder": "输入用户名",
|
||||
"validated_with_dnssec": "通过 DNSSEC 验证",
|
||||
|
|
|
|||
|
|
@ -478,7 +478,7 @@
|
|||
"protection": "防護",
|
||||
"protocol": "協定",
|
||||
"queries": "查詢",
|
||||
"queries_total": "共 %count% 條查詢 | 共 %count% 條查詢",
|
||||
"queries_total": "共 %value% 條查詢 | 共 %value% 條查詢",
|
||||
"query_details": "查詢詳細資料",
|
||||
"query_log": "查詢紀錄",
|
||||
"query_log_all_reasons": "全部原因",
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest';
|
|||
import {
|
||||
validateIdentifier,
|
||||
validateUpstreams,
|
||||
validateBootstrapDns,
|
||||
validateRequiredValue,
|
||||
validateIpv4,
|
||||
validatePort,
|
||||
|
|
@ -530,44 +529,6 @@ describe('validateUpstreams with bang prefix', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('validateBootstrapDns', () => {
|
||||
it('returns undefined for empty value', () => {
|
||||
expect(validateBootstrapDns('')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for valid IP', () => {
|
||||
expect(validateBootstrapDns('8.8.8.8')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns undefined for valid upstream URL', () => {
|
||||
expect(validateBootstrapDns('https://dns.example.com/dns-query')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('rejects # comment lines (no comment support)', () => {
|
||||
expect(validateBootstrapDns('# comment\n8.8.8.8')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects #-prefixed URLs that would otherwise pass address check', () => {
|
||||
expect(validateBootstrapDns('# https://dns.example.com')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects only # lines', () => {
|
||||
expect(validateBootstrapDns('# first\n# second')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects !-prefixed lines', () => {
|
||||
expect(validateBootstrapDns('! https://dns.example.com')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('rejects lines without dot or colon', () => {
|
||||
expect(validateBootstrapDns('not-a-server')).toBe('Invalid format');
|
||||
});
|
||||
|
||||
it('returns line-numbered error for mixed valid and # comment', () => {
|
||||
expect(validateBootstrapDns('8.8.8.8\n# comment')).toBe('Invalid format on line 2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateLeaseTime', () => {
|
||||
it('accepts 1 (minimum)', () => {
|
||||
expect(validateLeaseTime(1)).toBeUndefined();
|
||||
|
|
|
|||
117
client_v2/src/__tests__/common/controls/Textarea.test.tsx
Normal file
117
client_v2/src/__tests__/common/controls/Textarea.test.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import { render } from '@solidjs/testing-library';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { COMMENT_LINE_TOKENS } from 'panel/helpers/constants';
|
||||
|
||||
describe('Textarea — comment highlight', () => {
|
||||
// Use template literals with real newlines — JSX string attributes
|
||||
// treat \n as a literal backslash-n in some transform pipelines.
|
||||
const multilineValue = `# comment
|
||||
||example.com^`;
|
||||
|
||||
const commentOnlyValue = `! comment
|
||||
||example.com^`;
|
||||
|
||||
const indentedCommentValue = ' # indented comment';
|
||||
|
||||
it('renders overlay with all text when highlightComments is enabled', () => {
|
||||
const { container } = render(() => (
|
||||
<Textarea
|
||||
value={multilineValue}
|
||||
highlightComments
|
||||
commentPrefixes={COMMENT_LINE_TOKENS}
|
||||
/>
|
||||
));
|
||||
|
||||
const overlay = container.querySelector('[aria-hidden="true"]');
|
||||
expect(overlay).toBeInTheDocument();
|
||||
expect(overlay?.textContent).toContain('# comment');
|
||||
expect(overlay?.textContent).toContain('||example.com^');
|
||||
});
|
||||
|
||||
it('does not render overlay when highlightComments is disabled', () => {
|
||||
const { container } = render(() => <Textarea value={multilineValue} />);
|
||||
|
||||
const overlay = container.querySelector('[aria-hidden="true"]');
|
||||
expect(overlay).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies commentLine class to lines starting with #', () => {
|
||||
const { container } = render(() => (
|
||||
<Textarea
|
||||
value={multilineValue}
|
||||
highlightComments
|
||||
commentPrefixes={COMMENT_LINE_TOKENS}
|
||||
/>
|
||||
));
|
||||
|
||||
const overlay = container.querySelector('[aria-hidden="true"]');
|
||||
const spans = overlay?.querySelectorAll('span');
|
||||
expect(spans).toHaveLength(2);
|
||||
expect(spans?.[0].className).toContain('commentLine');
|
||||
expect(spans?.[1].className).toContain('overlayNonComment');
|
||||
});
|
||||
|
||||
it('detects ! as comment prefix when configured', () => {
|
||||
const { container } = render(() => (
|
||||
<Textarea
|
||||
value={commentOnlyValue}
|
||||
highlightComments
|
||||
commentPrefixes={COMMENT_LINE_TOKENS}
|
||||
/>
|
||||
));
|
||||
|
||||
const overlay = container.querySelector('[aria-hidden="true"]');
|
||||
const spans = overlay?.querySelectorAll('span');
|
||||
expect(spans?.[0].className).toContain('commentLine');
|
||||
});
|
||||
|
||||
it('detects comment with leading whitespace', () => {
|
||||
const { container } = render(() => (
|
||||
<Textarea
|
||||
value={indentedCommentValue}
|
||||
highlightComments
|
||||
commentPrefixes={COMMENT_LINE_TOKENS}
|
||||
/>
|
||||
));
|
||||
|
||||
const overlay = container.querySelector('[aria-hidden="true"]');
|
||||
const spans = overlay?.querySelectorAll('span');
|
||||
expect(spans?.[0].className).toContain('commentLine');
|
||||
});
|
||||
|
||||
it('applies transparentText class to textarea when highlightComments enabled', () => {
|
||||
const { container } = render(() => (
|
||||
<Textarea
|
||||
value={multilineValue}
|
||||
highlightComments
|
||||
commentPrefixes={COMMENT_LINE_TOKENS}
|
||||
/>
|
||||
));
|
||||
|
||||
const textarea = container.querySelector('textarea');
|
||||
expect(textarea?.className).toContain('transparentText');
|
||||
});
|
||||
|
||||
it('does not apply transparentText when highlightComments disabled', () => {
|
||||
const { container } = render(() => <Textarea value={multilineValue} />);
|
||||
|
||||
const textarea = container.querySelector('textarea');
|
||||
expect(textarea?.className).not.toContain('transparentText');
|
||||
});
|
||||
|
||||
it('renders the shared scroll container in highlighting mode', () => {
|
||||
const { container } = render(() => (
|
||||
<Textarea
|
||||
value={multilineValue}
|
||||
highlightComments
|
||||
commentPrefixes={COMMENT_LINE_TOKENS}
|
||||
/>
|
||||
));
|
||||
|
||||
// The overlay and textarea should share a parent
|
||||
const overlay = container.querySelector('[aria-hidden="true"]');
|
||||
const textarea = container.querySelector('textarea');
|
||||
expect(overlay?.parentElement).toBe(textarea?.parentElement);
|
||||
});
|
||||
});
|
||||
|
|
@ -8,6 +8,7 @@ const mockDashboardState = {
|
|||
theme: 'light',
|
||||
language: 'en',
|
||||
name: '',
|
||||
checkUpdateFlag: true,
|
||||
};
|
||||
|
||||
vi.mock('panel/stores/dashboard', () => ({
|
||||
|
|
@ -59,6 +60,7 @@ describe('Footer', () => {
|
|||
vi.clearAllMocks();
|
||||
mockDashboardState.dnsVersion = '';
|
||||
mockDashboardState.processingVersion = true;
|
||||
mockDashboardState.checkUpdateFlag = true;
|
||||
});
|
||||
|
||||
it('hides version badge when dnsVersion is empty', () => {
|
||||
|
|
@ -118,4 +120,16 @@ describe('Footer', () => {
|
|||
const button = screen.getByTestId('footer-check-updates');
|
||||
expect(button.getAttribute('aria-label')).toBe('Check for updates');
|
||||
});
|
||||
|
||||
it('hides check-updates button when checkUpdateFlag is false (Docker/Snap)', () => {
|
||||
mockDashboardState.dnsVersion = 'v1.0.0';
|
||||
mockDashboardState.processingVersion = false;
|
||||
mockDashboardState.checkUpdateFlag = false;
|
||||
|
||||
render(() => <Footer />);
|
||||
|
||||
expect(screen.queryByTestId('footer-check-updates')).not.toBeInTheDocument();
|
||||
// Version text is still visible
|
||||
expect(screen.getByText('Version v1.0.0')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
100
client_v2/src/__tests__/components/settings-helpers.test.ts
Normal file
100
client_v2/src/__tests__/components/settings-helpers.test.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
|
||||
import {
|
||||
buildQueryLogConfig,
|
||||
buildStatsConfig,
|
||||
type QueryLogConfig,
|
||||
type StatsConfig,
|
||||
} from 'panel/components/Settings/helpers';
|
||||
|
||||
describe('buildQueryLogConfig', () => {
|
||||
it('returns only the five config fields', () => {
|
||||
const state: QueryLogConfig & Record<string, unknown> = {
|
||||
enabled: true,
|
||||
anonymize_client_ip: false,
|
||||
interval: 86400000,
|
||||
ignored: ['example.org'],
|
||||
ignored_enabled: true,
|
||||
// runtime fields that must NOT appear in the result
|
||||
processingGetLogs: true,
|
||||
processingClear: false,
|
||||
processingGetConfig: false,
|
||||
processingSetConfig: false,
|
||||
processingAdditionalLogs: false,
|
||||
logs: [] as unknown[],
|
||||
oldest: '',
|
||||
filter: {} as Record<string, unknown>,
|
||||
isFiltered: false,
|
||||
isDetailed: true,
|
||||
isEntireLog: false,
|
||||
customInterval: null as null,
|
||||
};
|
||||
const result = buildQueryLogConfig(state);
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
anonymize_client_ip: false,
|
||||
interval: 86400000,
|
||||
ignored: ['example.org'],
|
||||
ignored_enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('applies overrides on top of the store values', () => {
|
||||
const state: QueryLogConfig = {
|
||||
enabled: true,
|
||||
anonymize_client_ip: false,
|
||||
interval: 86400000,
|
||||
ignored: [],
|
||||
ignored_enabled: false,
|
||||
};
|
||||
const result = buildQueryLogConfig(state, {
|
||||
enabled: false,
|
||||
ignored: ['a', 'b'],
|
||||
});
|
||||
expect(result).toEqual({
|
||||
enabled: false,
|
||||
anonymize_client_ip: false,
|
||||
interval: 86400000,
|
||||
ignored: ['a', 'b'],
|
||||
ignored_enabled: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildStatsConfig', () => {
|
||||
it('returns only the four config fields', () => {
|
||||
const state: StatsConfig & Record<string, unknown> = {
|
||||
enabled: true,
|
||||
interval: 86400000,
|
||||
ignored: [],
|
||||
ignored_enabled: false,
|
||||
processingGetConfig: false,
|
||||
processingSetConfig: false,
|
||||
processingStats: false,
|
||||
processingReset: false,
|
||||
customInterval: null as null,
|
||||
dnsQueries: [] as unknown[],
|
||||
topClients: [] as unknown[],
|
||||
avgProcessingTime: 0,
|
||||
};
|
||||
const result = buildStatsConfig(state);
|
||||
expect(result).toEqual({
|
||||
enabled: true,
|
||||
interval: 86400000,
|
||||
ignored: [],
|
||||
ignored_enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('applies overrides', () => {
|
||||
const state = {
|
||||
enabled: true,
|
||||
interval: 86400000,
|
||||
ignored: ['x'],
|
||||
ignored_enabled: true,
|
||||
};
|
||||
const result = buildStatsConfig(state, { enabled: false });
|
||||
expect(result.enabled).toBe(false);
|
||||
expect(result.interval).toBe(86400000);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import { render, screen } from '@solidjs/testing-library';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { HashRouter, Route } from '@solidjs/router';
|
||||
import type { Component } from 'solid-js';
|
||||
|
||||
import { EmptyState } from 'panel/components/Dashboard/blocks/EmptyState/EmptyState';
|
||||
|
||||
const renderWithRouter = (component: Component) => {
|
||||
return render(() => (
|
||||
<HashRouter>
|
||||
<Route path="/" component={component} />
|
||||
</HashRouter>
|
||||
));
|
||||
};
|
||||
|
||||
describe('Dashboard EmptyState', () => {
|
||||
it('renders no_stats_yet in default mode', () => {
|
||||
renderWithRouter(() => <EmptyState mode="default" />);
|
||||
expect(screen.getByTestId('dashboard-empty-state')).toBeInTheDocument();
|
||||
expect(screen.getByText(/No stats yet/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders period_notify in disabled mode with settings link', () => {
|
||||
renderWithRouter(() => <EmptyState mode="disabled" />);
|
||||
expect(screen.getByTestId('dashboard-empty-state')).toBeInTheDocument();
|
||||
// period_notify: "To change the displayed periods, edit Statistics retention in General settings"
|
||||
expect(screen.getByText(/Statistics retention/)).toBeInTheDocument();
|
||||
// The "General settings" text is rendered inside a Link
|
||||
// Note: period_notify uses — the link name is "General\u00a0settings"
|
||||
expect(screen.getByRole('link', { name: /General/ })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('defaults to default mode when no mode prop provided', () => {
|
||||
renderWithRouter(() => <EmptyState />);
|
||||
expect(screen.getByText(/No stats yet/)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -29,7 +29,7 @@ import { getDnsStatus } from 'panel/stores/dashboard';
|
|||
describe('getDnsStatus', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('fetches TLS status when the core is running (FR-005)', async () => {
|
||||
it('fetches TLS status when the core is running', async () => {
|
||||
mocks.getGlobalVersion.mockResolvedValue({
|
||||
disabled: true,
|
||||
new_version: 'x',
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ describe('setDhcpConfig', () => {
|
|||
mocks.setDhcpConfig.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('shows dhcp_config_saved toast (FR-010)', async () => {
|
||||
it('shows dhcp_config_saved toast', async () => {
|
||||
await setDhcpConfig({
|
||||
v4: { range_start: 'a', range_end: 'b' },
|
||||
interface_name: 'eth0',
|
||||
|
|
@ -95,7 +95,7 @@ describe('toggleDhcp', () => {
|
|||
mocks.setDhcpConfig.mockResolvedValue(undefined);
|
||||
});
|
||||
|
||||
it('computes enabled from passed config, not current state (FR-011)', async () => {
|
||||
it('computes enabled from passed config, not current state', async () => {
|
||||
await toggleDhcp({ enabled: false, interface_name: 'eth0' });
|
||||
expect(mocks.setDhcpConfig).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ enabled: true }),
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ import { getDnsConfig, dnsConfigState, toggleResolveClients } from 'panel/stores
|
|||
describe('getDnsConfig', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('defaults null blocking IPs and empty upstream_mode (FR-012)', async () => {
|
||||
it('defaults null blocking IPs and empty upstream_mode', async () => {
|
||||
mocks.getDnsConfig.mockResolvedValue({
|
||||
blocking_ipv4: null,
|
||||
blocking_ipv6: null,
|
||||
|
|
@ -42,7 +42,7 @@ describe('getDnsConfig', () => {
|
|||
describe('toggleResolveClients', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('toggles resolve_clients and persists (FR-032)', async () => {
|
||||
it('toggles resolve_clients and persists', async () => {
|
||||
mocks.setDnsConfig.mockResolvedValue({});
|
||||
|
||||
const before = dnsConfigState.resolve_clients;
|
||||
|
|
@ -54,7 +54,7 @@ describe('toggleResolveClients', () => {
|
|||
expect(dnsConfigState.resolve_clients).toBe(before);
|
||||
});
|
||||
|
||||
it('calls apiClient.setDnsConfig with inverted value (FR-032)', async () => {
|
||||
it('calls apiClient.setDnsConfig with inverted value', async () => {
|
||||
mocks.setDnsConfig.mockResolvedValue({});
|
||||
|
||||
const before = dnsConfigState.resolve_clients;
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ const mocks = vi.hoisted(() => ({
|
|||
validateTlsConfig: vi.fn(),
|
||||
addErrorToast: vi.fn(),
|
||||
addSuccessToast: vi.fn(),
|
||||
redirectToCurrentProtocol: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('panel/api/Api', () => ({
|
||||
|
|
@ -18,7 +19,13 @@ vi.mock('panel/stores/toasts', () => ({
|
|||
addErrorToast: mocks.addErrorToast,
|
||||
addSuccessToast: mocks.addSuccessToast,
|
||||
}));
|
||||
vi.mock('panel/stores/dashboard', () => ({ getDnsStatus: vi.fn() }));
|
||||
vi.mock('panel/stores/dashboard', () => ({
|
||||
getDnsStatus: vi.fn(),
|
||||
dashboardState: { httpPort: 80 },
|
||||
}));
|
||||
vi.mock('panel/helpers/helpers', () => ({
|
||||
redirectToCurrentProtocol: mocks.redirectToCurrentProtocol,
|
||||
}));
|
||||
|
||||
import {
|
||||
setTlsConfig,
|
||||
|
|
@ -30,7 +37,7 @@ import {
|
|||
describe('setTlsConfig', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('defaults empty ports to 0 (FR-013)', async () => {
|
||||
it('defaults empty ports to 0', async () => {
|
||||
mocks.setTlsConfig.mockImplementation(async (v: any) => ({
|
||||
...v,
|
||||
certificate_chain: '',
|
||||
|
|
@ -88,10 +95,9 @@ describe('setTlsConfig', () => {
|
|||
expect(encryptionState.dns_names).toBeNull();
|
||||
});
|
||||
|
||||
it('reloads when enabled+force_https on http: origin (FR-014)', async () => {
|
||||
const reloadFn = vi.fn();
|
||||
it('calls redirectToCurrentProtocol when enabled+force_https on http: origin', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { protocol: 'http:', reload: reloadFn },
|
||||
value: { protocol: 'http:' },
|
||||
writable: true,
|
||||
});
|
||||
mocks.setTlsConfig.mockImplementation(async (v: any) => ({
|
||||
|
|
@ -106,6 +112,61 @@ describe('setTlsConfig', () => {
|
|||
force_https: true,
|
||||
port_https: 443,
|
||||
});
|
||||
expect(reloadFn).toHaveBeenCalled();
|
||||
expect(mocks.redirectToCurrentProtocol).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
force_https: true,
|
||||
port_https: 443,
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls redirectToCurrentProtocol when disabling encryption on https: origin', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { protocol: 'https:' },
|
||||
writable: true,
|
||||
});
|
||||
mocks.setTlsConfig.mockImplementation(async (v: any) => ({
|
||||
...v,
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
}));
|
||||
await setTlsConfig({
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
enabled: false,
|
||||
});
|
||||
expect(mocks.redirectToCurrentProtocol).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ enabled: false }),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
|
||||
it('calls redirectToCurrentProtocol when changing port_https on https: origin', async () => {
|
||||
Object.defineProperty(window, 'location', {
|
||||
value: { protocol: 'https:' },
|
||||
writable: true,
|
||||
});
|
||||
mocks.setTlsConfig.mockImplementation(async (v: any) => ({
|
||||
...v,
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
}));
|
||||
await setTlsConfig({
|
||||
certificate_chain: '',
|
||||
private_key: '',
|
||||
enabled: true,
|
||||
force_https: true,
|
||||
port_https: 8443,
|
||||
});
|
||||
expect(mocks.redirectToCurrentProtocol).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
enabled: true,
|
||||
force_https: true,
|
||||
port_https: 8443,
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ vi.mock('panel/stores/toasts', () => ({
|
|||
describe('getFilteringStatus', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('normalizes filters to camelCase and joins user_rules array (FR-003)', async () => {
|
||||
it('normalizes filters to camelCase and joins user_rules array', async () => {
|
||||
mocks.apiGetFilteringStatus.mockResolvedValue({
|
||||
enabled: true,
|
||||
interval: 24,
|
||||
|
|
@ -68,7 +68,7 @@ describe('setRules', () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sends user_rules as a string array (FR-021)', async () => {
|
||||
it('sends user_rules as a string array', async () => {
|
||||
const result = await setRules('||fresh.example^\n');
|
||||
|
||||
expect(mocks.apiSetRules).toHaveBeenCalledWith({
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ vi.mock('panel/stores/toasts', () => ({
|
|||
describe('getFilteringStatus', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('normalizes filters to camelCase and joins user_rules array (FR-003)', async () => {
|
||||
it('normalizes filters to camelCase and joins user_rules array', async () => {
|
||||
mocks.apiGetFilteringStatus.mockResolvedValue({
|
||||
enabled: true,
|
||||
interval: 24,
|
||||
|
|
@ -68,7 +68,7 @@ describe('setRules', () => {
|
|||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('sends user_rules as a string array (FR-021)', async () => {
|
||||
it('sends user_rules as a string array', async () => {
|
||||
const result = await setRules('||fresh.example^\n');
|
||||
|
||||
expect(mocks.apiSetRules).toHaveBeenCalledWith({
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ describe('getStats', () => {
|
|||
mocks.searchClients.mockResolvedValue([]);
|
||||
});
|
||||
|
||||
it('enriches top clients and stores normalizedTopClients (FR-001/019)', async () => {
|
||||
it('enriches top clients and stores normalizedTopClients', async () => {
|
||||
mocks.getStats.mockResolvedValue({
|
||||
top_clients: [{ '1.2.3.4': 5 }],
|
||||
avg_processing_time: 0.012,
|
||||
|
|
@ -44,7 +44,7 @@ describe('getStats', () => {
|
|||
expect(Object.keys(statsState.normalizedTopClients.auto)).toContain('1.2.3.4');
|
||||
});
|
||||
|
||||
it('converts avg_processing_time to milliseconds, falsy passthrough (FR-002)', async () => {
|
||||
it('converts avg_processing_time to milliseconds, falsy passthrough', async () => {
|
||||
mocks.getStats.mockResolvedValue({ avg_processing_time: 0.012 });
|
||||
await getStats();
|
||||
expect(statsState.avgProcessingTime).toBe(12);
|
||||
|
|
@ -53,4 +53,22 @@ describe('getStats', () => {
|
|||
await getStats();
|
||||
expect(statsState.avgProcessingTime).toBe(0); // not NaN
|
||||
});
|
||||
|
||||
it('converts top_upstreams_avg_time entries from seconds to milliseconds', async () => {
|
||||
mocks.getStats.mockResolvedValue({
|
||||
top_upstreams_avg_time: [{ '1.1.1.1': 0.012 }, { '9.9.9.9': 0.3 }],
|
||||
top_clients: [],
|
||||
avg_processing_time: 0,
|
||||
top_blocked_domains: [],
|
||||
top_queried_domains: [],
|
||||
top_upstreams_responses: [],
|
||||
});
|
||||
|
||||
await getStats();
|
||||
|
||||
expect(statsState.topUpstreamsAvgTime).toEqual([
|
||||
{ name: '1.1.1.1', count: 12 },
|
||||
{ name: '9.9.9.9', count: 300 },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
262
client_v2/src/__tests__/tooltip.test.tsx
Normal file
262
client_v2/src/__tests__/tooltip.test.tsx
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach, beforeAll } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@solidjs/testing-library';
|
||||
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
|
||||
// jsdom lacks ResizeObserver, which floating-ui (used by Zag positioning) needs.
|
||||
beforeAll(() => {
|
||||
if (!global.ResizeObserver) {
|
||||
global.ResizeObserver = class {
|
||||
observe() {}
|
||||
unobserve() {}
|
||||
disconnect() {}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// Restore real matchMedia between test suites. The global mock from
|
||||
// src/__tests__/setup.ts returns matches: false for every query (desktop),
|
||||
// which is what most tests need. Touch tests in this file replace it
|
||||
// temporarily so that (hover: none) returns true.
|
||||
const realMatchMedia = window.matchMedia;
|
||||
|
||||
function mockTouchDevice() {
|
||||
window.matchMedia = (query: string): MediaQueryList => {
|
||||
const matches = query === '(hover: none)';
|
||||
return {
|
||||
matches,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
} as MediaQueryList;
|
||||
};
|
||||
}
|
||||
|
||||
function mockDesktop() {
|
||||
window.matchMedia = realMatchMedia;
|
||||
}
|
||||
|
||||
describe('Tooltip', () => {
|
||||
beforeEach(() => {
|
||||
mockDesktop();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Clean up any remaining fake timers between tests.
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
// ---- general rendering ----
|
||||
|
||||
it('renders children', () => {
|
||||
render(() => (
|
||||
<Tooltip content="Help text">
|
||||
<span>Trigger</span>
|
||||
</Tooltip>
|
||||
));
|
||||
expect(screen.getByText('Trigger')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders content element (hidden when closed)', () => {
|
||||
render(() => (
|
||||
<Tooltip content="Help text">
|
||||
<span>Trigger</span>
|
||||
</Tooltip>
|
||||
));
|
||||
const content = screen.getByText('Help text');
|
||||
expect(content).toBeInTheDocument();
|
||||
expect(content.closest('[hidden]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('renders without tooltip wrapper when disabled', () => {
|
||||
const { container } = render(() => (
|
||||
<Tooltip content="Help text" disabled>
|
||||
<span>Trigger</span>
|
||||
</Tooltip>
|
||||
));
|
||||
expect(screen.getByText('Trigger')).toBeInTheDocument();
|
||||
expect(container.querySelector('[data-scope="tooltip"]')).toBeNull();
|
||||
});
|
||||
|
||||
it('applies custom class to the trigger wrapper', () => {
|
||||
const { container } = render(() => (
|
||||
<Tooltip content="Help text" class="my-custom-class">
|
||||
<span>Trigger</span>
|
||||
</Tooltip>
|
||||
));
|
||||
const trigger = container.querySelector('[data-part="trigger"]');
|
||||
expect(trigger?.className).toContain('my-custom-class');
|
||||
});
|
||||
|
||||
it('applies overlayClass to the content', () => {
|
||||
render(() => (
|
||||
<Tooltip content="Help text" overlayClass="custom-overlay">
|
||||
<span>Trigger</span>
|
||||
</Tooltip>
|
||||
));
|
||||
const content = screen.getByText('Help text').closest('[data-part="content"]');
|
||||
expect(content?.className).toContain('custom-overlay');
|
||||
});
|
||||
|
||||
// ---- desktop hover (default matchMedia mock → (hover: none) = false) ----
|
||||
|
||||
it('opens on hover after open delay on desktop', async () => {
|
||||
render(() => (
|
||||
<Tooltip content="Tooltip text">
|
||||
<span>Hover me</span>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
const triggerEl = screen.getByText('Hover me').closest('[data-part="trigger"]')!;
|
||||
const content = screen.getByText('Tooltip text').closest('[data-part="content"]')!;
|
||||
|
||||
expect(content.hasAttribute('hidden')).toBe(true);
|
||||
|
||||
fireEvent.pointerOver(triggerEl, { pointerType: 'mouse' });
|
||||
|
||||
// SHOW_DELAY is 200 ms; use generous timeout (real timers, ~200 ms actual).
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(content.hasAttribute('hidden')).toBe(false);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
});
|
||||
|
||||
it('closes on pointer leave after close delay on desktop', async () => {
|
||||
render(() => (
|
||||
<Tooltip content="Tooltip text">
|
||||
<span>Hover me</span>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
const triggerEl = screen.getByText('Hover me').closest('[data-part="trigger"]')!;
|
||||
const content = screen.getByText('Tooltip text').closest('[data-part="content"]')!;
|
||||
|
||||
// Open via hover.
|
||||
fireEvent.pointerOver(triggerEl, { pointerType: 'mouse' });
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(content.hasAttribute('hidden')).toBe(false);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
|
||||
// Leave → closes after HIDE_DELAY (300 ms).
|
||||
// pointerleave does NOT bubble, so fire on the trigger element itself.
|
||||
fireEvent.pointerLeave(triggerEl);
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(content.hasAttribute('hidden')).toBe(true);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
});
|
||||
|
||||
// ---- touch (override matchMedia to report (hover: none) = true) ----
|
||||
|
||||
describe('touch device', () => {
|
||||
beforeEach(() => {
|
||||
mockTouchDevice();
|
||||
});
|
||||
|
||||
it('opens on click (tap)', async () => {
|
||||
render(() => (
|
||||
<Tooltip content="Touch help">
|
||||
<span>Tap me</span>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
const trigger = screen.getByText('Tap me');
|
||||
const content = screen.getByText('Touch help').closest('[data-part="content"]')!;
|
||||
|
||||
expect(content.hasAttribute('hidden')).toBe(true);
|
||||
|
||||
fireEvent.click(trigger);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(content.hasAttribute('hidden')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('does not close on second click (close is outside-tap only)', async () => {
|
||||
render(() => (
|
||||
<Tooltip content="Touch help">
|
||||
<span>Tap me</span>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
const trigger = screen.getByText('Tap me');
|
||||
const content = screen.getByText('Touch help').closest('[data-part="content"]')!;
|
||||
|
||||
// Open.
|
||||
fireEvent.click(trigger);
|
||||
await waitFor(() => {
|
||||
expect(content.hasAttribute('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
// Second tap on trigger — stays open (no toggle).
|
||||
fireEvent.click(trigger);
|
||||
expect(content.hasAttribute('hidden')).toBe(false);
|
||||
|
||||
// Outside tap closes.
|
||||
fireEvent.pointerDown(document.body);
|
||||
await waitFor(() => {
|
||||
expect(content.hasAttribute('hidden')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('closes when tapping outside (document pointerdown)', async () => {
|
||||
render(() => (
|
||||
<Tooltip content="Touch help">
|
||||
<span>Tap me</span>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
const trigger = screen.getByText('Tap me');
|
||||
const content = screen.getByText('Touch help').closest('[data-part="content"]')!;
|
||||
|
||||
// Open.
|
||||
fireEvent.click(trigger);
|
||||
await waitFor(() => {
|
||||
expect(content.hasAttribute('hidden')).toBe(false);
|
||||
});
|
||||
|
||||
// Tap on document.body (outside both trigger and content).
|
||||
fireEvent.pointerDown(document.body);
|
||||
await waitFor(() => {
|
||||
expect(content.hasAttribute('hidden')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('does NOT close when tapping inside open content (interactive)', async () => {
|
||||
render(() => (
|
||||
<Tooltip content={<a href="#">Link inside</a>}>
|
||||
<span>Tap me</span>
|
||||
</Tooltip>
|
||||
));
|
||||
|
||||
const trigger = screen.getByText('Tap me');
|
||||
|
||||
// Open via tap.
|
||||
fireEvent.click(trigger);
|
||||
|
||||
const contentEl = screen.getByText('Link inside').closest('[data-part="content"]')!;
|
||||
await waitFor(
|
||||
() => {
|
||||
expect(contentEl.hasAttribute('hidden')).toBe(false);
|
||||
},
|
||||
{ timeout: 1000 },
|
||||
);
|
||||
|
||||
// Tap on the link inside the open content.
|
||||
fireEvent.pointerDown(screen.getByText('Link inside'));
|
||||
// Should stay open.
|
||||
expect(contentEl.hasAttribute('hidden')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -157,12 +157,12 @@
|
|||
[data-scope='select'][data-part='content'],
|
||||
[data-scope='combobox'][data-part='content'] {
|
||||
color: var(--default-main-text);
|
||||
padding: 0;
|
||||
margin-top: 4px;
|
||||
padding: 8px 0;
|
||||
overflow: hidden;
|
||||
box-shadow: 8px 8px 24px 0 rgba(0, 0, 0, 0.1);
|
||||
background-color: var(--default-dropdown-menu-background);
|
||||
z-index: 3;
|
||||
border-radius: 8px;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
left: initial;
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ type Props = Omit<
|
|||
};
|
||||
|
||||
export const Textarea = (props: Props) => {
|
||||
const [scrollTop, setScrollTop] = createSignal(0);
|
||||
const [currentValue, setCurrentValue] = createSignal('');
|
||||
|
||||
// Sync from external prop changes (e.g. dialog open/close resets);
|
||||
|
|
@ -42,6 +41,16 @@ export const Textarea = (props: Props) => {
|
|||
|
||||
const highlightEnabled = () => !!props.highlightComments;
|
||||
|
||||
// Convert rows to CSS height for the shared scroll container.
|
||||
// line-height: 1.3 * font-size: 16px = 20.8px per row.
|
||||
// Add 34px for overlay padding (16px × 2) + scrollArea border (1px × 2).
|
||||
const LINE_HEIGHT_PX = 20.8;
|
||||
const CHROME_PX = 34;
|
||||
const scrollAreaStyle = (): JSX.CSSProperties | undefined => {
|
||||
if (!highlightEnabled() || props.rows == null) return undefined;
|
||||
return { height: `${Number(props.rows) * LINE_HEIGHT_PX + CHROME_PX}px` };
|
||||
};
|
||||
|
||||
const setRef = (el: HTMLTextAreaElement) => {
|
||||
if (typeof props.ref === 'function') {
|
||||
props.ref(el);
|
||||
|
|
@ -64,7 +73,6 @@ export const Textarea = (props: Props) => {
|
|||
const handleScroll = (
|
||||
e: Event & { currentTarget: HTMLTextAreaElement; target: HTMLTextAreaElement },
|
||||
) => {
|
||||
setScrollTop(e.target.scrollTop);
|
||||
props.onScroll?.(e);
|
||||
};
|
||||
|
||||
|
|
@ -75,38 +83,63 @@ export const Textarea = (props: Props) => {
|
|||
{props.label}
|
||||
</label>
|
||||
</Show>
|
||||
<div class={s.textareaContainer}>
|
||||
<textarea
|
||||
class={cn(
|
||||
s.textarea,
|
||||
props.size && s[props.size],
|
||||
{ [s.error]: !!props.errorMessage },
|
||||
{ [s.transparentText]: highlightEnabled() },
|
||||
props.class,
|
||||
)}
|
||||
id={props.id}
|
||||
name={props.name}
|
||||
placeholder={props.placeholder}
|
||||
value={props.value as string}
|
||||
cols={props.cols}
|
||||
rows={props.rows}
|
||||
onChange={handleChange}
|
||||
onInput={handleInput}
|
||||
onBlur={handleBlur}
|
||||
onScroll={handleScroll}
|
||||
wrap={props.wrap}
|
||||
maxLength={props.maxLength}
|
||||
disabled={props.disabled}
|
||||
ref={(el) => setRef(el)}
|
||||
/>
|
||||
<Show when={highlightEnabled()}>
|
||||
<TextareaHighlight
|
||||
value={currentValue}
|
||||
scrollTop={scrollTop}
|
||||
commentPrefixes={props.commentPrefixes}
|
||||
<Show
|
||||
when={highlightEnabled()}
|
||||
fallback={
|
||||
<textarea
|
||||
class={cn(
|
||||
s.textarea,
|
||||
props.size && s[props.size],
|
||||
{ [s.error]: !!props.errorMessage },
|
||||
props.class,
|
||||
)}
|
||||
id={props.id}
|
||||
name={props.name}
|
||||
placeholder={props.placeholder}
|
||||
value={props.value as string}
|
||||
cols={props.cols}
|
||||
rows={props.rows}
|
||||
onChange={handleChange}
|
||||
onInput={handleInput}
|
||||
onBlur={handleBlur}
|
||||
onScroll={handleScroll}
|
||||
wrap={props.wrap}
|
||||
maxLength={props.maxLength}
|
||||
disabled={props.disabled}
|
||||
ref={(el: HTMLTextAreaElement) => setRef(el)}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
class={cn(s.scrollArea, props.size && s[props.size], {
|
||||
[s.error]: !!props.errorMessage,
|
||||
})}
|
||||
style={scrollAreaStyle()}
|
||||
>
|
||||
<div class={s.contentWrapper}>
|
||||
<TextareaHighlight
|
||||
value={currentValue}
|
||||
commentPrefixes={props.commentPrefixes}
|
||||
/>
|
||||
<textarea
|
||||
class={cn(s.transparentText, props.class)}
|
||||
id={props.id}
|
||||
name={props.name}
|
||||
placeholder={props.placeholder}
|
||||
value={props.value as string}
|
||||
cols={props.cols}
|
||||
onChange={handleChange}
|
||||
onInput={handleInput}
|
||||
onBlur={handleBlur}
|
||||
onScroll={handleScroll}
|
||||
wrap={props.wrap}
|
||||
maxLength={props.maxLength}
|
||||
disabled={props.disabled}
|
||||
ref={(el: HTMLTextAreaElement) => setRef(el)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.errorMessage}>
|
||||
<div class={s.errorMessage}>{props.errorMessage}</div>
|
||||
</Show>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { For, type Accessor, createEffect, createMemo } from 'solid-js';
|
||||
import { For, type Accessor, createMemo } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { COMMENT_LINE_DEFAULT_TOKEN, type CommentLineToken } from 'panel/helpers/constants';
|
||||
|
|
@ -7,7 +7,6 @@ import s from './styles.module.pcss';
|
|||
|
||||
type Props = {
|
||||
value: Accessor<string>;
|
||||
scrollTop: Accessor<number>;
|
||||
class?: string;
|
||||
/** Comment line prefixes to highlight. Defaults to {@link COMMENT_LINE_DEFAULT_TOKEN}. */
|
||||
commentPrefixes?: readonly CommentLineToken[];
|
||||
|
|
@ -15,7 +14,6 @@ type Props = {
|
|||
|
||||
export const TextareaHighlight = (props: Props) => {
|
||||
const lines = () => (props.value() || '').split('\n');
|
||||
let ref: HTMLDivElement | undefined;
|
||||
|
||||
const prefixes = createMemo(() => props.commentPrefixes || [COMMENT_LINE_DEFAULT_TOKEN]);
|
||||
|
||||
|
|
@ -24,21 +22,8 @@ export const TextareaHighlight = (props: Props) => {
|
|||
return prefixes().some((p) => trimmed.startsWith(p));
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
const el = ref;
|
||||
if (el) {
|
||||
el.scrollTop = props.scrollTop();
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={(el) => {
|
||||
ref = el;
|
||||
}}
|
||||
class={cn(s.overlay, props.class)}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class={cn(s.overlay, props.class)} aria-hidden="true">
|
||||
<For each={lines()}>
|
||||
{(line, index) => (
|
||||
<>
|
||||
|
|
@ -46,6 +31,7 @@ export const TextareaHighlight = (props: Props) => {
|
|||
<span
|
||||
class={cn({
|
||||
[s.commentLine]: isComment(line),
|
||||
[s.overlayNonComment]: !isComment(line),
|
||||
})}
|
||||
>
|
||||
{line || ' '}
|
||||
|
|
|
|||
|
|
@ -62,46 +62,88 @@
|
|||
color: var(--default-description-text);
|
||||
}
|
||||
|
||||
.textareaContainer {
|
||||
position: relative;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
overflow-y: auto;
|
||||
pointer-events: none;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
padding: 16px;
|
||||
border: 1px solid transparent;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
line-height: 1.3;
|
||||
max-width: 100%;
|
||||
.scrollArea {
|
||||
overflow: auto;
|
||||
border: 1px solid var(--default-item-divider);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.commentLine {
|
||||
color: var(--default-additional-link);
|
||||
}
|
||||
|
||||
.transparentText {
|
||||
color: transparent;
|
||||
caret-color: var(--default-main-text);
|
||||
background-color: var(--default-page-background);
|
||||
transition:
|
||||
border var(--t2),
|
||||
background-color var(--t2);
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--pressed-link);
|
||||
}
|
||||
|
||||
&.error {
|
||||
border-color: var(--default-error-icon);
|
||||
}
|
||||
|
||||
&.medium {
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
&.large {
|
||||
min-height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.contentWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
line-height: 1.3;
|
||||
max-width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.commentLine {
|
||||
color: var(--default-additional-link);
|
||||
}
|
||||
|
||||
.overlayNonComment {
|
||||
color: var(--default-main-text);
|
||||
}
|
||||
|
||||
.transparentText {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
color: transparent;
|
||||
caret-color: var(--default-main-text);
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 16px;
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
line-height: 1.3;
|
||||
resize: none !important;
|
||||
outline: none;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
|
||||
&::placeholder {
|
||||
color: var(--default-placeholder);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: transparent;
|
||||
caret-color: var(--default-description-text);
|
||||
}
|
||||
}
|
||||
|
||||
.errorMessage {
|
||||
|
|
|
|||
|
|
@ -53,3 +53,14 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.trigger {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.flexWrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
* already-migrated Select ([data-scope="select"]) and any future Ark UI widget.
|
||||
*/
|
||||
[data-scope='popover'][data-part='anchor'] {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,37 +6,26 @@ import { Popover } from '@ark-ui/solid';
|
|||
import './Dropdown.pcss';
|
||||
import s from './Dropdown.module.pcss';
|
||||
|
||||
const TIMEOUT_HIDE_TOOLTIP = 1000;
|
||||
const HIDE_DELAY = 200;
|
||||
|
||||
type Props = {
|
||||
overlayClass?: string;
|
||||
menu: JSX.Element;
|
||||
position?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft' | 'top';
|
||||
trigger: 'click' | 'hover';
|
||||
noIcon?: true;
|
||||
iconClass?: string;
|
||||
class?: string;
|
||||
openClass?: string;
|
||||
open?: boolean;
|
||||
onOpenChange?: (e: boolean) => void;
|
||||
widthAuto?: boolean;
|
||||
flex?: boolean;
|
||||
minOverlayWidthMatchTrigger?: boolean;
|
||||
flexWrapper?: boolean;
|
||||
childrenClass?: string;
|
||||
wrapClass?: string;
|
||||
children?: JSX.Element;
|
||||
isSelect?: boolean;
|
||||
disableAnimation?: boolean;
|
||||
disabled?: boolean;
|
||||
autoClose?: boolean;
|
||||
};
|
||||
|
||||
export const Dropdown = (props: Props) => {
|
||||
let timer: ReturnType<typeof setTimeout> | null = null;
|
||||
let showTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
let hideTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
const [visible, setVisible] = createSignal(!!untrack(() => props.open));
|
||||
|
||||
const onVisibleChange = (details: { open: boolean }) => {
|
||||
|
|
@ -56,22 +45,8 @@ export const Dropdown = (props: Props) => {
|
|||
|
||||
onCleanup(() => {
|
||||
setVisible(false);
|
||||
if (timer) clearTimeout(timer);
|
||||
if (showTimer) clearTimeout(showTimer);
|
||||
if (hideTimer) clearTimeout(hideTimer);
|
||||
});
|
||||
|
||||
const handleOverlayClick = () => {
|
||||
if (!props.autoClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
timer = setTimeout(() => onVisibleChange({ open: false }), TIMEOUT_HIDE_TOOLTIP);
|
||||
};
|
||||
|
||||
// Ark UI uses floating-ui placement tokens. corvu collapsed every position
|
||||
// to plain `bottom`/`top` (ignoring left/right), so mapping `bottomRight` ->
|
||||
// `bottom-end` is an intentional behavior change that honors the `position`
|
||||
|
|
@ -94,27 +69,6 @@ export const Dropdown = (props: Props) => {
|
|||
}
|
||||
};
|
||||
|
||||
const scheduleHide = () => {
|
||||
if (showTimer) {
|
||||
clearTimeout(showTimer);
|
||||
showTimer = null;
|
||||
}
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer);
|
||||
}
|
||||
hideTimer = setTimeout(() => onVisibleChange({ open: false }), HIDE_DELAY);
|
||||
};
|
||||
|
||||
const cancelHideAndShow = () => {
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer);
|
||||
hideTimer = null;
|
||||
}
|
||||
if (!visible()) {
|
||||
onVisibleChange({ open: true });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root
|
||||
open={visible()}
|
||||
|
|
@ -135,46 +89,30 @@ export const Dropdown = (props: Props) => {
|
|||
[s.open]: props.flex,
|
||||
[s.disabled]: props.disabled,
|
||||
},
|
||||
visible() && props.openClass ? props.openClass : null,
|
||||
props.wrapClass,
|
||||
)}
|
||||
>
|
||||
{props.trigger === 'click' ? (
|
||||
// Click mode MUST use Popover.Trigger: it tags the element with
|
||||
// `data-part="trigger"` so Ark UI excludes it from outside-click
|
||||
// detection. A plain div would race (outside-click closes, then the
|
||||
// click reopens). Ark UI has no `as` prop — use `asChild`.
|
||||
<Popover.Trigger
|
||||
asChild={(triggerProps) => (
|
||||
<div
|
||||
{...triggerProps}
|
||||
class={cn(props.childrenClass, {
|
||||
[s.wrapper]: props.flexWrapper,
|
||||
})}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
// Hover mode: a plain div (no Trigger) so a click does NOT toggle.
|
||||
// Enter/leave go through onVisibleChange so `disabled` still guards.
|
||||
<div
|
||||
class={cn(props.childrenClass, {
|
||||
[s.wrapper]: props.flexWrapper,
|
||||
})}
|
||||
onMouseEnter={() => cancelHideAndShow()}
|
||||
onMouseLeave={() => scheduleHide()}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)}
|
||||
<Show when={!props.noIcon}>
|
||||
<Icon
|
||||
class={cn(s.arrow, props.iconClass, { [s.active]: visible() })}
|
||||
icon="arrow_bottom"
|
||||
/>
|
||||
</Show>
|
||||
<Popover.Trigger
|
||||
asChild={(triggerProps) => (
|
||||
<div
|
||||
{...triggerProps}
|
||||
class={cn(s.trigger, props.childrenClass, {
|
||||
[s.flexWrapper]: props.flexWrapper,
|
||||
})}
|
||||
>
|
||||
{props.children}
|
||||
<Show when={!props.noIcon}>
|
||||
<Icon
|
||||
aria-hidden="true"
|
||||
class={cn(s.arrow, props.iconClass, {
|
||||
[s.active]: visible(),
|
||||
})}
|
||||
icon="arrow_bottom"
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</Popover.Anchor>
|
||||
<Popover.Positioner>
|
||||
|
|
@ -183,9 +121,6 @@ export const Dropdown = (props: Props) => {
|
|||
[s.widthAuto]: props.widthAuto,
|
||||
[s.selectOverlay]: props.isSelect,
|
||||
})}
|
||||
onClick={handleOverlayClick}
|
||||
onMouseEnter={() => cancelHideAndShow()}
|
||||
onMouseLeave={() => scheduleHide()}
|
||||
>
|
||||
{props.menu}
|
||||
</Popover.Content>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,7 @@
|
|||
import { type JSX } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { useIsMobile } from 'panel/hooks/useIsMobile';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
|
||||
|
|
@ -18,16 +17,12 @@ type Props = {
|
|||
};
|
||||
|
||||
export const FaqTooltip = (props: Props) => {
|
||||
const isMobile = useIsMobile();
|
||||
|
||||
const currentPosition = () => (isMobile() ? 'bottom' : 'bottomLeft');
|
||||
const position = () => props.position ?? currentPosition();
|
||||
const position = () => props.position ?? 'bottomLeft';
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger={isMobile() ? 'click' : 'hover'}
|
||||
<Tooltip
|
||||
overlayClass={cn(s.overlay_mobile, props.overlayClass)}
|
||||
menu={
|
||||
content={
|
||||
<div
|
||||
class={cn(theme.dropdown.menu, s.menu, props.menuClass, {
|
||||
[s.menu_large]: props.menuSize === 'large',
|
||||
|
|
@ -39,11 +34,10 @@ export const FaqTooltip = (props: Props) => {
|
|||
}
|
||||
class={s.dropdown}
|
||||
position={position() as any}
|
||||
noIcon
|
||||
>
|
||||
<div class={s.trigger} onPointerDown={(e: PointerEvent) => e.stopPropagation()}>
|
||||
<Icon icon="faq" class={s.icon} />
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -83,20 +83,25 @@ export const Footer = () => {
|
|||
value: dashboardState.dnsVersion,
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class={cn(s.checkUpdateBtn, {
|
||||
[s.checkUpdateBtn_loading]: dashboardState.processingVersion,
|
||||
})}
|
||||
aria-label={intl.getMessage('check_updates_btn')}
|
||||
disabled={dashboardState.processingVersion}
|
||||
data-testid="footer-check-updates"
|
||||
onClick={() => getVersion(true)}
|
||||
>
|
||||
<Icon
|
||||
icon={dashboardState.processingVersion ? 'loader' : 'refresh'}
|
||||
/>
|
||||
</button>
|
||||
<Show when={dashboardState.checkUpdateFlag}>
|
||||
<button
|
||||
type="button"
|
||||
class={cn(s.checkUpdateBtn, {
|
||||
[s.checkUpdateBtn_loading]:
|
||||
dashboardState.processingVersion,
|
||||
})}
|
||||
aria-label={intl.getMessage('check_updates_btn')}
|
||||
disabled={dashboardState.processingVersion}
|
||||
data-testid="footer-check-updates"
|
||||
onClick={() => getVersion(true)}
|
||||
>
|
||||
<Icon
|
||||
icon={
|
||||
dashboardState.processingVersion ? 'loader' : 'refresh'
|
||||
}
|
||||
/>
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
@ -118,7 +123,6 @@ export const Footer = () => {
|
|||
|
||||
<div class={s.dropdownWrapper}>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
open={themeDropdownOpen()}
|
||||
onOpenChange={setThemeDropdownOpen}
|
||||
menu={
|
||||
|
|
|
|||
|
|
@ -872,6 +872,7 @@ export const Icons = () => (
|
|||
id="connections"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="1.5"
|
||||
|
|
@ -884,7 +885,7 @@ export const Icons = () => (
|
|||
<path d="m5.5 17h13" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="adblocking" viewBox="0 0 24 24" fill="none">
|
||||
<symbol id="adblocking" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
|
|
@ -895,7 +896,7 @@ export const Icons = () => (
|
|||
<path d="M4 21L20 3" stroke-width="1.5" stroke-linecap="round" />
|
||||
</symbol>
|
||||
|
||||
<symbol id="parental" viewBox="0 0 24 24" fill="none">
|
||||
<symbol id="parental" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle
|
||||
cx="6.76196"
|
||||
cy="17.2381"
|
||||
|
|
@ -927,7 +928,7 @@ export const Icons = () => (
|
|||
/>
|
||||
</symbol>
|
||||
|
||||
<symbol id="time" viewBox="0 0 24 24" fill="none">
|
||||
<symbol id="time" viewBox="0 0 24 24" fill="none" stroke="currentColor">
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ export const LanguageDropdown = (props: LanguageDropdownProps) => {
|
|||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
open={open()}
|
||||
onOpenChange={setOpen}
|
||||
menu={
|
||||
|
|
|
|||
|
|
@ -50,16 +50,16 @@
|
|||
|
||||
.tableHeaderCell {
|
||||
@media (min-width: 768px) {
|
||||
min-height: 48px;
|
||||
padding: 12px;
|
||||
min-height: 42px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tableBodyCell {
|
||||
@media (min-width: 768px) {
|
||||
min-height: 56px;
|
||||
padding: 12px;
|
||||
align-items: flex-start;
|
||||
min-height: 42px;
|
||||
padding: 8px 12px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -147,7 +147,6 @@ export const Pagination = (props: Props) => {
|
|||
|
||||
<div class={theme.pagination.limitContainer}>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
menu={limitMenu}
|
||||
open={limitMenuOpen()}
|
||||
|
|
|
|||
11
client_v2/src/common/ui/Tooltip/Tooltip.module.pcss
Normal file
11
client_v2/src/common/ui/Tooltip/Tooltip.module.pcss
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
.wrapper {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
background: var(--default-dropdown-menu-background);
|
||||
max-width: 300px;
|
||||
z-index: 1504;
|
||||
cursor: default;
|
||||
}
|
||||
17
client_v2/src/common/ui/Tooltip/Tooltip.pcss
Normal file
17
client_v2/src/common/ui/Tooltip/Tooltip.pcss
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
[data-scope='tooltip'][data-part='trigger'] {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
[data-scope='tooltip'][data-part='positioner'] {
|
||||
z-index: 1504;
|
||||
}
|
||||
|
||||
[data-scope='tooltip'][data-part='content'] {
|
||||
border-radius: 8px;
|
||||
box-shadow:
|
||||
0 8px 18px -6px var(--dropdown-box-shadow),
|
||||
0 12px 42px -4px var(--dropdown-box-shadow);
|
||||
outline: none;
|
||||
}
|
||||
120
client_v2/src/common/ui/Tooltip/Tooltip.tsx
Normal file
120
client_v2/src/common/ui/Tooltip/Tooltip.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import { type JSX, Show, createMemo, createSignal } from 'solid-js';
|
||||
import cn from 'clsx';
|
||||
import { Tooltip as ArkTooltip } from '@ark-ui/solid';
|
||||
|
||||
import { useMediaQuery } from 'panel/helpers/useMediaQuery';
|
||||
import { useOutsideDismiss } from 'panel/hooks/useOutsideDismiss';
|
||||
|
||||
import './Tooltip.pcss';
|
||||
import s from './Tooltip.module.pcss';
|
||||
|
||||
const SHOW_DELAY = 200;
|
||||
const HIDE_DELAY = 300;
|
||||
|
||||
type Props = {
|
||||
content: JSX.Element;
|
||||
position?: 'bottomRight' | 'bottomLeft' | 'topRight' | 'topLeft' | 'top';
|
||||
overlayClass?: string;
|
||||
class?: string;
|
||||
disabled?: boolean;
|
||||
children?: JSX.Element;
|
||||
};
|
||||
|
||||
export const Tooltip = (props: Props) => {
|
||||
const placement = createMemo(
|
||||
(): 'top-end' | 'top-start' | 'bottom-end' | 'bottom-start' | 'top' => {
|
||||
switch (props.position) {
|
||||
case 'topRight':
|
||||
return 'top-end';
|
||||
case 'topLeft':
|
||||
return 'top-start';
|
||||
case 'bottomRight':
|
||||
return 'bottom-end';
|
||||
case 'bottomLeft':
|
||||
return 'bottom-start';
|
||||
case 'top':
|
||||
return 'top';
|
||||
default:
|
||||
return 'bottom-end';
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const positioning = createMemo(() => ({
|
||||
placement: placement(),
|
||||
gutter: 4,
|
||||
flip: true,
|
||||
}));
|
||||
|
||||
// Touch-device detection via hover-media query.
|
||||
const isTouch = useMediaQuery('(hover: none)');
|
||||
|
||||
// Managed open-state: hover-driven on desktop, tap-driven on touch.
|
||||
const [hoverOpen, setHoverOpen] = createSignal(false);
|
||||
const [tapOpen, setTapOpen] = createSignal(false);
|
||||
const open = () => hoverOpen() || tapOpen();
|
||||
|
||||
let triggerRef: HTMLDivElement | undefined;
|
||||
let contentRef: HTMLDivElement | undefined;
|
||||
|
||||
// On touch devices: tapping outside an open tooltip closes it.
|
||||
useOutsideDismiss(
|
||||
() => isTouch() && tapOpen(),
|
||||
() => setTapOpen(false),
|
||||
triggerRef,
|
||||
contentRef,
|
||||
);
|
||||
|
||||
// Zag.js Tooltip's "disabled" prop only closes an already-open tooltip
|
||||
// when disabled changes to true; it does not prevent opening on hover.
|
||||
// Render children without the tooltip wrapper when disabled.
|
||||
return (
|
||||
<Show
|
||||
when={!props.disabled}
|
||||
fallback={<div class={cn(props.class, s.wrapper)}>{props.children}</div>}
|
||||
>
|
||||
<ArkTooltip.Root
|
||||
open={open()}
|
||||
onOpenChange={(details) => setHoverOpen(details.open)}
|
||||
openDelay={SHOW_DELAY}
|
||||
closeDelay={HIDE_DELAY}
|
||||
interactive
|
||||
positioning={positioning()}
|
||||
closeOnClick={false}
|
||||
closeOnPointerDown={false}
|
||||
closeOnScroll={false}
|
||||
closeOnEscape={false}
|
||||
>
|
||||
<ArkTooltip.Trigger
|
||||
asChild={(triggerProps) => (
|
||||
<div
|
||||
{...triggerProps}
|
||||
ref={(el) => {
|
||||
triggerRef = el;
|
||||
}}
|
||||
class={cn(props.class, s.wrapper)}
|
||||
onClick={() => {
|
||||
if (isTouch()) {
|
||||
setTapOpen((v) => !v);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
<ArkTooltip.Positioner>
|
||||
<ArkTooltip.Content class={cn(s.overlay, props.overlayClass)}>
|
||||
<div
|
||||
ref={(el) => {
|
||||
contentRef = el;
|
||||
}}
|
||||
>
|
||||
{props.content}
|
||||
</div>
|
||||
</ArkTooltip.Content>
|
||||
</ArkTooltip.Positioner>
|
||||
</ArkTooltip.Root>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
1
client_v2/src/common/ui/Tooltip/index.ts
Normal file
1
client_v2/src/common/ui/Tooltip/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export { Tooltip } from './Tooltip';
|
||||
|
|
@ -53,8 +53,12 @@ export const ScheduleRow = (props: Props) => {
|
|||
|
||||
return (
|
||||
<div class={s.scheduleRow} data-testid={`schedule-row-${props.day}`}>
|
||||
<div class={s.dayName} data-testid={`schedule-row-${props.day}-day`}>{dayName()}</div>
|
||||
<div class={s.timeDisplay} data-testid={`schedule-row-${props.day}-time`}>{getTimeDisplay()}</div>
|
||||
<div class={s.dayName} data-testid={`schedule-row-${props.day}-day`}>
|
||||
{dayName()}
|
||||
</div>
|
||||
<div class={s.timeDisplay} data-testid={`schedule-row-${props.day}-time`}>
|
||||
{getTimeDisplay()}
|
||||
</div>
|
||||
<Show
|
||||
when={isConfigured()}
|
||||
fallback={
|
||||
|
|
@ -98,7 +102,6 @@ export const ScheduleRow = (props: Props) => {
|
|||
</div>
|
||||
<div class={cn(s.actions, s.actionsMobile)}>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
noIcon
|
||||
open={dropdownOpen()}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import type { Client, NormalizedTopClients } from 'panel/initialState';
|
|||
import { LOCAL_STORAGE_KEYS, LocalStorageHelper } from 'panel/helpers/localStorageHelper';
|
||||
import { Table, type TableColumn } from 'panel/common/ui/Table';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import { addSuccessToast } from 'panel/stores/toasts';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
||||
|
|
@ -73,11 +73,9 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
{firstId}
|
||||
</span>
|
||||
<Show when={hiddenCount > 0}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
noIcon
|
||||
<Tooltip
|
||||
overlayClass={s.idsTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={s.idsTooltip}>
|
||||
<For each={nonEmpty}>
|
||||
{(id) => (
|
||||
|
|
@ -104,7 +102,7 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
}
|
||||
>
|
||||
<span class={s.countLabel}>{hiddenCount}</span>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -125,11 +123,9 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
<span class={theme.table.cellLabel}>{intl.getMessage('name')}</span>
|
||||
|
||||
<div class={theme.table.cellValueText}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
noIcon
|
||||
<Tooltip
|
||||
overlayClass={s.nameTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={s.nameTooltip}>
|
||||
<span class={s.nameTooltipText}>{value}</span>
|
||||
<button
|
||||
|
|
@ -142,13 +138,12 @@ export const PersistentClientsTable = (props: Props) => {
|
|||
</button>
|
||||
</div>
|
||||
}
|
||||
class={s.nameDropdown}
|
||||
childrenClass={s.nameDropdownInner}
|
||||
class={s.nameDropdownInner}
|
||||
>
|
||||
<span class={cn(theme.common.textOverflow, s.nameTrigger)}>
|
||||
{value}
|
||||
</span>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { createMemo, Show, For } from 'solid-js';
|
||||
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import { decodeSvg } from 'panel/helpers/helpers';
|
||||
|
||||
import s from './PersistentClientsTable.module.pcss';
|
||||
|
|
@ -47,11 +47,9 @@ export const ServiceIcons = (props: ServiceIconsProps) => {
|
|||
</div>
|
||||
<Show when={hiddenCount() > 0}>
|
||||
<div class={s.countDropdown}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
noIcon
|
||||
<Tooltip
|
||||
overlayClass={s.servicesTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={s.servicesTooltip}>
|
||||
<div class={s.servicesTooltipGrid}>
|
||||
<For each={props.serviceIds}>
|
||||
|
|
@ -74,7 +72,7 @@ export const ServiceIcons = (props: ServiceIconsProps) => {
|
|||
}
|
||||
>
|
||||
<span class={s.countLabel}>{hiddenCount()}</span>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { createMemo, Show, For } from 'solid-js';
|
|||
import cn from 'clsx';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
|
||||
import s from './PersistentClientsTable.module.pcss';
|
||||
|
|
@ -28,11 +28,9 @@ export const TagsRow = (props: TagsRowProps) => {
|
|||
</span>
|
||||
<Show when={hiddenCount() > 0}>
|
||||
<div class={s.countDropdown}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
noIcon
|
||||
<Tooltip
|
||||
overlayClass={s.tagsTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={s.tagsTooltip}>
|
||||
<For each={props.tags}>
|
||||
{(tag) => <span class={s.tagsTooltipItem}>{tag}</span>}
|
||||
|
|
@ -53,7 +51,7 @@ export const TagsRow = (props: TagsRowProps) => {
|
|||
}
|
||||
>
|
||||
<span class={s.countLabel}>{hiddenCount()}</span>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,14 +25,14 @@
|
|||
|
||||
.rowDivider {
|
||||
display: none;
|
||||
padding: 16px;
|
||||
padding: 16px 16px 24px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
&:before {
|
||||
content: "";
|
||||
content: '';
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { EmptyState } from '../EmptyState';
|
|||
|
||||
import s from './GeneralStatistics.module.pcss';
|
||||
import { StatRow } from '../StatRow';
|
||||
import { formatCompactNumber } from 'panel/helpers/helpers';
|
||||
|
||||
type Props = {
|
||||
numDnsQueries: number;
|
||||
|
|
@ -38,7 +39,9 @@ export const GeneralStatistics = (props: Props) => {
|
|||
|
||||
<Show when={hasStats()}>
|
||||
<div class={cn(theme.text.t3, s.cardSubtitle)}>
|
||||
{intl.getPlural('queries_total', props.numDnsQueries)}
|
||||
{intl.getPlural('queries_total', props.numDnsQueries, {
|
||||
value: formatCompactNumber(props.numDnsQueries),
|
||||
})}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -172,7 +172,6 @@
|
|||
border: 0;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
color: var(--default-secondary-text);
|
||||
transition: var(--t2) background-color;
|
||||
|
||||
&:hover,
|
||||
|
|
@ -213,5 +212,5 @@
|
|||
|
||||
.cardSubtitle {
|
||||
font-size: 14px;
|
||||
color: var(--default-secondary-text);
|
||||
color: var(--default-description-text);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,14 +185,12 @@ export const Header = (props: Props) => {
|
|||
|
||||
<Dropdown
|
||||
menu={protectionMenu}
|
||||
trigger="click"
|
||||
position="bottomLeft"
|
||||
open={protectionMenuOpen()}
|
||||
onOpenChange={setProtectionMenuOpen}
|
||||
wrapClass={s.protectionMenuWrapper}
|
||||
disabled={!props.protectionEnabled}
|
||||
noIcon
|
||||
disableAnimation
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@
|
|||
}
|
||||
|
||||
.sortIcon {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
transition: transform var(--t2);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,10 @@
|
|||
}
|
||||
|
||||
.chartTooltip {
|
||||
position: fixed;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
opacity: 0;
|
||||
background: var(--default-cards-background);
|
||||
border: 1px solid var(--default-item-divider);
|
||||
border-radius: 8px;
|
||||
|
|
@ -135,6 +139,6 @@
|
|||
|
||||
.chartTooltipDate {
|
||||
font-size: 12px;
|
||||
color: var(--default-secondary-text);
|
||||
color: var(--default-description-text);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ export const StatCard = (props: StatCardProps) => {
|
|||
maintainAspectRatio: false,
|
||||
animation: false as const,
|
||||
layout: {
|
||||
padding: { bottom: 12 },
|
||||
padding: { top: 6, bottom: 12 },
|
||||
},
|
||||
plugins: {
|
||||
tooltip: {
|
||||
|
|
@ -179,23 +179,14 @@ export const StatCard = (props: StatCardProps) => {
|
|||
|
||||
<div class={cn(theme.text.t4, s.statCardLabel)}>
|
||||
<Show when={props.linkTo} fallback={props.label}>
|
||||
<Link to={props.linkTo!} query={props.query} class={s.statLabelLink}>
|
||||
<Link to={props.linkTo} query={props.query} class={s.statLabelLink}>
|
||||
{props.label}
|
||||
</Link>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class={s.statCardChart}>
|
||||
<div
|
||||
ref={setTooltipRef}
|
||||
class={s.chartTooltip}
|
||||
style={{
|
||||
position: 'fixed',
|
||||
'pointer-events': 'none',
|
||||
'z-index': '100',
|
||||
opacity: '0',
|
||||
}}
|
||||
/>
|
||||
<div ref={setTooltipRef} class={s.chartTooltip} />
|
||||
<canvas ref={setCanvasRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
flex-wrap: wrap;
|
||||
gap: 0 24px;
|
||||
padding: 16px;
|
||||
min-height: 40px;
|
||||
transition: background-color var(--t2);
|
||||
|
||||
@media (min-width: 768px) {
|
||||
|
|
@ -14,17 +15,14 @@
|
|||
|
||||
&.dnsQueries {
|
||||
& .tableRowIcon {
|
||||
stroke: var(--default-gray-icons);
|
||||
color: var(--default-gray-icons);
|
||||
}
|
||||
}
|
||||
|
||||
&.adsBlocked {
|
||||
& .queryCount {
|
||||
color: var(--default-error-link);
|
||||
}
|
||||
|
||||
& .queryCount,
|
||||
& .tableRowIcon {
|
||||
stroke: var(--default-error-icon);
|
||||
color: var(--default-error-icon);
|
||||
}
|
||||
|
||||
& .queryBarFill {
|
||||
|
|
@ -33,12 +31,9 @@
|
|||
}
|
||||
|
||||
&.threatsBlocked {
|
||||
& .queryCount {
|
||||
color: var(--default-attention-icon-stroke);
|
||||
}
|
||||
|
||||
& .queryCount,
|
||||
& .tableRowIcon {
|
||||
stroke: var(--default-attention-icon-stroke);
|
||||
color: var(--default-attention-icon-stroke);
|
||||
}
|
||||
|
||||
& .queryBarFill {
|
||||
|
|
@ -47,12 +42,9 @@
|
|||
}
|
||||
|
||||
&.adultWebsitesBlocked {
|
||||
& .queryCount {
|
||||
color: var(--default-product-secondary-icon-stroke);
|
||||
}
|
||||
|
||||
& .queryCount,
|
||||
& .tableRowIcon {
|
||||
stroke: var(--default-product-secondary-icon-stroke);
|
||||
color: var(--default-product-secondary-icon-stroke);
|
||||
}
|
||||
|
||||
& .queryBarFill {
|
||||
|
|
@ -61,12 +53,9 @@
|
|||
}
|
||||
|
||||
&.safeSearchUsed {
|
||||
& .queryCount {
|
||||
color: var(--default-product-icon);
|
||||
}
|
||||
|
||||
& .queryCount,
|
||||
& .tableRowIcon {
|
||||
stroke: var(--default-product-icon);
|
||||
color: var(--default-product-icon);
|
||||
}
|
||||
|
||||
& .queryBarFill {
|
||||
|
|
@ -75,6 +64,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.statRowDropdown {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.statRowLeft {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -84,7 +79,7 @@
|
|||
.statRowIcon {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--default-secondary-text);
|
||||
color: var(--default-description-text);
|
||||
}
|
||||
|
||||
.statRowLabel {
|
||||
|
|
@ -104,7 +99,7 @@
|
|||
|
||||
.statRowPercent {
|
||||
font-size: 12px;
|
||||
color: var(--default-secondary-text);
|
||||
color: var(--default-description-text);
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
|
|
@ -159,6 +154,8 @@
|
|||
}
|
||||
|
||||
.queryTooltip {
|
||||
font-size: var(--fs-text-t3);
|
||||
line-height: var(--lh-t3-normal);
|
||||
padding: 8px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { Show } from 'solid-js';
|
|||
import intl from 'panel/common/intl';
|
||||
import { Icon, type IconType } from 'panel/common/ui/Icon';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import cn from 'clsx';
|
||||
import { formatCompactNumber, formatNumber } from 'panel/helpers/helpers';
|
||||
|
||||
|
|
@ -29,21 +29,23 @@ export type StatRowProps = {
|
|||
export const StatRow = (props: StatRowProps) => {
|
||||
const isQueriesValue = () => props.isQueriesValue !== false;
|
||||
|
||||
const formattedValue = () =>
|
||||
typeof props.value === 'number' ? formatNumber(props.value) : props.value;
|
||||
|
||||
return (
|
||||
<div class={cn(s.statRow, s[props.rowTheme])}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
position="bottomLeft"
|
||||
noIcon
|
||||
disableAnimation
|
||||
overlayClass={s.queryTooltipOverlay}
|
||||
menu={<div class={s.statTooltip}>{props.tooltip}</div>}
|
||||
>
|
||||
<div class={cn(theme.text.t3, theme.text.condenced, s.statRowLeft)}>
|
||||
<Icon icon={props.icon} class={s.tableRowIcon} />
|
||||
{props.label}
|
||||
</div>
|
||||
</Dropdown>
|
||||
<div class={s.statRowDropdown}>
|
||||
<Tooltip
|
||||
position="bottomLeft"
|
||||
overlayClass={s.queryTooltipOverlay}
|
||||
content={<div class={cn(theme.text.t3, s.statTooltip)}>{props.tooltip}</div>}
|
||||
>
|
||||
<div class={cn(theme.text.t3, theme.text.condenced, s.statRowLeft)}>
|
||||
<Icon icon={props.icon} class={s.tableRowIcon} />
|
||||
{props.label}
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class={s.statRowValue}>
|
||||
<Show
|
||||
|
|
@ -55,18 +57,14 @@ export const StatRow = (props: StatRowProps) => {
|
|||
}
|
||||
>
|
||||
<div class={s.dropdownWrapper}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
<Tooltip
|
||||
position="top"
|
||||
noIcon
|
||||
disableAnimation
|
||||
overlayClass={s.queryTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={s.queryTooltip}>
|
||||
{typeof props.value === 'number'
|
||||
? formatNumber(props.value)
|
||||
: props.value}{' '}
|
||||
{intl.getMessage('queries').toLowerCase()}
|
||||
{intl.getMessage('queries_tooltip', {
|
||||
value: formattedValue(),
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -95,7 +93,7 @@ export const StatRow = (props: StatRowProps) => {
|
|||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@
|
|||
}
|
||||
|
||||
.cardSubtitle {
|
||||
white-space: nowrap;
|
||||
color: var(--default-description-text);
|
||||
}
|
||||
|
||||
|
|
@ -46,12 +47,19 @@
|
|||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
min-height: 40px;
|
||||
gap: 0 24px;
|
||||
padding: 16px 24px 24px;
|
||||
transition: background-color var(--t2);
|
||||
border-bottom: 1px solid var(--default-item-divider);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--hovered-page-background);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
border-radius: 8px;
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
}
|
||||
|
|
@ -148,6 +156,8 @@
|
|||
}
|
||||
|
||||
.queryTooltip {
|
||||
font-size: var(--fs-text-t3);
|
||||
line-height: var(--lh-t3-normal);
|
||||
padding: 8px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { useIsDesktop } from 'panel/helpers/useMediaQuery';
|
|||
import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
|
||||
import intl from 'panel/common/intl';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import { formatNumber, formatCompactNumber } from 'panel/helpers/helpers';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { getTrackerData } from 'panel/helpers/trackers/trackers';
|
||||
|
|
@ -90,30 +90,28 @@ export const TopBlockedDomains = (props: Props) => {
|
|||
when={trackerData}
|
||||
fallback={<div class={s.tableRowDot} />}
|
||||
>
|
||||
<Dropdown
|
||||
menu={<TrackerTooltip trackerData={trackerData!} />}
|
||||
trigger="hover"
|
||||
<Tooltip
|
||||
content={
|
||||
<TrackerTooltip trackerData={trackerData!} />
|
||||
}
|
||||
position="bottomLeft"
|
||||
noIcon
|
||||
>
|
||||
<Icon icon="eye_open" class={s.tableRowIcon} />
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<span class={s.domainName}>{domain.name}</span>
|
||||
</div>
|
||||
|
||||
<div class={s.tableRowRight}>
|
||||
<div class={s.dropdowWrapper}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
<Tooltip
|
||||
position="top"
|
||||
noIcon
|
||||
disableAnimation
|
||||
overlayClass={s.queryTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={s.queryTooltip}>
|
||||
{formatNumber(domain.count)}{' '}
|
||||
{intl.getMessage('queries').toLowerCase()}
|
||||
{intl.getMessage('queries_tooltip', {
|
||||
value: formatNumber(domain.count),
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -136,7 +134,7 @@ export const TopBlockedDomains = (props: Props) => {
|
|||
({percent().toFixed(2)}%)
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class={s.queryBar}>
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
}
|
||||
|
||||
.cardSubtitle {
|
||||
white-space: nowrap;
|
||||
color: var(--default-description-text);
|
||||
}
|
||||
|
||||
|
|
@ -116,6 +117,8 @@
|
|||
}
|
||||
|
||||
.queryTooltip {
|
||||
font-size: var(--fs-text-t3);
|
||||
line-height: var(--lh-t3-normal);
|
||||
padding: 8px 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
@ -125,14 +128,27 @@
|
|||
}
|
||||
|
||||
.protectionMenu {
|
||||
padding: 8px 0;
|
||||
min-width: 200px;
|
||||
}
|
||||
|
||||
.protectionMenuItem {
|
||||
padding: 8px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: 56px;
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
color: var(--default-link);
|
||||
transition: background-color var(--t2);
|
||||
|
||||
&:hover,
|
||||
&:focus-visible {
|
||||
background-color: var(--hovered-dropdown-menu-background);
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--pressed-dropdown-menu-background);
|
||||
}
|
||||
}
|
||||
|
||||
.protectionMenuItemRed {
|
||||
|
|
@ -148,13 +164,15 @@
|
|||
padding: 16px 24px 24px;
|
||||
transition: background-color var(--t2);
|
||||
border-bottom: 1px solid var(--default-item-divider);
|
||||
border-radius: 4px;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--hovered-page-background);
|
||||
}
|
||||
|
||||
|
|
@ -202,15 +220,16 @@
|
|||
}
|
||||
|
||||
.actionButton {
|
||||
padding: 4px;
|
||||
padding: 0;
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
color: var(--default-secondary-text);
|
||||
color: var(--default-description-text);
|
||||
border-radius: 4px;
|
||||
transition: background-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: var(--hovered-secondary-button);
|
||||
}
|
||||
}
|
||||
|
|
@ -262,6 +281,7 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
padding: 0 24px;
|
||||
border: 1px solid var(--default-link);
|
||||
border-radius: 8px;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
|
|||
|
||||
import intl from 'panel/common/intl';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
|
||||
import { formatNumber, formatCompactNumber } from 'panel/helpers/helpers';
|
||||
|
|
@ -124,7 +125,12 @@ export const TopClients = (props: Props) => {
|
|||
}
|
||||
>
|
||||
<div
|
||||
class={cn(theme.text.t2, theme.text.condenced, s.protectionMenuItem)}
|
||||
class={cn(
|
||||
theme.text.t2,
|
||||
theme.text.condenced,
|
||||
theme.dropdown.item,
|
||||
s.protectionMenuItem,
|
||||
)}
|
||||
onClick={() => openConfirmDialog(client.name, 'unblock')}
|
||||
>
|
||||
{intl.getMessage('unblock_client')}
|
||||
|
|
@ -185,66 +191,69 @@ export const TopClients = (props: Props) => {
|
|||
</div>
|
||||
|
||||
<div class={s.tableRowRight}>
|
||||
<div class={s.dropdowWrapper}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
position="top"
|
||||
noIcon
|
||||
disableAnimation
|
||||
overlayClass={s.queryTooltipOverlay}
|
||||
wrapClass={s.queryTooltipWrap}
|
||||
menu={
|
||||
<div class={s.queryTooltip}>
|
||||
{formatNumber(client.count)}{' '}
|
||||
{intl.getMessage('queries').toLowerCase()}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
class={cn(
|
||||
theme.text.t3,
|
||||
theme.text.condenced,
|
||||
s.queryCount,
|
||||
s.queryCountHover,
|
||||
)}
|
||||
<Show when={isDesktop()}>
|
||||
<div class={s.dropdowWrapper}>
|
||||
<Tooltip
|
||||
position="top"
|
||||
overlayClass={s.queryTooltipOverlay}
|
||||
content={
|
||||
<div class={s.queryTooltip}>
|
||||
{intl.getMessage('queries_tooltip', {
|
||||
value: formatNumber(client.count),
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{formatCompactNumber(client.count)}
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
theme.text.t3,
|
||||
theme.text.condenced,
|
||||
s.queryPercent,
|
||||
s.queryCount,
|
||||
s.queryCountHover,
|
||||
)}
|
||||
>
|
||||
({percent().toFixed(1)}%)
|
||||
{formatCompactNumber(client.count)}
|
||||
|
||||
<div
|
||||
class={cn(
|
||||
theme.text.t3,
|
||||
theme.text.condenced,
|
||||
s.queryPercent,
|
||||
)}
|
||||
>
|
||||
({percent().toFixed(1)}%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isDesktop()}>
|
||||
<div class={s.queryBar}>
|
||||
<div
|
||||
class={s.queryBarFill}
|
||||
style={{ width: `${percent()}%` }}
|
||||
/>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<div class={s.dropdownWrapper}>
|
||||
<Dropdown
|
||||
wrapClass={s.clientActionsDropdown}
|
||||
menu={getClientMenu(client)}
|
||||
position="bottomRight"
|
||||
noIcon
|
||||
open={openMenuClient() === client.name}
|
||||
onOpenChange={(isOpen: boolean) =>
|
||||
setOpenMenuClient(isOpen ? client.name : null)
|
||||
}
|
||||
>
|
||||
<button type="button" class={s.actionButton}>
|
||||
<Icon icon="bullets" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<div class={s.queryBar}>
|
||||
<div
|
||||
class={s.queryBarFill}
|
||||
style={{ width: `${percent()}%` }}
|
||||
/>
|
||||
</div>
|
||||
<Dropdown
|
||||
wrapClass={s.clientActionsDropdown}
|
||||
menu={getClientMenu(client)}
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
noIcon
|
||||
open={openMenuClient() === client.name}
|
||||
onOpenChange={(isOpen: boolean) =>
|
||||
setOpenMenuClient(isOpen ? client.name : null)
|
||||
}
|
||||
>
|
||||
<button type="button" class={s.actionButton}>
|
||||
<Icon icon="bullets" />
|
||||
</button>
|
||||
</Dropdown>
|
||||
|
||||
<Show when={isBlocked}>
|
||||
<div
|
||||
class={cn(
|
||||
|
|
@ -267,7 +276,7 @@ export const TopClients = (props: Props) => {
|
|||
s.clientName,
|
||||
)}
|
||||
>
|
||||
{client.info!.name}
|
||||
{client.info.name}
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={isBlocked}>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { useIsDesktop } from 'panel/helpers/useMediaQuery';
|
|||
import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
|
||||
import intl from 'panel/common/intl';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import { formatNumber, formatCompactNumber } from 'panel/helpers/helpers';
|
||||
import { getTrackerData } from 'panel/helpers/trackers/trackers';
|
||||
import theme from 'panel/lib/theme';
|
||||
|
|
@ -82,30 +82,28 @@ export const TopQueriedDomains = (props: Props) => {
|
|||
when={trackerData}
|
||||
fallback={<div class={s.tableRowDot} />}
|
||||
>
|
||||
<Dropdown
|
||||
menu={<TrackerTooltip trackerData={trackerData!} />}
|
||||
trigger="hover"
|
||||
<Tooltip
|
||||
content={
|
||||
<TrackerTooltip trackerData={trackerData!} />
|
||||
}
|
||||
position="bottomLeft"
|
||||
noIcon
|
||||
>
|
||||
<Icon icon="eye_open" class={s.tableRowIcon} />
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
<span class={s.domainName}>{domain.name}</span>
|
||||
</div>
|
||||
|
||||
<div class={s.tableRowRight}>
|
||||
<div class={s.dropdowWrapper}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
<Tooltip
|
||||
position="top"
|
||||
noIcon
|
||||
disableAnimation
|
||||
overlayClass={s.queryTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={s.queryTooltip}>
|
||||
{formatNumber(domain.count)}{' '}
|
||||
{intl.getMessage('queries').toLowerCase()}
|
||||
{intl.getMessage('queries_tooltip', {
|
||||
value: formatNumber(domain.count),
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -128,7 +126,7 @@ export const TopQueriedDomains = (props: Props) => {
|
|||
({percent().toFixed(1)}%)
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class={s.queryBar}>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { MOBILE_TABLE_MAX_ROWS } from 'panel/helpers/constants';
|
|||
import intl from 'panel/common/intl';
|
||||
import { formatNumber, formatCompactNumber } from 'panel/helpers/helpers';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import cn from 'clsx';
|
||||
import { SortableTableHeader } from '../SortableTableHeader';
|
||||
import { EmptyState } from '../EmptyState';
|
||||
|
|
@ -79,16 +79,14 @@ export const TopUpstreams = (props: Props) => {
|
|||
|
||||
<div class={s.tableRowRight}>
|
||||
<div class={s.dropdowWrapper}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
<Tooltip
|
||||
position="top"
|
||||
noIcon
|
||||
disableAnimation
|
||||
overlayClass={s.queryTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={s.queryTooltip}>
|
||||
{formatNumber(upstream.count)}{' '}
|
||||
{intl.getMessage('queries').toLowerCase()}
|
||||
{intl.getMessage('queries_tooltip', {
|
||||
value: formatNumber(upstream.count),
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
|
|
@ -111,7 +109,7 @@ export const TopUpstreams = (props: Props) => {
|
|||
({percent().toFixed(2)}%)
|
||||
</div>
|
||||
</div>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div class={s.queryBar}>
|
||||
|
|
|
|||
|
|
@ -132,7 +132,6 @@ export const Dhcp = () => {
|
|||
{intl.getMessage('dhcp')}
|
||||
</h1>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
noIcon
|
||||
open={menuOpen()}
|
||||
|
|
|
|||
|
|
@ -218,7 +218,6 @@ export const DynamicLeasesTable = (props: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
noIcon
|
||||
open={openMenuId() === rowId}
|
||||
|
|
|
|||
|
|
@ -108,7 +108,6 @@ export const LeasesPage = () => {
|
|||
</h1>
|
||||
<div class={s.headerActions}>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
noIcon
|
||||
open={menuOpen()}
|
||||
|
|
|
|||
|
|
@ -190,7 +190,6 @@ export const StaticLeasesTable = (props: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
}
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
noIcon
|
||||
open={openMenuId() === rowId}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,11 @@
|
|||
import { untrack, type Accessor } from 'solid-js';
|
||||
|
||||
import { accessState, setAccessList } from 'panel/stores/access';
|
||||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { validateDomainsPerLine } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { COMMENT_LINE_TOKENS } from 'panel/helpers/constants';
|
||||
|
||||
type Props = {
|
||||
open: Accessor<boolean>;
|
||||
|
|
@ -18,7 +17,6 @@ export const DisallowedDomainsDialog = (props: Props) => {
|
|||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => accessState.blocked_hosts,
|
||||
{ validate: (v) => (v ? validateDomainsPerLine(v) || '' : '') },
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -49,6 +47,8 @@ export const DisallowedDomainsDialog = (props: Props) => {
|
|||
label={intl.getMessage('dns_disallowed_domains_label')}
|
||||
size="medium"
|
||||
errorMessage={field.error()}
|
||||
commentPrefixes={COMMENT_LINE_TOKENS}
|
||||
highlightComments
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { dnsConfigState, setDnsConfig } from 'panel/stores/dnsConfig';
|
|||
import intl from 'panel/common/intl';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import { Textarea } from 'panel/common/controls/Textarea';
|
||||
import { validateBootstrapDns } from 'panel/helpers/validators';
|
||||
import { validateUpstreams } from 'panel/helpers/validators';
|
||||
import { useField } from 'panel/hooks/useField';
|
||||
|
||||
import theme from 'panel/lib/theme';
|
||||
|
|
@ -18,7 +18,7 @@ export const BootstrapDnsDialog = (props: Props) => {
|
|||
const field = useField<string>(
|
||||
() => props.open(),
|
||||
() => dnsConfigState.bootstrap_dns,
|
||||
{ validate: (v) => (v ? validateBootstrapDns(v) || '' : '') },
|
||||
{ validate: (v) => (v ? validateUpstreams(v) || '' : '') },
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
@ -28,7 +28,6 @@ export const BootstrapDnsDialog = (props: Props) => {
|
|||
description={
|
||||
<>
|
||||
<p>{intl.getMessage('dns_bootstrap_dns_desc')}</p>
|
||||
<p>{intl.getMessage('dns_bootstrap_dns_desc_2')}</p>
|
||||
</>
|
||||
}
|
||||
onClose={props.onClose}
|
||||
|
|
@ -50,6 +49,7 @@ export const BootstrapDnsDialog = (props: Props) => {
|
|||
placeholder={intl.getMessage('dns_bootstrap_dns_placeholder')}
|
||||
errorMessage={field.error()}
|
||||
size="medium"
|
||||
highlightComments
|
||||
/>
|
||||
</div>
|
||||
</ConfigDialog>
|
||||
|
|
|
|||
|
|
@ -172,7 +172,6 @@ export const Encryption = () => {
|
|||
{intl.getMessage('dns_protocols_title')}
|
||||
</h1>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
position="bottomRight"
|
||||
noIcon
|
||||
open={menuOpen()}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export const ServerSettingsModal = (props: Props) => {
|
|||
const [portDoq, setPortDoq] = createSignal(0);
|
||||
const [errors, setErrors] = createSignal<Record<string, string>>({});
|
||||
|
||||
// Reset local state when modal opens
|
||||
createEffect(
|
||||
on(
|
||||
() => props.open,
|
||||
|
|
@ -47,8 +46,6 @@ export const ServerSettingsModal = (props: Props) => {
|
|||
|
||||
const hasErrors = () => Object.values(errors()).some(Boolean);
|
||||
|
||||
// --- Change handlers ---
|
||||
|
||||
const handleServerNameChange = (e: Event) => {
|
||||
setServerName((e.target as HTMLInputElement).value);
|
||||
clearError('server_name');
|
||||
|
|
@ -69,8 +66,6 @@ export const ServerSettingsModal = (props: Props) => {
|
|||
clearError('port_dns_over_quic');
|
||||
};
|
||||
|
||||
// --- Blur validators ---
|
||||
|
||||
const handleServerNameBlur = () => {
|
||||
const err = validateServerName(serverName());
|
||||
setErrors((prev) => {
|
||||
|
|
|
|||
|
|
@ -95,7 +95,6 @@ export const FiltersList = (props: Props) => {
|
|||
{name}
|
||||
</Checkbox>
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
menu={
|
||||
<div class={theme.dropdown.menu}>
|
||||
<a
|
||||
|
|
|
|||
|
|
@ -38,36 +38,6 @@
|
|||
color: var(--default-description-text);
|
||||
}
|
||||
|
||||
.cellNameToggleOuter {
|
||||
@media (max-width: 767px) {
|
||||
width: 100% !important;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.cellValueToggle {
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.cellNameLabel {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
text-align: left;
|
||||
word-break: break-word;
|
||||
overflow: hidden;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.nameDesktopOnly {
|
||||
@media (max-width: 767px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tableRewrites {
|
||||
@media (min-width: 768px) {
|
||||
grid-template-columns: 3fr 5fr 80px;
|
||||
|
|
|
|||
|
|
@ -80,24 +80,25 @@ export const ListsTable = (props: Props) => {
|
|||
|
||||
const columns = createMemo<TableColumn<Filter>[]>(() => [
|
||||
{
|
||||
key: 'enabled',
|
||||
key: 'name',
|
||||
header: {
|
||||
text: '',
|
||||
text: intl.getMessage('name_label'),
|
||||
className: s.headerCell,
|
||||
},
|
||||
accessor: 'enabled',
|
||||
sortable: false,
|
||||
width: 64,
|
||||
className: s.cellNameToggleOuter,
|
||||
render: (value: boolean, row: Filter) => {
|
||||
accessor: 'name',
|
||||
sortable: true,
|
||||
render: (value: string, row: Filter) => {
|
||||
const { name, url, enabled } = row;
|
||||
const id = `filter_${url}`;
|
||||
|
||||
return (
|
||||
<div class={theme.table.cell}>
|
||||
<span class={s.cellNameLabel}>{name}</span>
|
||||
<span class={theme.table.cellLabel}>
|
||||
{intl.getMessage('name_label')}
|
||||
</span>
|
||||
|
||||
<div class={s.cellValueToggle}>
|
||||
<div class={cn(theme.table.cellValueText, s.domainCellValue)}>
|
||||
<span class={theme.common.textOverflow}>{value}</span>
|
||||
<Switch
|
||||
id={id}
|
||||
checked={enabled}
|
||||
|
|
@ -109,25 +110,6 @@ export const ListsTable = (props: Props) => {
|
|||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
header: {
|
||||
text: intl.getMessage('name_label'),
|
||||
className: s.headerCell,
|
||||
},
|
||||
accessor: 'name',
|
||||
sortable: true,
|
||||
className: s.nameDesktopOnly,
|
||||
render: (value: string) => (
|
||||
<div class={theme.table.cell}>
|
||||
<span class={theme.table.cellLabel}>{intl.getMessage('name_label')}</span>
|
||||
|
||||
<div class={theme.table.cellValueText}>
|
||||
<span class={theme.common.textOverflow}>{value}</span>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'url',
|
||||
header: {
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ export const ActionsMenu = (props: Props) => {
|
|||
|
||||
return (
|
||||
<Dropdown
|
||||
trigger="click"
|
||||
menu={menu}
|
||||
open={open()}
|
||||
onOpenChange={setOpen}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { Show } from 'solid-js';
|
|||
import cn from 'clsx';
|
||||
|
||||
import intl from 'panel/common/intl';
|
||||
import { Dropdown } from 'panel/common/ui/Dropdown';
|
||||
import { Tooltip } from 'panel/common/ui/Tooltip';
|
||||
import theme from 'panel/lib/theme';
|
||||
import { Icon } from 'panel/common/ui/Icon';
|
||||
import { getProtocolName } from 'panel/components/QueryLog/helpers';
|
||||
|
|
@ -28,17 +28,15 @@ export const RequestCell = (props: Props) => {
|
|||
</span>
|
||||
|
||||
<div class={s.requestIcons}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
<Tooltip
|
||||
position="bottomLeft"
|
||||
overlayClass={s.iconTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={cn(theme.dropdown.menu, s.queryDetailsTooltipMenu)}>
|
||||
<QueryDetailsTooltipContent row={props.row} />
|
||||
</div>
|
||||
}
|
||||
childrenClass={s.iconTooltipTrigger}
|
||||
noIcon
|
||||
class={s.iconTooltipTrigger}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -53,23 +51,21 @@ export const RequestCell = (props: Props) => {
|
|||
class={s.requestIcon}
|
||||
/>
|
||||
</button>
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
|
||||
<Show when={props.row.answer_dnssec}>
|
||||
<Dropdown
|
||||
trigger="hover"
|
||||
<Tooltip
|
||||
position="bottomLeft"
|
||||
overlayClass={s.iconTooltipOverlay}
|
||||
menu={
|
||||
content={
|
||||
<div class={cn(theme.dropdown.menu, s.iconTooltipMenu)}>
|
||||
{intl.getMessage('validated_with_dnssec')}
|
||||
</div>
|
||||
}
|
||||
childrenClass={s.iconTooltipTrigger}
|
||||
noIcon
|
||||
class={s.iconTooltipTrigger}
|
||||
>
|
||||
<Icon icon="lock" color="green" class={s.requestIcon} />
|
||||
</Dropdown>
|
||||
</Tooltip>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { createSignal, createEffect, Show, on } from 'solid-js';
|
|||
import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import intl from 'panel/common/intl';
|
||||
import { formatIntervalText, resolveInterval } from 'panel/components/Settings/helpers';
|
||||
import {
|
||||
formatIntervalText,
|
||||
resolveInterval,
|
||||
buildQueryLogConfig,
|
||||
} from 'panel/components/Settings/helpers';
|
||||
import { setLogsConfig, queryLogsState } from 'panel/stores/queryLogs';
|
||||
|
||||
import { Form, FormValues } from './Form';
|
||||
|
|
@ -70,14 +74,7 @@ export const LogsConfig = (props: Props) => {
|
|||
setConfirmConfig({ interval: newInterval });
|
||||
} else {
|
||||
// Save with all required fields from current state
|
||||
setLogsConfig({
|
||||
enabled: queryLogsState.enabled,
|
||||
anonymize_client_ip: queryLogsState.anonymize_client_ip,
|
||||
ignored: queryLogsState.ignored,
|
||||
ignored_enabled: queryLogsState.ignored_enabled,
|
||||
interval: newInterval,
|
||||
customInterval: values.customInterval,
|
||||
});
|
||||
setLogsConfig(buildQueryLogConfig(queryLogsState, { interval: newInterval }));
|
||||
addSuccessToast(intl.getMessage('changes_saved_success'));
|
||||
props.onModalClose();
|
||||
}
|
||||
|
|
@ -86,14 +83,7 @@ export const LogsConfig = (props: Props) => {
|
|||
const handleConfirmDecrease = () => {
|
||||
const config = confirmConfig();
|
||||
if (config) {
|
||||
setLogsConfig({
|
||||
enabled: queryLogsState.enabled,
|
||||
anonymize_client_ip: queryLogsState.anonymize_client_ip,
|
||||
ignored: queryLogsState.ignored,
|
||||
ignored_enabled: queryLogsState.ignored_enabled,
|
||||
interval: config.interval,
|
||||
customInterval: formValues().customInterval,
|
||||
});
|
||||
setLogsConfig(buildQueryLogConfig(queryLogsState, { interval: config.interval }));
|
||||
addSuccessToast(intl.getMessage('changes_saved_success'));
|
||||
props.onModalClose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,12 @@ import { LogsConfig } from './LogsConfig';
|
|||
import { FiltersConfig } from './FiltersConfig';
|
||||
import { SafeSearchModal } from './SafeSearchModal';
|
||||
import { IgnoredDomainsModal } from './IgnoredDomainsModal';
|
||||
import { getRetentionSummary, getSafeSearchProviderTitle } from './helpers';
|
||||
import {
|
||||
getRetentionSummary,
|
||||
getSafeSearchProviderTitle,
|
||||
buildQueryLogConfig,
|
||||
buildStatsConfig,
|
||||
} from './helpers';
|
||||
|
||||
import s from './Settings.module.pcss';
|
||||
|
||||
|
|
@ -88,7 +93,7 @@ export const Settings = () => {
|
|||
};
|
||||
|
||||
const handleLogsIgnoredSave = (ignored: string[]) => {
|
||||
setLogsConfig({ ...queryLogsState, ignored }).then((result) => {
|
||||
setLogsConfig(buildQueryLogConfig(queryLogsState, { ignored })).then((result) => {
|
||||
if (result) {
|
||||
setLogsIgnoredModalOpen(false);
|
||||
addSuccessToast(intl.getMessage('changes_saved_success'));
|
||||
|
|
@ -103,7 +108,7 @@ export const Settings = () => {
|
|||
};
|
||||
|
||||
const handleStatsIgnoredSave = (ignored: string[]) => {
|
||||
setStatsConfig({ ...statsState, ignored }).then((result) => {
|
||||
setStatsConfig(buildStatsConfig(statsState, { ignored })).then((result) => {
|
||||
if (result) {
|
||||
setStatsIgnoredModalOpen(false);
|
||||
addSuccessToast(intl.getMessage('changes_saved_success'));
|
||||
|
|
@ -229,10 +234,9 @@ export const Settings = () => {
|
|||
align="center"
|
||||
checked={queryLogsState.enabled}
|
||||
onChange={(v) =>
|
||||
setLogsConfig({
|
||||
...queryLogsState,
|
||||
enabled: v,
|
||||
})
|
||||
setLogsConfig(
|
||||
buildQueryLogConfig(queryLogsState, { enabled: v }),
|
||||
)
|
||||
}
|
||||
inputClass={s.queryLogSwitch}
|
||||
/>
|
||||
|
|
@ -247,10 +251,11 @@ export const Settings = () => {
|
|||
checked={queryLogsState.anonymize_client_ip}
|
||||
disabled={!queryLogsState.enabled}
|
||||
onChange={(v) =>
|
||||
setLogsConfig({
|
||||
...queryLogsState,
|
||||
anonymize_client_ip: v,
|
||||
})
|
||||
setLogsConfig(
|
||||
buildQueryLogConfig(queryLogsState, {
|
||||
anonymize_client_ip: v,
|
||||
}),
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
|
|
@ -273,7 +278,11 @@ export const Settings = () => {
|
|||
divider
|
||||
disabled={!queryLogsState.enabled}
|
||||
onChange={(v) =>
|
||||
setLogsConfig({ ...queryLogsState, ignored_enabled: v })
|
||||
setLogsConfig(
|
||||
buildQueryLogConfig(queryLogsState, {
|
||||
ignored_enabled: v,
|
||||
}),
|
||||
)
|
||||
}
|
||||
onClick={() => setLogsIgnoredModalOpen(true)}
|
||||
/>
|
||||
|
|
@ -332,7 +341,9 @@ export const Settings = () => {
|
|||
s.statsTitle,
|
||||
)}
|
||||
checked={statsState.enabled}
|
||||
onChange={(v) => setStatsConfig({ ...statsState, enabled: v })}
|
||||
onChange={(v) =>
|
||||
setStatsConfig(buildStatsConfig(statsState, { enabled: v }))
|
||||
}
|
||||
/>
|
||||
|
||||
<SettingRow
|
||||
|
|
@ -354,7 +365,9 @@ export const Settings = () => {
|
|||
divider
|
||||
disabled={!statsState.enabled}
|
||||
onChange={(v) =>
|
||||
setStatsConfig({ ...statsState, ignored_enabled: v })
|
||||
setStatsConfig(
|
||||
buildStatsConfig(statsState, { ignored_enabled: v }),
|
||||
)
|
||||
}
|
||||
onClick={() => setStatsIgnoredModalOpen(true)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -3,7 +3,11 @@ import { createSignal, createEffect, Show, on } from 'solid-js';
|
|||
import { ConfirmDialog } from 'panel/common/ui/ConfirmDialog';
|
||||
import { ConfigDialog } from 'panel/common/ui/ConfigDialog';
|
||||
import intl from 'panel/common/intl';
|
||||
import { formatIntervalText, resolveInterval } from 'panel/components/Settings/helpers';
|
||||
import {
|
||||
formatIntervalText,
|
||||
resolveInterval,
|
||||
buildStatsConfig,
|
||||
} from 'panel/components/Settings/helpers';
|
||||
|
||||
import { setStatsConfig, statsState } from 'panel/stores/stats';
|
||||
|
||||
|
|
@ -71,13 +75,7 @@ export const StatsConfig = (props: Props) => {
|
|||
setConfirmConfig({ interval: newInterval });
|
||||
} else {
|
||||
// Save with all required fields from current state
|
||||
setStatsConfig({
|
||||
enabled: statsState.enabled,
|
||||
ignored: statsState.ignored,
|
||||
ignored_enabled: statsState.ignored_enabled,
|
||||
interval: newInterval,
|
||||
customInterval: values.customInterval,
|
||||
});
|
||||
setStatsConfig(buildStatsConfig(statsState, { interval: newInterval }));
|
||||
addSuccessToast(intl.getMessage('changes_saved_success'));
|
||||
props.onModalClose();
|
||||
}
|
||||
|
|
@ -86,13 +84,7 @@ export const StatsConfig = (props: Props) => {
|
|||
const handleConfirmDecrease = () => {
|
||||
const config = confirmConfig();
|
||||
if (config) {
|
||||
setStatsConfig({
|
||||
enabled: statsState.enabled,
|
||||
ignored: statsState.ignored,
|
||||
ignored_enabled: statsState.ignored_enabled,
|
||||
interval: config.interval,
|
||||
customInterval: formValues().customInterval,
|
||||
});
|
||||
setStatsConfig(buildStatsConfig(statsState, { interval: config.interval }));
|
||||
addSuccessToast(intl.getMessage('changes_saved_success'));
|
||||
props.onModalClose();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,3 +63,49 @@ const SAFESEARCH_TITLES = {
|
|||
export const getSafeSearchProviderTitle = (key: string) => {
|
||||
return SAFESEARCH_TITLES[key as keyof typeof SAFESEARCH_TITLES] ?? captitalizeWords(key);
|
||||
};
|
||||
|
||||
export type QueryLogConfig = {
|
||||
enabled: boolean;
|
||||
anonymize_client_ip: boolean;
|
||||
interval: number;
|
||||
ignored: string[];
|
||||
ignored_enabled: boolean;
|
||||
};
|
||||
|
||||
export type StatsConfig = {
|
||||
enabled: boolean;
|
||||
interval: number;
|
||||
ignored: string[];
|
||||
ignored_enabled: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a backend-ready query-log config payload from the queryLogs store
|
||||
* (or any object with the required fields), applying optional overrides.
|
||||
*/
|
||||
export const buildQueryLogConfig = (
|
||||
state: QueryLogConfig,
|
||||
overrides?: Partial<QueryLogConfig>,
|
||||
): QueryLogConfig => ({
|
||||
enabled: state.enabled,
|
||||
anonymize_client_ip: state.anonymize_client_ip,
|
||||
interval: state.interval,
|
||||
ignored: state.ignored,
|
||||
ignored_enabled: state.ignored_enabled,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
/**
|
||||
* Builds a backend-ready stats config payload from the stats store,
|
||||
* applying optional overrides. Strips runtime-only fields.
|
||||
*/
|
||||
export const buildStatsConfig = (
|
||||
state: StatsConfig,
|
||||
overrides?: Partial<StatsConfig>,
|
||||
): StatsConfig => ({
|
||||
enabled: state.enabled,
|
||||
interval: state.interval,
|
||||
ignored: state.ignored,
|
||||
ignored_enabled: state.ignored_enabled,
|
||||
...overrides,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -521,21 +521,6 @@ export const DHCP_VALUES_PLACEHOLDERS = {
|
|||
},
|
||||
};
|
||||
|
||||
export const DHCP_DESCRIPTION_PLACEHOLDERS = {
|
||||
ipv4: {
|
||||
gateway_ip: 'dhcp_form_gateway_input',
|
||||
subnet_mask: 'dhcp_form_subnet_input',
|
||||
range_start: 'dhcp_form_range_start',
|
||||
range_end: 'dhcp_form_range_end',
|
||||
lease_duration: 'dhcp_form_lease_input',
|
||||
},
|
||||
ipv6: {
|
||||
range_start: 'dhcp_form_range_start',
|
||||
range_end: 'dhcp_form_range_end',
|
||||
lease_duration: 'dhcp_form_lease_input',
|
||||
},
|
||||
};
|
||||
|
||||
export const TOAST_TRANSITION_TIMEOUT = 500;
|
||||
|
||||
export const TOAST_TYPES = {
|
||||
|
|
|
|||
|
|
@ -689,24 +689,6 @@ export const validateUpstreams = (value: string): string | undefined =>
|
|||
(line) => !R_UPSTREAM_COMMENT.test(line),
|
||||
);
|
||||
|
||||
/**
|
||||
* Validates bootstrap DNS server lines. Unlike {@link validateUpstreams},
|
||||
* comments are NOT allowed here (`#`-prefixed lines are rejected) because
|
||||
* the backend does not filter comments from bootstrap DNS entries.
|
||||
* Each line must contain a dot or colon.
|
||||
*
|
||||
* @example validateBootstrapDns("8.8.8.8") // undefined (valid)
|
||||
* @example validateBootstrapDns("# comment\n8.8.8.8") // error (# not supported)
|
||||
* @example validateBootstrapDns("! https://dns.example.com") // error (! not supported)
|
||||
* @example validateBootstrapDns("not-a-server") // error (no dot or colon)
|
||||
*/
|
||||
export const validateBootstrapDns = (value: string): string | undefined =>
|
||||
validatePerLine(
|
||||
value,
|
||||
(line) =>
|
||||
R_HAS_ADDRESS.test(line) && !R_BANG_PREFIX.test(line) && !R_UPSTREAM_COMMENT.test(line),
|
||||
);
|
||||
|
||||
/**
|
||||
* Validates that each non-empty, non-comment line in a blocked_hosts
|
||||
* entry contains a dot (domain, wildcard, or AdGuard URL filter rule).
|
||||
|
|
|
|||
27
client_v2/src/hooks/useOutsideDismiss.ts
Normal file
27
client_v2/src/hooks/useOutsideDismiss.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
import { createEffect, onCleanup } from 'solid-js';
|
||||
|
||||
/**
|
||||
* Calls `dismiss` when a `pointerdown` fires outside every one of the
|
||||
* given `refs` (the "safe zone"). Only activates when `isActive()` is true.
|
||||
*
|
||||
* Uses capture phase so it can intercept the event before
|
||||
* `stopPropagation` calls from descendants.
|
||||
*/
|
||||
export function useOutsideDismiss(
|
||||
isActive: () => boolean,
|
||||
dismiss: () => void,
|
||||
...refs: Array<HTMLElement | undefined>
|
||||
) {
|
||||
createEffect(() => {
|
||||
if (!isActive()) return;
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
const target = e.target as Element;
|
||||
if (refs.some((r) => r?.contains(target))) return;
|
||||
dismiss();
|
||||
};
|
||||
|
||||
document.addEventListener('pointerdown', onPointerDown, true);
|
||||
onCleanup(() => document.removeEventListener('pointerdown', onPointerDown, true));
|
||||
});
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { createSignal, createEffect, createMemo, untrack } from 'solid-js';
|
||||
import { createSignal, createEffect, createMemo, untrack, onMount } from 'solid-js';
|
||||
|
||||
import { ALL_INTERFACES_IP, STANDARD_DNS_PORT, STANDARD_WEB_PORT } from 'panel/helpers/constants';
|
||||
import { validateInstallPort } from 'panel/helpers/validators';
|
||||
|
|
@ -72,12 +72,7 @@ export const useInstallSettingsForm = (
|
|||
setIsDirty(true);
|
||||
};
|
||||
|
||||
// Validate and call validateForm when values change
|
||||
createEffect(() => {
|
||||
if (!isDirty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const runValidation = () => {
|
||||
const webPortVal = webPort();
|
||||
const dnsPortVal = dnsPort();
|
||||
const webIpVal = webIp();
|
||||
|
|
@ -91,15 +86,18 @@ export const useInstallSettingsForm = (
|
|||
}
|
||||
|
||||
validateForm({
|
||||
web: {
|
||||
ip: webIpVal,
|
||||
port: webPortVal,
|
||||
},
|
||||
dns: {
|
||||
ip: dnsIpVal,
|
||||
port: dnsPortVal,
|
||||
},
|
||||
web: { ip: webIpVal, port: webPortVal },
|
||||
dns: { ip: dnsIpVal, port: dnsPortVal },
|
||||
});
|
||||
};
|
||||
|
||||
onMount(runValidation);
|
||||
|
||||
createEffect(() => {
|
||||
if (!isDirty()) {
|
||||
return;
|
||||
}
|
||||
runValidation();
|
||||
});
|
||||
|
||||
const isValid = createMemo(() => {
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@
|
|||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 186px;
|
||||
min-height: 56px;
|
||||
padding: 16px 12px;
|
||||
font-size: 16px;
|
||||
transition: var(--t2) background-color;
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@ import { createStore } from 'solid-js/store';
|
|||
import { untrack } from 'solid-js';
|
||||
import { apiClient } from 'panel/api/Api';
|
||||
import { addErrorToast, addSuccessToast } from './toasts';
|
||||
import { dashboardState } from './dashboard';
|
||||
import { redirectToCurrentProtocol } from '../helpers/helpers';
|
||||
import intl from 'panel/common/intl';
|
||||
|
||||
type EncryptionState = {
|
||||
|
|
@ -139,10 +141,7 @@ export const setTlsConfig = async (values: any, opts?: { silent?: boolean }) =>
|
|||
const data = await apiClient.setTlsConfig(encoded);
|
||||
const decoded = decodeResponse(data);
|
||||
|
||||
if (fullValues.enabled && fullValues.force_https && window.location.protocol === 'http:') {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
redirectToCurrentProtocol(fullValues, dashboardState.httpPort);
|
||||
|
||||
setState({ ...decoded, processingConfig: false });
|
||||
if (!opts?.silent) {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,12 @@ export const getStats = async (period?: number) => {
|
|||
numReplacedSafesearch: data.num_replaced_safesearch || 0,
|
||||
avgProcessingTime: secondsToMilliseconds(data.avg_processing_time),
|
||||
timeUnits: data.time_units || initialState.timeUnits,
|
||||
topUpstreamsAvgTime: normalizeTopStats(data.top_upstreams_avg_time || []),
|
||||
topUpstreamsAvgTime: normalizeTopStats(data.top_upstreams_avg_time || []).map(
|
||||
(item: { name: string; count: number }) => ({
|
||||
...item,
|
||||
count: secondsToMilliseconds(item.count),
|
||||
}),
|
||||
),
|
||||
topUpstreamsResponses: normalizeTopStats(data.top_upstreams_responses || []),
|
||||
processingStats: false,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue