From 0b44f3c25e99e233ba048024dabf92c0c2d68368 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:39:11 +0000 Subject: [PATCH 01/15] Fix(i18n): correct "For Forker" typo to "For developers" in changelog Co-authored-by: jason5ng32 <1546290+jason5ng32@users.noreply.github.com> --- frontend/data/changelog.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/data/changelog.json b/frontend/data/changelog.json index b536be964..f2e438ce6 100644 --- a/frontend/data/changelog.json +++ b/frontend/data/changelog.json @@ -1671,10 +1671,10 @@ { "type": "add", "change": { - "en": "For Forker: added section promotion slots, can add promotion content in different sections as needed", - "zh": "为 Forker: 新增 Section 推广位,可以根据需要,在不同的 Section 增加推广内容", - "fr": "Pour les Forker : ajout de slots promotionnels par section, permettant d'ajouter du contenu promotionnel dans différentes sections selon les besoins", - "ru": "Для Forker: добавлены слоты продвижения, чтобы можно было добавлять продвижение в разные секции в зависимости от потребностей" + "en": "For developers: added section promotion slots, can add promotion content in different sections as needed", + "zh": "为开发者:新增 Section 推广位,可以根据需要,在不同的 Section 增加推广内容", + "fr": "Pour les développeurs : ajout de slots promotionnels par section, permettant d'ajouter du contenu promotionnel dans différentes sections selon les besoins", + "ru": "Для разработчиков: добавлены слоты продвижения, чтобы можно было добавлять продвижение в разные секции в зависимости от потребностей" } }, { From 8c6e1455133c0c15331edc5f610a4449fb39ca48 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 22 Aug 2026 16:51:18 +0000 Subject: [PATCH 02/15] Style(preferences): fix Beta badge vertical alignment Co-authored-by: jason5ng32 <1546290+jason5ng32@users.noreply.github.com> --- frontend/components/widgets/Preferences.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/widgets/Preferences.vue b/frontend/components/widgets/Preferences.vue index 99230e682..41f35492c 100644 --- a/frontend/components/widgets/Preferences.vue +++ b/frontend/components/widgets/Preferences.vue @@ -43,7 +43,7 @@ {{ lang.label }} Beta + class="px-1.5 py-0.5 text-[10px] font-medium">Beta From da8698f308beb19674af126d042f49a1b577e5f2 Mon Sep 17 00:00:00 2001 From: Vitor Parras Date: Sat, 22 Aug 2026 14:09:46 -0300 Subject: [PATCH 03/15] Feat(i18n): add Brazilian Portuguese beta locale --- common/locale-registry.js | 1 + frontend/locales/pt-BR.json | 1476 +++++++++++++++++++++++++++++++++++ 2 files changed, 1477 insertions(+) create mode 100644 frontend/locales/pt-BR.json diff --git a/common/locale-registry.js b/common/locale-registry.js index 428861123..6153ca6f2 100644 --- a/common/locale-registry.js +++ b/common/locale-registry.js @@ -14,6 +14,7 @@ export const LOCALES = [ { code: 'zh', nativeName: '简体中文', flag: 'cn', apiTag: 'zh-CN', htmlLang: 'zh-CN', status: 'full' }, { code: 'fr', nativeName: 'Français', flag: 'fr', apiTag: 'fr', htmlLang: 'fr', status: 'full' }, { code: 'ru', nativeName: 'Русский', flag: 'ru', apiTag: 'ru', htmlLang: 'ru', status: 'full' }, + { code: 'pt-BR', nativeName: 'Português (Brasil)', flag: 'br', apiTag: 'pt-BR', htmlLang: 'pt-BR', status: 'beta' }, ]; // Registry order — also the order the language picker renders. diff --git a/frontend/locales/pt-BR.json b/frontend/locales/pt-BR.json new file mode 100644 index 000000000..b0e21ea49 --- /dev/null +++ b/frontend/locales/pt-BR.json @@ -0,0 +1,1476 @@ +{ + "user": { + "SignIn": "", + "SignInWithGoogle": "", + "SignInWithGithub": "", + "SignOut": "", + "SignInToView": "", + "SignInToUse": "", + "InvalidUserToken": "", + "QuotaExceeded": "", + "QuotaSponsorCta": "", + "ViewUsage": "", + "Usage": { + "Title": "", + "Note": "", + "IpinfoFeature": "", + "SponsorNote": "" + }, + "MyAchievements": "", + "Fields": { + "User": "", + "CreatedAt": "", + "LastLogin": "", + "Level": "", + "Fetching": "", + "SignInMethods": "" + }, + "Level": { + "Standard": "", + "Premium": "", + "Owner": "", + "Developer": "", + "HonoraryMember": "" + }, + "Benefits": { + "Title": "", + "Benefit": "", + "Note1": "", + "Note2": "", + "Current": "", + "Tiers": { + "Visitor": { + "Name": "", + "Item1": "", + "Item2": "", + "Item3": "", + "Item4": "", + "Item5": "", + "Item6": "" + }, + "SignedIn": { + "Name": "", + "Item1": "", + "Item2": "", + "Item3": "", + "Item4": "", + "Item5": "", + "Item6": "", + "Item7": "" + }, + "Sponsor": { + "Name": "", + "Item1": "", + "Item2": "", + "Item3": "" + } + } + }, + "Achievements": { + "Title": "", + "Note": "", + "Get": "", + "GetCount": "", + "NotGet": "", + "NotGetCount": "", + "FooterNote": "", + "Congrats": "", + "CongratsMessage": "", + "NewAchievementIs": "", + "Type": { + "IAmHuman": { + "Title": "", + "Meet": "" + }, + "BarelyEnough": { + "Title": "", + "Meet": "" + }, + "RapidPace": { + "Title": "", + "Meet": "" + }, + "TorrentFlow": { + "Title": "", + "Meet": "" + }, + "SteadyGoing": { + "Title": "", + "Meet": "" + }, + "TooFastTooSimple": { + "Title": "", + "Meet": "" + }, + "SwiftAscent": { + "Title": "", + "Meet": "" + }, + "SurfaceCheck": { + "Title": "", + "Meet": "" + }, + "HalfwayThere": { + "Title": "", + "Meet": "" + }, + "FullySecured": { + "Title": "", + "Meet": "" + }, + "JustInCase": { + "Title": "", + "Meet": "" + }, + "HiddenWell": { + "Title": "", + "Meet": "" + }, + "SlipUp": { + "Title": "", + "Meet": "" + }, + "CleverTrickery": { + "Title": "", + "Meet": "" + }, + "EnergySaver": { + "Title": "", + "Meet": "" + }, + "ResourceHog": { + "Title": "", + "Meet": "" + }, + "MakingBigNews": { + "Title": "", + "Meet": "" + }, + "GenerousDonor": { + "Title": "", + "Meet": "" + }, + "ItIsOpen": { + "Title": "", + "Meet": "" + }, + "CuriousCat": { + "Title": "", + "Meet": "" + }, + "CrossingTheWall": { + "Title": "", + "Meet": "" + }, + "GlobeTrotter": { + "Title": "", + "Meet": "" + }, + "HelloWorld": { + "Title": "", + "Meet": "" + }, + "AddSweetAdd": { + "Title": "", + "Meet": "" + }, + "PrettyDirty": { + "Title": "", + "Meet": "" + }, + "SqueakyClean": { + "Title": "", + "Meet": "" + }, + "WalkWithPenguins": { + "Title": "", + "Meet": "" + } + } + } + }, + "nav": { + "id": "nav", + "Title": "IPCheck.ing", + "Navigation": "Navegação", + "IPInfo": "Informações de IP", + "Connectivity": "Conectividade", + "ServiceStatus": "Status dos serviços", + "WebRTC": "Teste de vazamento WebRTC", + "DNSLeakTest": "Teste de vazamento de DNS", + "SpeedTest": "Teste de velocidade", + "PingTest": "Latência global", + "MTRTest": "Teste MTR", + "RuleTest": "Teste de regras", + "AdvancedTools": "Ferramentas avançadas", + "SearchDocs": "Pesquisar na documentação…", + "DocsToolConfirm": "Compartilhar seus resultados atuais dos testes?", + "DocsToolSummary": "Ler os resultados nesta página", + "DocsToolRunConfirm": "Executar testes na sua página?", + "DocsToolRunSummary": "Executou seus testes de rede", + "AskIPilot": "Perguntar ao IPilot", + "OpenDocs": "Abrir documentação", + "DocsGreetingTitle": "Como posso ajudar?", + "DocsGreetingSubtitle": "Pergunte qualquer coisa sobre o MyIP — as ferramentas, seus resultados ou auto-hospedagem.", + "DocsQuestions": [ + "Minha VPN está vazando meu IP real?", + "Por que a localização do meu IP está errada?", + "Compare meus resultados do teste WebRTC e do teste de vazamento de DNS e me dê sugestões", + "Como leio resultados de MTR?", + "Como faço para hospedar meu próprio MyIP?", + "Quero patrocinar o projeto MyIP" + ], + "pulse": { + "title": "Earth Online", + "tooltip": "Visitantes do mundo todo", + "others": "Outros", + "shareStatus": "Compartilhar seu status", + "liveMap": "Mapa de calor de visitantes ao vivo", + "noVisitors": "Nenhum visitante ainda", + "quiet": "Tudo tranquilo agora — seja a primeira pessoa a compartilhar", + "loadError": "Não foi possível carregar a atividade mais recente — tente novamente em instantes", + "share": "Compartilhamento de visitante", + "latest": "Mais recente", + "sent": "Enviado", + "alreadySent": "Este status já foi enviado", + "rateLimited": "Apenas um status pode ser compartilhado em um curto período", + "limitedTitle": "Status por tempo limitado", + "outages": { + "title": "Eventos globais de indisponibilidade", + "outage": "Queda de internet", + "anomaly": "Anomalia de tráfego", + "ongoing": "Em andamento", + "ended": "Recuperado", + "started": "Iniciado {time}", + "lasted": "Durou {duration}", + "quiet": "Tudo tranquilo — nenhum evento de indisponibilidade nos últimos 30 dias", + "loadMore": "Carregar mais", + "cause": { + "GOVERNMENT_DIRECTED": "Direcionado pelo governo", + "POWER_OUTAGE": "Falta de energia", + "CABLE_CUT": "Rompimento de cabo", + "TECHNICAL_PROBLEM": "Problema técnico", + "MECHANICAL": "Problema mecânico", + "UNKNOWN": "Causa desconhecida", + "WEATHER": "Tempo severo", + "FIRE": "Incêndio", + "CYBERATTACK": "Ataque cibernético", + "NATURAL_DISASTER": "Desastre natural", + "MAINTENANCE": "Manutenção", + "MILITARY_ACTION": "Ação militar" + }, + "level": { + "NATIONWIDE": "Nacional", + "REGIONAL": "Regional", + "NETWORK": "Rede" + } + }, + "statuses": { + "fast": "Rede extremamente rápida", + "ok": "Tudo certo por aqui", + "slow": "A rede parece lenta", + "flaky": "A conexão vive caindo", + "blocked": "Um site não abre", + "good": "Me sentindo bem hoje", + "bad": "Dia difícil por aqui", + "passing": "Só passando", + "lag": "O lag no jogo está surreal", + "newip": "Acabei de ganhar um IP novo", + "vpn": "Navegando por uma VPN", + "night": "Acordado até tarde hoje", + "vibing": "Codando no embalo", + "fixing": "Consertando o Wi-Fi da família de novo", + "tuning": "Tirei mais 10 ms", + "prgday": "Feliz 0x100º dia!", + "internetday": "LO and behold, feliz Dia da Internet", + "halloween": "Travessura ou ping!", + "diwali": "Festival das luzes (e da fibra)", + "christmas": "O Papai Noel está em uma rota de baixa latência", + "newyear": "Ano novo, IP novo!", + "privacyday": "Este status é criptografado de ponta a ponta", + "lunar": "Que todos os seus envelopes vermelhos cheguem", + "valentine": "Procurando uma conexão estável", + "jobs": "Continue faminto, continue tolo.", + "eid": "Eid Mubarak, mundo!", + "aprilfools": "Meu IP é 127.0.0.1", + "earthday": "Fazendo logoff para tocar na grama", + "turing": "Este status foi enviado por um humano. Provavelmente.", + "emojiday": "📶🌍🎉✨", + "birthday": "Feliz aniversário, IPCheck.ing!" + }, + "festivalDesc": { + "prgday": "Dia dos Programadores — o 256º dia do ano, 0x100 em hexadecimal", + "internetday": "29 de outubro de 1969: a ARPANET tentou enviar LOGIN — travou depois de “LO”", + "halloween": "Halloween (31 de outubro) — o mais assustador ainda é a perda de pacotes", + "diwali": "Diwali — que suas lâmpadas e sua fibra brilhem", + "christmas": "Natal — trenó em linha dedicada, jitter zero", + "newyear": "Ano Novo — fogos incluídos, sem largura de banda extra", + "privacyday": "Dia da Privacidade de Dados (28 de janeiro) — sim, percebemos a ironia de transmitir isto", + "lunar": "Ano-Novo Lunar — perda de pacotes zero nos envelopes vermelhos", + "valentine": "Dia de São Valentim — TCP e amor verdadeiro começam com um handshake", + "jobs": "Aniversário de Steve Jobs (24 de fevereiro de 1955) — do discurso dele em Stanford, em 2005", + "eid": "Eid al-Fitr — votos calorosos deste canto do Earth Online", + "aprilfools": "Dia da Mentira — não há lugar como 127.0.0.1", + "earthday": "Dia da Terra (22 de abril) — até o Earth Online recomenda um pouco de Earth Offline", + "turing": "Aniversário de Alan Turing (23 de junho de 1912) — pai do teste de Turing", + "emojiday": "Dia Mundial do Emoji (17 de julho) — palavras são coisa do século passado", + "birthday": "Aniversário do IPCheck.ing — a v1.0 foi lançada em 6 de novembro de 2020" + } + }, + "preferences": { + "title": "Preferências", + "preferenceTips": "Estas configurações são salvas no seu navegador para reutilização fácil. Algumas opções exigem atualizar a página para entrar em vigor.", + "colorScheme": "Esquema de cores", + "systemAuto": "Automático", + "colorLight": "Dia", + "colorDark": "Noite", + "ipSourcesToCheck": "Servidores para verificar IPs", + "ipSourcesToCheckTips": "Selecione o número de servidores que verificarão seu IP. Quanto mais servidores forem selecionados, mais tempo a verificação levará.", + "appSettings": "Configurações do programa", + "autoRun": "Execução automática", + "autoRunTips": "As informações de IP sempre são verificadas quando a página carrega. Escolha quais outros módulos serão executados automaticamente na inicialização.", + "simpleMode": "Modo simples", + "simpleModeTips": "Oculta o texto descritivo de cada módulo, adequado para usuários que já conhecem o aplicativo.", + "connectivityMultipleTests": "Múltiplas atualizações de conectividade", + "connectivityMultipleTestsTips": "Quando ativado, o aplicativo executa várias verificações na inicialização e exibe a menor latência.", + "popupConnectivityNotifications": "Alertas pop-up de conectividade", + "popupConnectivityNotificationsTips": "Quando ativado, os resultados da verificação inicial são exibidos como alerta pop-up.", + "ipHistory": "Histórico de IP", + "ipHistoryEnabled": "Registrar histórico de IP", + "ipHistoryDays": "Dias de retenção", + "ipHistoryTips": "Quando ativado, os IPs detectados são registrados somente neste navegador e mantidos pelo número de dias selecionado. Desativar essa opção interrompe o registro; os registros existentes permanecem até você apagá-los no painel Histórico de IP.", + "ipDB": "Banco de geolocalização de IP", + "ipDBTips": "Você pode selecionar a fonte padrão de geolocalização de IP. Se a fonte selecionada estiver indisponível, o sistema usará as fontes seguintes em ordem.", + "language": "Configuração de idioma", + "languageTips": "Atualize o navegador para a alteração entrar em vigor." + }, + "DocsPrompt": "Digite sua pergunta…", + "DocsSignInTitle": "Login necessário", + "DocsSignInMessage": "Faça login para perguntar ao IPilot" + }, + "securitychecklist": { + "Title": "", + "Note": "", + "Note2": "", + "Progress": "", + "Item": "", + "Items": "", + "Priority": "", + "Ignore": "", + "alert-total": "", + "alert-checked": "", + "alert-ignored": "", + "alert-unchecked": "", + "Checked": "", + "Ignored": "", + "Unchecked": "", + "Basic": "", + "Optional": "", + "Essential": "", + "Advanced": "", + "Reset": "", + "ShowAll": "", + "ShowUnchecked": "", + "ShowIgnored": "", + "ShowChecked": "" + }, + "curl": { + "Title": "", + "Note1": "", + "Note2_1": "", + "Note2_2": "", + "Note3": "", + "Note4": "", + "getIPv4": "", + "getIPv6": "", + "get6and4": "", + "notAvailable": "" + }, + "browserinfo": { + "Title": "", + "Note": "", + "Note2": "", + "browser": { + "Infos": "", + "browserName": "", + "engineName": "", + "osName": "", + "deviceVendor": "", + "cpuArchitecture": "", + "gpu": "", + "cpuCores": "", + "languages": "", + "timezone": "", + "display": "", + "connection": "", + "touchPoints": "", + "doNotTrack": "", + "doNotTrackOn": "", + "doNotTrackOff": "", + "doNotTrackUnset": "", + "pdfViewer": "", + "pdfViewerYes": "", + "pdfViewerNo": "", + "cookieEnabled": "", + "cookieEnabledTrue": "", + "cookieEnabledFalse": "" + }, + "fingerprint": { + "Infos": "", + "fingerprint": "", + "changeOption": "", + "browserTips": "" + }, + "options": { + "audio": "", + "canvas": "", + "fonts": "", + "hardware": "", + "locales": "", + "permissions": "", + "plugins": "", + "screen": "", + "system": "", + "webgl": "", + "math": "", + "intl": "", + "mediaDevices": "", + "speech": "", + "mathml": "", + "webrtc": "" + }, + "components": { + "title": "", + "note": "" + }, + "calError": "", + "calculating": "" + }, + "censorshipcheck": { + "Title": "", + "Note": "", + "Note2": "", + "Placeholder": "", + "Country": "", + "Status": "", + "City": "", + "Network": "", + "invalidURL": "", + "fetchError": "", + "HistoryTitle": "", + "SummaryFlagged": "", + "SummaryClean": "", + "SummarySparseFlagged": "", + "SummarySparseClean": "", + "NoData": "", + "OoniError": "", + "TierConfirmed": "", + "TierLikely": "", + "TierSigns": "", + "Measurements": "", + "AnomalyShare": "", + "MethodDns": "", + "MethodTcpIp": "", + "MethodHttpFailure": "", + "MethodHttpDiff": "", + "RealtimeTitle": "", + "ResultsHint": "", + "SectionFlagged": "", + "PresetHighRisk": "", + "PresetOpen": "", + "RunVerify": "", + "RealtimeNote": "", + "RealtimeFooterNote": "" + }, + "invisibilitytest": { + "Title": "", + "Note": "", + "Note2": "", + "fetchError": "", + "scriptLoadError": "", + "yourIP": "", + "proxyScore": "", + "VPNScore": "", + "isProxy": "", + "notProxy": "", + "isVPN": "", + "notVPN": "", + "itemName": "", + "itemProxyResult": "", + "itemComment": "", + "agreement": "", + "blocklist": { + "proxy": { + "title": "", + "positive": "", + "negative": "" + }, + "vpn": { + "title": "", + "positive": "", + "negative": "" + }, + "vpnExitNode": { + "title": "", + "positive": "", + "negative": "" + } + }, + "tor": { + "title": "", + "positive": "", + "negative": "" + }, + "headers": { + "title": "", + "positive": "", + "negative": "" + }, + "datacenter": { + "title": "", + "positive": "", + "negative": "" + }, + "tcp": { + "title": "", + "positive": "", + "computer": "", + "server": "", + "negative": "" + }, + "timezone": { + "title": "", + "positive": "", + "computer": "", + "server": "", + "negative": "" + }, + "net": { + "title": "", + "positive": "", + "negative": "" + }, + "webrtc": { + "title": "", + "positive": "", + "ipsAre": "", + "negative": "" + }, + "flow": { + "title": "", + "positive": "", + "negative": "" + }, + "latencyVSWS": { + "title": "", + "positive": "", + "fromTCP": "", + "fromWS": "", + "negative": "" + }, + "latencyVSPing": { + "title": "", + "positive": "", + "fromTCP": "", + "fromPing": "", + "negative": "" + }, + "highlatency": { + "title": "", + "positive": "", + "negative": "" + } + }, + "personacheck": { + "Title": "", + "Note": "", + "NoteVs": "", + "Note2": "", + "axis": { + "match": "", + "coherence": "", + "leak": "" + }, + "zone": { + "expected": "", + "optional": "", + "run": "" + }, + "selectCountry": "", + "noCountryMatch": "", + "noProfile": "", + "languageSingle": "", + "languageMulti": "", + "timezoneMulti": "", + "hourCycle": { + "h11": "", + "h12": "", + "h23": "", + "h24": "" + }, + "profile": { + "timezone": "" + }, + "runCompare": "", + "runError": "", + "source": { + "ipinfo": "", + "webrtc": "", + "dnsleak": "" + }, + "report": { + "grade": { + "A": "", + "B": "", + "C": "", + "D": "", + "unknown": "" + }, + "gradeNote": { + "A": "", + "B": "", + "C": "", + "D": "", + "unknown": "" + }, + "counts": { + "leak": "", + "warning": "", + "match": "", + "unknown": "", + "notApplicable": "" + }, + "nothingActionable": "", + "howToFix": "", + "verdict": { + "match": "", + "mismatch": "", + "leak": "", + "unnatural": "", + "unknown": "", + "not-applicable": "" + }, + "visibility": { + "public": "", + "probed": "", + "risk-engine": "" + }, + "profileTitle": "", + "state": { + "match": "", + "mismatch": "", + "unnatural": "", + "leak": "", + "unknown": "", + "not-applicable": "" + } + }, + "detail": { + "expected": "", + "actual": "", + "disputed": "", + "v4": "", + "v6": "", + "candidateCount": "", + "matching": "", + "ipType": "", + "expectedOffset": "", + "actualOffset": "", + "sameOffset": "", + "primary": "", + "primaryExpected": "", + "demoted": "", + "timeZone": "", + "reportedOffset": "", + "zoneOffset": "", + "samples": "", + "expectedScripts": "", + "installedScripts": "", + "expectedLanguages": "", + "voiceLanguages": "", + "voiceCount": "", + "expectedLayout": "", + "actualLayout": "", + "headerPrimary": "", + "scriptPrimary": "", + "headerLanguages": "", + "ipZone": "", + "browserZone": "", + "ipOffset": "", + "browserOffset": "", + "colo": "", + "gpsTimezone": "", + "accuracyMetres": "", + "cardIssuer": "", + "cardNetwork": "", + "cardTier": "", + "cardType": "" + }, + "checks": { + "ip-country": { + "title": "", + "fix": "", + "what": "" + }, + "webrtc-leak": { + "title": "", + "fix": "", + "what": "" + }, + "dns-resolver-country": { + "title": "", + "fix": "", + "what": "" + }, + "asn-type": { + "title": "", + "fix": "", + "what": "" + }, + "timezone-vs-persona": { + "title": "", + "fix": "", + "what": "" + }, + "language-match": { + "title": "", + "fix": "", + "what": "" + }, + "script-fonts": { + "title": "", + "fix": "", + "what": "" + }, + "speech-voices": { + "title": "", + "fix": "", + "what": "" + }, + "keyboard-layout": { + "title": "", + "fix": "", + "what": "" + }, + "edge-geo-consensus": { + "title": "", + "fix": "", + "what": "" + }, + "accept-language-header": { + "title": "", + "fix": "", + "what": "" + }, + "ip-timezone-vs-browser": { + "title": "", + "fix": "", + "what": "" + }, + "gps-location": { + "title": "", + "fix": "", + "what": "" + }, + "payment-region": { + "title": "", + "fix": "", + "what": "" + }, + "intl-number-format": { + "title": "", + "what": "", + "fix": "" + }, + "intl-date-format": { + "title": "", + "what": "", + "fix": "" + }, + "intl-hour-cycle": { + "title": "", + "what": "", + "fix": "" + }, + "timezone-authenticity": { + "title": "", + "what": "", + "fix": "" + } + }, + "reason": { + "no-marker-script": "", + "bin-lookup-failed": "", + "bin-not-recognized": "", + "font-probe-unavailable": "", + "no-voice-packs": "", + "keyboard-api-unavailable": "", + "no-layout-expectation": "", + "no-persona-languages": "", + "geolocation-unsupported": "", + "ip-no-country": "", + "ip-no-type": "", + "ip-no-timezone": "", + "webrtc-no-candidates": "", + "dns-no-country": "" + }, + "optional": { + "gpsNote": "", + "gpsButton": "", + "gpsGranted": "", + "gpsFailed": { + "denied": "", + "timeout": "", + "unavailable": "", + "unsupported": "" + }, + "cardNote": "", + "cardError": "", + "cardReady": "" + }, + "dependencies": { + "note": "", + "runButton": "", + "runButtonRunning": "", + "measured": "", + "allReady": "" + }, + "step": { + "country": "", + "language": "", + "location": "", + "card": "", + "awaitingCountry": "", + "otherTests": "" + }, + "emptyHint": "", + "readyHint": "", + "value": { + "yes": "", + "no": "" + } + }, + "advancedtools": { + "OpenInNewTab": "", + "BackToHome": "", + "Title": "", + "Note": "", + "PingTestNote": "", + "MTRTestNote": "", + "DNSResolverNote": "", + "EnhancedDnsLeakTest": "", + "RuleTestNote": "", + "CensorshipCheck": "", + "ServiceStatus": "", + "Whois": "", + "InvisibilityTest": "", + "MacChecker": "", + "BrowserInfo": "", + "SecurityChecklist": "", + "PersonaCheck": "" + }, + "macchecker": { + "Title": "", + "Note": "", + "Note2": "", + "Placeholder": "", + "invalidMAC": "", + "fetchError": "", + "company": "", + "macPrefix": "", + "address": "", + "country": "", + "blockStart": "", + "blockEnd": "", + "blockSize": "", + "blockType": "", + "blockRange": "", + "isRand": "", + "isPrivate": "", + "isMulticast": "", + "isUnicast": "", + "isGlobal": "", + "isLocal": "", + "property": "", + "value": "", + "manufacturer": "" + }, + "whois": { + "Title": "", + "Note": "", + "Note2": "", + "Note3": "", + "Placeholder": "", + "invalidURL": "", + "reservedIP": "", + "noData": "", + "fetchError": "", + "Provider": "" + }, + "enhanceddnsleaktest": { + "Title": "", + "Note": "", + "Note2": "", + "Note3": "", + "Run": "", + "RunAgain": "", + "RunningProbes": "", + "PollingResult": "", + "FetchError": "", + "Empty": "", + "TableTitle": "", + "DedupeToggle": "", + "Flow": { + "Probing": "", + "Waiting": "", + "Fetching": "", + "Done": "" + }, + "DnssecChip": { + "Signed": "", + "Unsigned": "", + "Validated": "", + "CdSet": "" + }, + "Summary": { + "ResolverCount": "", + "RawCount": "", + "DnssecPosture": "", + "DnssecGood": "", + "DnssecNoDo": "", + "DnssecCdSet": "", + "DnssecNone": "" + }, + "Fields": { + "ResolverIP": "", + "Location": "", + "ASN": "", + "QueryType": "", + "Transport": "", + "ECS": "" + }, + "Legend": { + "Title": "", + "ResolverIP": "", + "QueryType": "", + "Transport": "", + "ECS": "", + "DO": "", + "CD": "" + } + }, + "dnsresolver": { + "Title": "", + "Note": "", + "Note2": "", + "Placeholder": "", + "invalidURL": "", + "fetchError": "", + "Provider": "", + "Result": "", + "Record": "", + "AllRegions": "" + }, + "ruletest": { + "Title": "", + "Name": "", + "Note": "", + "StatusWait": "", + "StatusError": "", + "Country": "", + "RefreshAll": "" + }, + "Tooltips": { + "RefreshIPCard": "", + "SaveAsImage": "", + "RefreshConnectivityTests": "", + "RefreshWebRTC": "", + "RefreshDNSLeakTest": "", + "SpeedTestButton": "", + "ShowASNInfo": "", + "ShowASNHistory": "", + "ShowASNConnectivity": "", + "CopyIP": "", + "CopySdpLog": "", + "ViewOnMap": "", + "InfoMask": "", + "IPHistory": "", + "ShareReport": "", + "QueryIP": "", + "GithubLink": "" + }, + "report": { + "Title": "", + "Note": "", + "SectionsLabel": "", + "NotTested": "", + "MaskTail": "", + "TtlLabel": "", + "Ttl1": "", + "Ttl3": "", + "Ttl7": "", + "Preview": "", + "CreateLink": "", + "CopyAI": "", + "DownloadJson": "", + "CopyLink": "", + "NoSelection": "", + "CreateFailed": "", + "LinkOnce": "", + "ExpiresAt": "", + "CopiedTitle": "", + "CopiedMessage": "", + "ai": { + "Heading": "", + "Intro": "", + "Masked": "", + "Instruction": "", + "Sections": { + "ipinfo": "", + "connectivity": "", + "webrtc": "", + "dnsleak": "", + "speedtest": "", + "pingtest": "", + "mtrtest": "", + "ruletest": "", + "browserinfo": "", + "invisibility": "", + "enhanceddnsleak": "", + "persona": "" + } + } + }, + "reportPage": { + "Loading": "", + "NotFound": "", + "NotFoundNote": "", + "LoadFailed": "", + "Unsupported": "", + "GeneratedBy": "", + "Stale": "", + "TestMine": "", + "Col": { + "Source": "", + "Name": "", + "IP": "", + "Location": "", + "ISP": "", + "Status": "", + "Server": "", + "Provider": "", + "Target": "", + "UniqueIPs": "", + "Resolvers": "" + }, + "Dnssec": { + "ok": "", + "partial": "", + "none": "" + } + }, + "ipHistory": { + "Title": "", + "Notes": "", + "Empty": "", + "FilterByType": "", + "FilterByCountry": "", + "UnknownCountry": "", + "NoMatch": "", + "ClearAll": "", + "ClearConfirm": "" + }, + "ipInfos": { + "Title": "", + "Notes": "", + "Source": "", + "Country": "", + "Region": "", + "City": "", + "TimeZone": "", + "ISP": "", + "ASN": "", + "Coordinates": "", + "IPv4Error": "", + "IPv6Error": "", + "type": "", + "isProxy": "", + "qualityScore": "", + "qualityScoreUnknown": "", + "advancedUnlockCta": "", + "advancedQuotaCta": "", + "ASNInfo": { + "trafficPercentage": "", + "note": "", + "asnName": "", + "asnOrgName": "", + "asnCountryCode": "", + "estimatedUsers": "", + "IPv4_Pct": "", + "IPv6_Pct": "", + "HTTP_Pct": "", + "HTTPS_Pct": "", + "Desktop_Pct": "", + "Mobile_Pct": "", + "Bot_Pct": "", + "Human_Pct": "", + "moreData": "" + }, + "ASNHistory": { + "announcedPrefixes": "", + "note": "", + "empty": "", + "error": "", + "seenBy": "" + }, + "ASNConnectivity": { + "note": "", + "empty": "", + "error": "", + "expand": "", + "dialogTitle": "", + "legendOrigin": "", + "legendTier1": "", + "legendIntermediate": "", + "zoomIn": "", + "zoomOut": "", + "zoomReset": "" + }, + "advancedData": { + "proxyYes": "", + "proxyMaybe": "", + "proxyNo": "", + "proxyUnknown": "", + "type": { + "Residential": "", + "Wireless": "", + "Business": "", + "Hosting": "", + "unknownType": "" + }, + "Nativeness": "", + "NativeIPYes": "", + "NativeIPNo": "", + "nativenessTooltip": "" + } + }, + "serviceStatus": { + "Title": "", + "Note": "", + "Refresh": "", + "LastChecked": "", + "AllOperational": "", + "ComponentsTitle": "", + "IncidentsTitle": "", + "NoComponents": "", + "NoRecentIncidents": "", + "Loading": "", + "Checking": "", + "IncidentsError": "", + "ComponentsError": "", + "indicator": { + "minor": "", + "major": "", + "critical": "", + "maintenance": "", + "unknown": "" + }, + "component": { + "operational": "", + "degraded_performance": "", + "partial_outage": "", + "major_outage": "", + "under_maintenance": "" + }, + "incidentStatus": { + "investigating": "", + "identified": "", + "monitoring": "", + "resolved": "", + "postmortem": "", + "scheduled": "", + "in_progress": "", + "verifying": "", + "completed": "" + } + }, + "connectivity": { + "Title": "", + "Note": "", + "StatusWait": "", + "StatusAvailable": "", + "StatusUnavailable": "", + "StatusTimeout": "", + "checking": "", + "minTestTime": "", + "RefreshThisTest": "", + "RoundCount": "", + "addCustom": { + "AddCard": "", + "Title": "", + "Hint": "", + "NameLabel": "", + "NamePlaceholder": "", + "UrlLabel": "", + "UrlPlaceholder": "", + "Add": "", + "Remove": "", + "NameRequired": "", + "UrlRequired": "", + "InvalidUrl": "", + "LimitReached": "", + "LastTargetTitle": "", + "LastTargetMessage": "" + }, + "importDialog": { + "Title": "", + "CustomOption": "", + "ImportOption": "", + "SiteCount": "", + "Import": "", + "Capacity": "", + "NotEnoughCapacity": "" + }, + "importLists": { + "defaults": "", + "iran": "", + "russia": "", + "china": "", + "brazil": "", + "ai": "", + "social": "", + "streaming": "", + "gaming": "", + "developer": "", + "cloud": "", + "crypto": "", + "ecommerce": "", + "news": "" + } + }, + "webrtc": { + "id": "webrtc", + "Title": "", + "Name": "", + "Note": "", + "StatusWait": "", + "StatusError": "", + "StatusUnavailable": "", + "SdpLog": "", + "NATType": { + "srflx": "", + "prflx": "", + "relay": "", + "host": "", + "unknown": "" + } + }, + "dnsleaktest": { + "Name": "", + "Title": "", + "Note": "", + "Note2": "", + "EndpointCountry": "", + "StatusWait": "", + "StatusError": "" + }, + "speedtest": { + "Title": "", + "Note": "", + "Download": "", + "Upload": "", + "Latency": "", + "Jitter": "", + "loadedLatency": "", + "videoStreaming": "", + "gaming": "", + "rtc": "", + "score": "", + "connectionFrom": "", + "connectionTo": "", + "connectionEnd": "", + "testFailed": "", + "quality": { + "bad": "", + "poor": "", + "average": "", + "good": "", + "great": "" + } + }, + "globalping": { + "Selected": "", + "Suggested": "", + "NoProbe": "", + "GroupFull": "", + "UnreachableTarget": "", + "ResultsHint": "", + "Asia": "", + "Europe": "", + "Africa": "", + "Americas": "", + "Oceania": "" + }, + "pingtest": { + "EnterIPLabel": "", + "EnterIPPlaceholder": "", + "UseStored": "", + "id": "pingtest", + "Title": "", + "Note": "", + "Note2": "", + "Note3": "", + "SelectIP": "", + "Region": "", + "MinDelay": "", + "MaxDelay": "", + "AvgDelay": "", + "TotalPackets": "", + "PacketLoss": "", + "ReceivedPackets": "", + "DroppedPackets": "", + "Error": "" + }, + "mtrtest": { + "EnterIPLabel": "", + "EnterIPPlaceholder": "", + "UseStored": "", + "Title": "", + "Note": "", + "Note2": "", + "Note3": "", + "SelectIP": "", + "Region": "", + "Error": "", + "ColHost": "", + "ColIP": "", + "ColASN": "", + "ColLoss": "", + "ColSent": "", + "ColDrop": "", + "ColRecv": "", + "ColLast": "", + "ColAvg": "", + "ColBest": "", + "ColWorst": "", + "ColStDev": "", + "ColJitter": "", + "NoReply": "" + }, + "ipcheck": { + "Title": "", + "Placeholder": "", + "Error": "", + "reservedIP": "", + "NoData": "" + }, + "alert": { + "refreshEverythingMessage": "", + "refreshEverythingTitle": "", + "OhNo": "", + "Congrats": "", + "OhNo_Message": "", + "Congrats_Message": "", + "maskedInfoTitle": "", + "maskedInfoMessage": "", + "unmaskedInfoTitle": "", + "unmaskedInfoMessage": "", + "SignInFailed": "", + "SignInFailedReason": "", + "IpGeoSourceFallbackTitle": "", + "IpGeoSourceFallbackMessage": "", + "SignInEmailTakenTitle": "", + "SignInEmailTakenMessage": "" + }, + "shortcutKeys": { + "GoToTop": "", + "GoToBottom": "", + "ToggleDarkMode": "", + "RefreshEverything": "", + "RefreshIPCard": "", + "RefreshConnectivityTests": "", + "RefreshWebRTC": "", + "RefreshDNSLeakTest": "", + "SpeedTestButton": "", + "PingTest": "", + "MTRTest": "", + "RuleTest": "", + "DNSResolver": "", + "IPCheck": "", + "ToggleInfoMask": "", + "IPHistory": "", + "ShareReport": "", + "Help": "", + "HelpNote": "", + "GoNext": "", + "GoPrevious": "", + "OpenHighlightedTool": "", + "RefreshRuleTests": "", + "CensorshipCheck": "", + "Pulse": "", + "Whois": "", + "ServiceStatus": "", + "InvisibilityTest": "", + "EnhancedDnsLeakTest": "", + "MacChecker": "", + "BrowserInfo": "", + "SecurityChecklist": "", + "PersonaCheck": "" + }, + "page": { + "title": "IPCheck.ing - Verifique meu IP e geolocalização - Teste de vazamento de IP - Teste de vazamento de DNS - Verificação de qualidade de IP - Teste de velocidade da rede - Caixa de ferramentas de IP tudo em um", + "description": "Uma caixa de ferramentas de IP melhor e de código aberto: verifique seu IP e sua geolocalização, teste vazamentos de IP por WebRTC e DNS, faça uma verificação de qualidade de IP, verifique a impressão digital do navegador, além de teste de velocidade, teste de latência global, teste MTR, pesquisa Whois e muito mais.", + "keywords": "MyIP, ferramenta de IP, verificar IP, teste de vazamento de IP, verificação de qualidade de IP, consulta de IP, teste de vazamento de DNS, teste de vazamento WebRTC, verificação de impressão digital do navegador, teste de velocidade, teste de latência global, teste MTR, consulta DNS, pesquisa Whois", + "footerLink": "https://github.com/jason5ng32/MyIP", + "copyRightName": "Originalmente de ", + "copyRightLink": "https://ipcheck.ing", + "copyRightLinkName": "IPCheck.ing" + }, + "helpModal": { + "Title": "" + }, + "about": { + "Title": "", + "project": [ + "", + "", + "", + "" + ], + "founderTitle": "", + "founder": [ + "", + "", + "" + ], + "contactTitle": "", + "contact": [ + "", + "", + "" + ], + "personal": "", + "blog": "", + "linkedIn": "", + "twitter": "", + "Sponsor": "", + "Privacy": "", + "HelpCenter": "", + "Contribute": "", + "Close": "" + }, + "acknowledgement": { + "Title": "", + "Note1": "" + }, + "changelog": { + "Title": "", + "add": "", + "improve": "", + "fix": "" + }, + "pwa": { + "appDescription": "", + "installDescription": "" + } +} From 36d6177d15e3e59ef7c4aec9d9ad35e2cb3eda2f Mon Sep 17 00:00:00 2001 From: Vitor Parras Date: Sat, 22 Aug 2026 16:27:41 -0300 Subject: [PATCH 04/15] Feat(i18n): expand Brazilian Portuguese UI translation --- frontend/locales/pt-BR.json | 1996 +++++++++++++++++------------------ 1 file changed, 998 insertions(+), 998 deletions(-) diff --git a/frontend/locales/pt-BR.json b/frontend/locales/pt-BR.json index b0e21ea49..8e6037b72 100644 --- a/frontend/locales/pt-BR.json +++ b/frontend/locales/pt-BR.json @@ -1,190 +1,190 @@ { "user": { - "SignIn": "", - "SignInWithGoogle": "", - "SignInWithGithub": "", - "SignOut": "", - "SignInToView": "", - "SignInToUse": "", - "InvalidUserToken": "", - "QuotaExceeded": "", - "QuotaSponsorCta": "", - "ViewUsage": "", + "SignIn": "Entrar", + "SignInWithGoogle": "Entrar com Google", + "SignInWithGithub": "Entrar com GitHub", + "SignOut": "Sair", + "SignInToView": "Entrar para visualizar", + "SignInToUse": "Faça login para usar", + "InvalidUserToken": "Token de usuário inválido", + "QuotaExceeded": "Você usou toda a cota gratuita deste mês para este recurso. Ela será redefinida automaticamente no próximo mês.", + "QuotaSponsorCta": "Patrocine-nos", + "ViewUsage": "Ver uso", "Usage": { - "Title": "", - "Note": "", - "IpinfoFeature": "", - "SponsorNote": "" + "Title": "Estatísticas de uso", + "Note": "Recursos avançados incluem uma cota mensal gratuita, redefinida automaticamente no início de cada mês (UTC). Dados avançados de IP contam apenas IPs únicos — consultar um IP que você já pesquisou neste mês nunca consome cota.", + "IpinfoFeature": "Dados avançados de IP (IPs únicos)", + "SponsorNote": "Patrocinar no GitHub aumenta suas cotas mensais." }, - "MyAchievements": "", + "MyAchievements": "Minhas conquistas", "Fields": { - "User": "", - "CreatedAt": "", - "LastLogin": "", - "Level": "", - "Fetching": "", - "SignInMethods": "" + "User": "Usuário", + "CreatedAt": "Criado em", + "LastLogin": "Último login", + "Level": "Nível do usuário", + "Fetching": "Buscando...", + "SignInMethods": "Login" }, "Level": { - "Standard": "", - "Premium": "", - "Owner": "", - "Developer": "", - "HonoraryMember": "" + "Standard": "Usuário padrão", + "Premium": "Usuário premium", + "Owner": "Administrador", + "Developer": "Desenvolvedor", + "HonoraryMember": "Membro honorário" }, "Benefits": { - "Title": "", - "Benefit": "", - "Note1": "", - "Note2": "", - "Current": "", + "Title": "Benefícios e uso", + "Benefit": "Benefícios", + "Note1": "Como produto de código aberto, o IPCheck.ing é gratuito. Mantê-lo exige esforço e custos consideráveis.", + "Note2": "Para manter o projeto sustentável e evitar abuso, alguns recursos avançados exigem login. Todo usuário autenticado recebe uma cota mensal generosa — se ela não for suficiente para você, considere nos patrocinar para liberar limites maiores.", + "Current": "Atual", "Tiers": { "Visitor": { - "Name": "", - "Item1": "", - "Item2": "", - "Item3": "", - "Item4": "", - "Item5": "", - "Item6": "" + "Name": "Visitantes", + "Item1": "Consulta de IP com comparação entre várias fontes", + "Item2": "Verificações de conectividade e disponibilidade de serviços", + "Item3": "Testes de vazamento WebRTC / DNS", + "Item4": "Teste de velocidade, latência global e MTR", + "Item5": "Whois, resolvedor DNS e mais ferramentas avançadas", + "Item6": "Sem cadastro obrigatório — código aberto e auto-hospedável" }, "SignedIn": { - "Name": "", - "Item1": "", - "Item2": "", - "Item3": "", - "Item4": "", - "Item5": "", - "Item6": "", - "Item7": "" + "Name": "Usuários autenticados", + "Item1": "Tudo em Visitantes", + "Item2": "Visualização de dados avançados de IP: pontuação de qualidade, detecção de proxy, tipo de rede e alinhamento geográfico", + "Item3": "Uso do teste de invisibilidade", + "Item4": "Uso do teste aprofundado de vazamento de DNS", + "Item5": "Visualização dos seus emblemas de conquistas", + "Item6": "Perguntas ao IPilot, nosso assistente de IA", + "Item7": "Recursos avançados incluem uma cota mensal gratuita redefinida todo mês" }, "Sponsor": { - "Name": "", - "Item1": "", - "Item2": "", - "Item3": "" + "Name": "Patrocinadores", + "Item1": "Tudo em Usuários autenticados", + "Item2": "Cotas mensais maiores para recursos avançados", + "Item3": "Apoio ao desenvolvimento contínuo deste projeto de código aberto" } } }, "Achievements": { - "Title": "", - "Note": "", - "Get": "", - "GetCount": "", - "NotGet": "", - "NotGetCount": "", - "FooterNote": "", - "Congrats": "", - "CongratsMessage": "", - "NewAchievementIs": "", + "Title": "Conquistas", + "Note": "Ao usar o IPCheck.ing, você pode desbloquear algumas conquistas. Aqui você pode ver todas as suas conquistas.", + "Get": "Obtida", + "GetCount": "Conquistas obtidas", + "NotGet": "Ainda não", + "NotGetCount": "Conquistas pendentes", + "FooterNote": "Às vezes, podem existir conquistas por tempo limitado.", + "Congrats": "🎉 Parabéns", + "CongratsMessage": "Você desbloqueou uma nova conquista!", + "NewAchievementIs": "A nova conquista é:", "Type": { "IAmHuman": { - "Title": "", - "Meet": "" + "Title": "Sou humano", + "Meet": "Use o IPCheck.ing depois de fazer login" }, "BarelyEnough": { - "Title": "", - "Meet": "" + "Title": "Quase suficiente", + "Meet": "Velocidade de download acima de 100 Mb/s" }, "RapidPace": { - "Title": "", - "Meet": "" + "Title": "Ritmo rápido", + "Meet": "Velocidade de download acima de 500 Mb/s" }, "TorrentFlow": { - "Title": "", - "Meet": "" + "Title": "Fluxo de torrent", + "Meet": "Velocidade de download acima de 1000 Mb/s" }, "SteadyGoing": { - "Title": "", - "Meet": "" + "Title": "Ritmo constante", + "Meet": "Velocidade de upload acima de 50 Mb/s" }, "TooFastTooSimple": { - "Title": "", - "Meet": "" + "Title": "Rápido demais, simples demais", + "Meet": "Velocidade de upload acima de 200 Mb/s" }, "SwiftAscent": { - "Title": "", - "Meet": "" + "Title": "Subida veloz", + "Meet": "Velocidade de upload acima de 1000 Mb/s" }, "SurfaceCheck": { - "Title": "", - "Meet": "" + "Title": "Verificação inicial", + "Meet": "Complete um item da checklist de segurança" }, "HalfwayThere": { - "Title": "", - "Meet": "" + "Title": "Meio caminho andado", + "Meet": "Complete 50% da checklist de segurança" }, "FullySecured": { - "Title": "", - "Meet": "" + "Title": "Totalmente protegido", + "Meet": "Complete toda a checklist de segurança" }, "JustInCase": { - "Title": "", - "Meet": "" + "Title": "Por precaução", + "Meet": "Complete um teste de discrição" }, "HiddenWell": { - "Title": "", - "Meet": "" + "Title": "Bem escondido", + "Meet": "Probabilidade de proxy e VPN igual a 0 no teste de discrição" }, "SlipUp": { - "Title": "", - "Meet": "" + "Title": "Escorregão", + "Meet": "Probabilidade de proxy ou VPN maior que 50% no teste de discrição" }, "CleverTrickery": { - "Title": "", - "Meet": "" + "Title": "Truque inteligente", + "Meet": "Visualize todos os atalhos de teclado" }, "EnergySaver": { - "Title": "", - "Meet": "" + "Title": "Economia de energia", + "Meet": "Configure o programa para não executar automaticamente" }, "ResourceHog": { - "Title": "", - "Meet": "" + "Title": "Consumidor de recursos", + "Meet": "Configure múltiplas atualizações de disponibilidade" }, "MakingBigNews": { - "Title": "", - "Meet": "" + "Title": "Virando notícia", + "Meet": "Chamadas de funções avançadas acima de 1000 vezes" }, "GenerousDonor": { - "Title": "", - "Meet": "" + "Title": "Doador generoso", + "Meet": "Doe para o projeto IPCheck.ing" }, "ItIsOpen": { - "Title": "", - "Meet": "" + "Title": "Está aberto", + "Meet": "Encontre um site bloqueado na Verificação de censura" }, "CuriousCat": { - "Title": "", - "Meet": "" + "Title": "Curiosidade em ação", + "Meet": "Consulte informações do IPCheck.ing na Pesquisa Whois" }, "CrossingTheWall": { - "Title": "", - "Meet": "" + "Title": "Cruzando o muro", + "Meet": "Todos os 8 IPs de teste são diferentes no Teste de regras" }, "GlobeTrotter": { - "Title": "", - "Meet": "" + "Title": "Viajante global", + "Meet": "Alcance 10 países ou regiões no seu histórico de IP" }, "HelloWorld": { - "Title": "", - "Meet": "" + "Title": "Olá, mundo", + "Meet": "Compartilhe um status no Earth Online" }, "AddSweetAdd": { - "Title": "", - "Meet": "" + "Title": "Adição certeira", + "Meet": "Adicione um alvo personalizado no teste de conectividade" }, "PrettyDirty": { - "Title": "", - "Meet": "" + "Title": "Bem suspeito", + "Meet": "Qualquer cartão de IP mostra pontuação de qualidade 1" }, "SqueakyClean": { - "Title": "", - "Meet": "" + "Title": "Limpíssimo", + "Meet": "Qualquer cartão de IP mostra pontuação de qualidade 100" }, "WalkWithPenguins": { - "Title": "", - "Meet": "" + "Title": "Passeio na Antártida", + "Meet": "Qualquer cartão de IP localiza você na Antártida" } } } @@ -349,1073 +349,1073 @@ "DocsSignInMessage": "Faça login para perguntar ao IPilot" }, "securitychecklist": { - "Title": "", - "Note": "", - "Note2": "", - "Progress": "", - "Item": "", - "Items": "", - "Priority": "", - "Ignore": "", - "alert-total": "", - "alert-checked": "", - "alert-ignored": "", - "alert-unchecked": "", - "Checked": "", - "Ignored": "", - "Unchecked": "", - "Basic": "", - "Optional": "", - "Essential": "", - "Advanced": "", - "Reset": "", - "ShowAll": "", - "ShowUnchecked": "", - "ShowIgnored": "", - "ShowChecked": "" + "Title": "Checklist de segurança", + "Note": "Esta é uma checklist abrangente de cibersegurança, cobrindo aspectos como segurança de senhas, segurança de navegação e segurança de dispositivos móveis. No ambiente atual, vazamentos de informações podem causar perdas significativas; portanto, é necessário garantir que protegemos nossa privacidade e reforçamos a segurança do nosso acesso enquanto estamos conectados à internet.", + "Note2": "Esta lista contém muitos itens; você não precisa verificar tudo de uma vez. O progresso será salvo no navegador, e você poderá voltar a qualquer momento para continuar as verificações.", + "Progress": "Progresso da verificação", + "Item": "Item", + "Items": "Item(ns) ", + "Priority": "Prioridade", + "Ignore": "Ignorar", + "alert-total": "A verificação de segurança tem um total de", + "alert-checked": "Você concluiu", + "alert-ignored": "Ignorado", + "alert-unchecked": "Não verificado.", + "Checked": "Verificado", + "Ignored": "Ignorado", + "Unchecked": "Não verificado", + "Basic": "Básico", + "Optional": "Opcional", + "Essential": "Essencial", + "Advanced": "Avançado", + "Reset": "Redefinir checklist", + "ShowAll": "Mostrar tudo", + "ShowUnchecked": "Mostrar não verificados", + "ShowIgnored": "Mostrar ignorados", + "ShowChecked": "Mostrar verificados" }, "curl": { - "Title": "", - "Note1": "", - "Note2_1": "", - "Note2_2": "", - "Note3": "", - "Note4": "", - "getIPv4": "", - "getIPv6": "", - "get6and4": "", - "notAvailable": "" + "Title": "API de linha de comando", + "Note1": "Ei, pelo visto você também gosta de usar o terminal?", + "Note2_1": "Você pode usar o comando", + "Note2_2": "para obter o IP da sua máquina no terminal", + "Note3": "é um caminho opcional; adicioná-lo obtém as informações geográficas do IP", + "Note4": "é sua chave de API do IPCheck.ing", + "getIPv4": "Obter o endereço IPv4 da máquina local", + "getIPv6": "Obter o endereço IPv6 da máquina local", + "get6and4": "Obter o IP de saída de rede preferencial da máquina local", + "notAvailable": "Este recurso ainda não está disponível" }, "browserinfo": { - "Title": "", - "Note": "", - "Note2": "", + "Title": "Informações do navegador", + "Note": "Esta página exibe dois tipos de informação do navegador: informações básicas derivadas do User Agent e uma impressão digital do navegador calculada por várias métricas de dados. Impressões digitais de navegador são semelhantes às digitais da palma da mão; quanto mais informações seu navegador expõe, maior é sua singularidade para os sites, tornando você mais fácil de rastrear. Às vezes, mesmo alternar para modo anônimo ou trocar de navegador pode não alterar sua impressão digital.", + "Note2": "Os métodos para calcular impressões digitais de navegador são diversos, e abordagens técnicas diferentes podem levar a resultados inconsistentes. Aqui, usamos um método relativamente simples para o cálculo.", "browser": { - "Infos": "", - "browserName": "", - "engineName": "", - "osName": "", - "deviceVendor": "", - "cpuArchitecture": "", - "gpu": "", - "cpuCores": "", - "languages": "", - "timezone": "", - "display": "", - "connection": "", - "touchPoints": "", - "doNotTrack": "", - "doNotTrackOn": "", - "doNotTrackOff": "", - "doNotTrackUnset": "", - "pdfViewer": "", - "pdfViewerYes": "", - "pdfViewerNo": "", - "cookieEnabled": "", - "cookieEnabledTrue": "", - "cookieEnabledFalse": "" + "Infos": "Informações do navegador", + "browserName": "Nome do navegador", + "engineName": "Motor", + "osName": "Sistema operacional", + "deviceVendor": "Fabricante", + "cpuArchitecture": "Arquitetura da CPU", + "gpu": "GPU", + "cpuCores": "Núcleos da CPU", + "languages": "Idiomas", + "timezone": "Fuso horário", + "display": "Tela", + "connection": "Conexão", + "touchPoints": "Pontos de toque", + "doNotTrack": "Do Not Track", + "doNotTrackOn": "Ativado", + "doNotTrackOff": "Desativado", + "doNotTrackUnset": "Não definido", + "pdfViewer": "Visualizador de PDF", + "pdfViewerYes": "Sim", + "pdfViewerNo": "Não", + "cookieEnabled": "Cookies ativados", + "cookieEnabledTrue": "Sim", + "cookieEnabledFalse": "Não" }, "fingerprint": { - "Infos": "", - "fingerprint": "", - "changeOption": "", - "browserTips": "" + "Infos": "Informações da impressão digital", + "fingerprint": "Código da impressão digital", + "changeOption": "Você pode tentar alterar os dados usados para calcular a impressão digital do navegador e observar as variações (lembre-se de comparar em modo anônimo).", + "browserTips": "Alguns navegadores, como o Safari, protegem usuários contra rastreamento gerando valores diferentes de impressão digital a cada vez, mesmo quando os mesmos dados são selecionados." }, "options": { - "audio": "", - "canvas": "", - "fonts": "", - "hardware": "", - "locales": "", - "permissions": "", - "plugins": "", - "screen": "", - "system": "", - "webgl": "", - "math": "", - "intl": "", - "mediaDevices": "", - "speech": "", - "mathml": "", - "webrtc": "" + "audio": "Áudio", + "canvas": "Canvas", + "fonts": "Fontes", + "hardware": "Hardware", + "locales": "Idiomas", + "permissions": "Permissões", + "plugins": "Plugins", + "screen": "Tela", + "system": "Versão do navegador", + "webgl": "WebGL", + "math": "Matemática", + "intl": "Formatadores Intl", + "mediaDevices": "Dispositivos de mídia", + "speech": "Síntese de fala", + "mathml": "MathML", + "webrtc": "WebRTC" }, "components": { - "title": "", - "note": "" + "title": "Componentes da impressão digital", + "note": "Cada componente abaixo contribui para o hash acima. Ative ou desative itens no painel Impressão digital para excluí-los da impressão digital." }, - "calError": "", - "calculating": "" + "calError": "Falha ao calcular as informações do navegador. Atualize e tente novamente.", + "calculating": "Calculando..." }, "censorshipcheck": { - "Title": "", - "Note": "", - "Note2": "", - "Placeholder": "", - "Country": "", - "Status": "", - "City": "", - "Network": "", - "invalidURL": "", - "fetchError": "", - "HistoryTitle": "", - "SummaryFlagged": "", - "SummaryClean": "", - "SummarySparseFlagged": "", - "SummarySparseClean": "", - "NoData": "", - "OoniError": "", - "TierConfirmed": "", - "TierLikely": "", - "TierSigns": "", - "Measurements": "", - "AnomalyShare": "", - "MethodDns": "", - "MethodTcpIp": "", - "MethodHttpFailure": "", - "MethodHttpDiff": "", - "RealtimeTitle": "", - "ResultsHint": "", - "SectionFlagged": "", - "PresetHighRisk": "", - "PresetOpen": "", - "RunVerify": "", - "RealtimeNote": "", - "RealtimeFooterNote": "" + "Title": "Verificação de censura", + "Note": "Consulte onde um site foi bloqueado ou sofreu interferência nos últimos 30 dias — e de que forma — com base em dados abertos da rede global de voluntários da OONI. Depois, você pode verificar regiões suspeitas com um teste de sonda em tempo real do Globalping. Em alguns países, a censura na internet é claramente definida por lei; em outros, é mais vaga.", + "Note2": "Digite uma URL ou nome de domínio para iniciar a consulta:", + "Placeholder": "URL ou nome de domínio", + "Country": "Região", + "Status": "Status", + "City": "Cidade", + "Network": "Rede", + "invalidURL": "URL ou nome de domínio inválido", + "fetchError": "Não foi possível obter os resultados do teste", + "HistoryTitle": "Observações globais nos últimos 30 dias", + "SummaryFlagged": "Sinais de bloqueio ou interferência foram observados em {n} países/regiões nos últimos 30 dias.", + "SummaryClean": "Nenhum sinal claro de bloqueio foi observado em {n} países/regiões nos últimos 30 dias.", + "SummarySparseFlagged": "Nos últimos 30 dias, apenas {total} países/regiões observaram este site, e {n} deles mostraram sinais de bloqueio ou interferência.", + "SummarySparseClean": "Nos últimos 30 dias, apenas {total} países/regiões observaram este site ({m} medições) — há poucos dados para dizer se ele está bloqueado.", + "NoData": "Nenhuma medição para este site nos últimos 30 dias — o que não significa que ele não esteja bloqueado. Você pode marcar abaixo os países com censura intensa e executar um teste em tempo real.", + "OoniError": "Os dados históricos de observação estão temporariamente indisponíveis — a API upstream pode estar com problemas. Você ainda pode marcar países abaixo e executar um teste em tempo real.", + "TierConfirmed": "Bloqueio confirmado", + "TierLikely": "Provável bloqueio", + "TierSigns": "Interferência parcial", + "Measurements": "{n} medições", + "AnomalyShare": "{pct}% anômalas", + "MethodDns": "Manipulação de DNS", + "MethodTcpIp": "Bloqueio TCP", + "MethodHttpFailure": "Falha de conexão", + "MethodHttpDiff": "Manipulação de conteúdo", + "RealtimeTitle": "Teste em tempo real", + "ResultsHint": "Os resultados aparecerão aqui depois que você selecionar sondas e executar o teste.", + "SectionFlagged": "Países sinalizados", + "PresetHighRisk": "Países com censura intensa", + "PresetOpen": "Países geralmente abertos", + "RunVerify": "Executar teste em tempo real", + "RealtimeNote": "Você pode selecionar várias sondas para testes de conectividade em tempo real e avaliar de forma abrangente o status de bloqueio do site. Ao executar o teste, 2 sondas serão executadas por país; resultados correspondentes das duas sondas indicam maior confiabilidade.", + "RealtimeFooterNote": "As sondas em tempo real geralmente rodam em redes de datacenter; seus resultados podem diferir de observações reais em redes residenciais, já que alguns sites podem bloquear acesso vindo de datacenters. Leia os resultados em conjunto e faça um julgamento com base na situação real." }, "invisibilitytest": { - "Title": "", - "Note": "", - "Note2": "", - "fetchError": "", - "scriptLoadError": "", - "yourIP": "", - "proxyScore": "", - "VPNScore": "", - "isProxy": "", - "notProxy": "", - "isVPN": "", - "notVPN": "", - "itemName": "", - "itemProxyResult": "", - "itemComment": "", - "agreement": "", + "Title": "Teste de invisibilidade", + "Note": "Quando você usa um proxy ou VPN, os sites que visita realmente não sabem que você está usando um proxy? Você está mesmo invisível? Usaremos vários métodos para tentar calcular vulnerabilidades no seu disfarce e encontrar evidências de que você está usando um proxy. Embora alguns resultados possam não ser totalmente precisos, este teste ainda é altamente informativo.", + "Note2": "Após clicar em Executar, o sistema rodará um script que geralmente conclui todos os testes em até 10 segundos. Durante esse processo, suas informações de IP, navegador etc. serão coletadas e analisadas em segundo plano.", + "fetchError": "Não foi possível obter os resultados do teste", + "scriptLoadError": "O script de teste falhou ao carregar, então o teste não pode continuar. Verifique sua rede e tente novamente.", + "yourIP": "Seu IP principal de teste é", + "proxyScore": "Porcentagem de proxy", + "VPNScore": "Porcentagem de VPN", + "isProxy": "Você pode estar usando um proxy.", + "notProxy": "Nenhuma evidência de uso de proxy foi encontrada.", + "isVPN": "Você pode estar usando uma VPN.", + "notVPN": "Nenhuma evidência de uso de VPN foi encontrada.", + "itemName": "Item de teste", + "itemProxyResult": "Resultado do teste", + "itemComment": "Análise", + "agreement": "Concordo", "blocklist": { "proxy": { - "title": "", - "positive": "", - "negative": "" + "title": "Lista de proxies", + "positive": "Seu IP está na lista de proxies.", + "negative": "Seu IP não está na lista de proxies." }, "vpn": { - "title": "", - "positive": "", - "negative": "" + "title": "Lista de VPNs", + "positive": "Seu IP está na lista de VPNs.", + "negative": "Seu IP não está na lista de VPNs." }, "vpnExitNode": { - "title": "", - "positive": "", - "negative": "" + "title": "Lista de nós de saída VPN", + "positive": "Seu IP está na lista de nós de saída VPN.", + "negative": "Seu IP não está na lista de nós de saída VPN." } }, "tor": { - "title": "", - "positive": "", - "negative": "" + "title": "Nó de saída TOR", + "positive": "Seu IP está na lista de nós de saída TOR.", + "negative": "Seu IP não está na lista de nós de saída TOR." }, "headers": { - "title": "", - "positive": "", - "negative": "" + "title": "Cabeçalhos HTTP", + "positive": "Seus cabeçalhos HTTP contêm informações de proxy, indicando que você pode estar usando um proxy.", + "negative": "Seus cabeçalhos HTTP não contêm informações de proxy." }, "datacenter": { - "title": "", - "positive": "", - "negative": "" + "title": "IP de datacenter", + "positive": "Seu IP é de datacenter, algo que usuários comuns normalmente não usam.", + "negative": "Seu IP não é de datacenter." }, "tcp": { - "title": "", - "positive": "", - "computer": "", - "server": "", - "negative": "" + "title": "Impressão digital TCP", + "positive": "O sistema ao qual você está se conectando é diferente do sistema do seu computador, indicando que você pode estar usando um proxy. Embora o julgamento do sistema possa não ser preciso, é certo que os dois sistemas são diferentes.", + "computer": "O sistema do seu computador é: ", + "server": "O sistema ao qual você está se conectando pode ser: ", + "negative": "O sistema ao qual você está se conectando é igual ao sistema do seu computador." }, "timezone": { - "title": "", - "positive": "", - "computer": "", - "server": "", - "negative": "" + "title": "Diferença de fuso horário", + "positive": "Seu fuso horário não corresponde ao fuso horário da geolocalização do seu IP, indicando que você pode estar usando um proxy.", + "computer": "Seu fuso horário é: ", + "server": "O fuso horário da geolocalização do seu IP é: ", + "negative": "Seu fuso horário corresponde ao fuso horário da geolocalização do seu IP." }, "net": { - "title": "", - "positive": "", - "negative": "" + "title": "Comportamento de rede", + "positive": "Com base no comportamento de rede do cliente, foi identificado que você pode estar usando um proxy ou VPN.", + "negative": "Com base no comportamento de rede do cliente, foi identificado que você não está usando proxy nem VPN." }, "webrtc": { - "title": "", - "positive": "", - "ipsAre": "", - "negative": "" + "title": "Detecção WebRTC", + "positive": "O WebRTC detectou vários IPs de regiões diferentes, indicando que você pode estar usando proxy ou VPN.", + "ipsAre": "Os IPs são: ", + "negative": "Nenhuma evidência de vários IPs de regiões diferentes foi encontrada na detecção WebRTC." }, "flow": { - "title": "", - "positive": "", - "negative": "" + "title": "Características do tráfego", + "positive": "Com base nas características do tráfego TCP/IP, foi identificado que você pode estar usando proxy ou VPN.", + "negative": "Com base nas características do tráfego TCP/IP, foi identificado que você não está usando proxy nem VPN." }, "latencyVSWS": { - "title": "", - "positive": "", - "fromTCP": "", - "fromWS": "", - "negative": "" + "title": "Latência TCP vs WebSocket", + "positive": "A diferença entre a latência TCP e WebSocket é grande, indicando que você pode estar usando proxy ou VPN.", + "fromTCP": "Valor de latência TCP: ", + "fromWS": "Valor de latência WebSocket: ", + "negative": "A diferença entre a latência TCP e WebSocket não é grande, indicando que talvez você não esteja usando proxy nem VPN." }, "latencyVSPing": { - "title": "", - "positive": "", - "fromTCP": "", - "fromPing": "", - "negative": "" + "title": "Latência TCP vs Ping", + "positive": "A diferença entre a latência TCP e Ping é grande, indicando que você pode estar usando proxy ou VPN.", + "fromTCP": "Valor de latência TCP: ", + "fromPing": "Valor de latência Ping: ", + "negative": "A diferença entre a latência TCP e Ping não é grande, indicando que talvez você não esteja usando proxy nem VPN." }, "highlatency": { - "title": "", - "positive": "", - "negative": "" + "title": "Análise de alta latência", + "positive": "A análise de alta latência indica que você pode estar usando proxy ou VPN.", + "negative": "Nenhuma evidência de uso de proxy ou VPN foi encontrada na análise de alta latência." } }, "personacheck": { - "Title": "", - "Note": "", - "NoteVs": "", - "Note2": "", + "Title": "Verificação aprofundada de persona", + "Note": "Existe um teste antigo: se parece um pato, nada como um pato e grasna como um pato, provavelmente é um pato. Sites leem você da mesma forma — seu IP pode dizer que você está no país A, mas seu fuso horário, idiomas, fontes instaladas, teclado e uma dúzia de outros sinais contam suas próprias histórias; juntos, eles formam a persona que um site realmente enxerga. Esta ferramenta mede essa persona, compara com a pessoa local que você quer parecer ser e mostra exatamente onde as duas divergem.", + "NoteVs": "Ela faz uma pergunta diferente do Teste de invisibilidade. Aquele verifica se o próprio proxy pode ser detectado; este verifica quem os sites acham que estão vendo — e quão distante essa imagem está daquela que você espera apresentar.", + "Note2": "Comece escolhendo o país no qual você quer parecer local.", "axis": { - "match": "", - "coherence": "", - "leak": "" + "match": "Correspondência com o alvo", + "coherence": "Consistência interna", + "leak": "Vazamento de identidade" }, "zone": { - "expected": "", - "optional": "", - "run": "" + "expected": "Identidade esperada", + "optional": "Sinais mais precisos (opcional)", + "run": "Executar a verificação" }, - "selectCountry": "", - "noCountryMatch": "", - "noProfile": "", - "languageSingle": "", - "languageMulti": "", - "timezoneMulti": "", + "selectCountry": "Pesquisar um país", + "noCountryMatch": "Nenhum país correspondente", + "noProfile": "Este território não tem perfil local para comparação.", + "languageSingle": "Este país tem um único idioma principal.", + "languageMulti": "Este país tem vários idiomas — escolha aquele que você quer apresentar.", + "timezoneMulti": "Este país abrange vários fusos horários — escolha aquele que você quer apresentar.", "hourCycle": { - "h11": "", - "h12": "", - "h23": "", - "h24": "" + "h11": "Relógio de 12 horas (0–11)", + "h12": "Relógio de 12 horas", + "h23": "Relógio de 24 horas", + "h24": "Relógio de 24 horas (1–24)" }, "profile": { - "timezone": "" + "timezone": "Fuso horário" }, - "runCompare": "", - "runError": "", + "runCompare": "Executar a verificação", + "runError": "A verificação não pôde ser concluída.", "source": { - "ipinfo": "", - "webrtc": "", - "dnsleak": "" + "ipinfo": "Consulta de IP", + "webrtc": "WebRTC", + "dnsleak": "Vazamento de DNS" }, "report": { "grade": { - "A": "", - "B": "", - "C": "", - "D": "", - "unknown": "" + "A": "🎉 Você parece totalmente local de {country}", + "B": "😊 Você parece um pouco local de {country}", + "C": "🤔 Você não parece exatamente local de {country}", + "D": "😅 Você não parece local de {country}", + "unknown": "🤷 Sinais insuficientes para dar uma nota" }, "gradeNote": { - "A": "", - "B": "", - "C": "", - "D": "", - "unknown": "" + "A": "Todos os sinais que conseguimos ler concordam com a persona escolhida.", + "B": "A persona se sustenta em grande parte, mas alguns sinais apontam para outro lugar.", + "C": "Vários sinais discordam da persona, ou entre si — os resultados abaixo mostram quais.", + "D": "A maioria dos sinais aponta para algum lugar diferente desta persona.", + "unknown": "Execute primeiro os testes subjacentes — há poucos dados para avaliar." }, "counts": { - "leak": "", - "warning": "", - "match": "", - "unknown": "", - "notApplicable": "" + "leak": "{n} expondo", + "warning": "{n} inconsistente(s)", + "match": "{n} aprovado(s)", + "unknown": "{n} não medido(s)", + "notApplicable": "{n} não aplicável(is)" }, - "nothingActionable": "", - "howToFix": "", + "nothingActionable": "Nada a corrigir — todo sinal mensurável combina com a persona.", + "howToFix": "Como corrigir", "verdict": { - "match": "", - "mismatch": "", - "leak": "", - "unnatural": "", - "unknown": "", - "not-applicable": "" + "match": "Combina com a persona.", + "mismatch": "Contradiz a persona.", + "leak": "Expõe seu ambiente real.", + "unnatural": "Internamente inconsistente — dois sinais que deveriam concordar contam histórias diferentes, o que chama mais atenção do que uma divergência simples.", + "unknown": "Não mensurável agora.", + "not-applicable": "Nenhuma conclusão é possível aqui." }, "visibility": { - "public": "", - "probed": "", - "risk-engine": "" + "public": "Qualquer site vê isto", + "probed": "Sites que sondam este sinal", + "risk-engine": "Sistemas antifraude" }, - "profileTitle": "", + "profileTitle": "Detalhes da medição", "state": { - "match": "", - "mismatch": "", - "unnatural": "", - "leak": "", - "unknown": "", - "not-applicable": "" + "match": "Aprovado", + "mismatch": "Não local", + "unnatural": "Inconsistente", + "leak": "Exposto", + "unknown": "Não medido", + "not-applicable": "N/A" } }, "detail": { - "expected": "", - "actual": "", - "disputed": "", - "v4": "", - "v6": "", - "candidateCount": "", - "matching": "", - "ipType": "", - "expectedOffset": "", - "actualOffset": "", - "sameOffset": "", - "primary": "", - "primaryExpected": "", - "demoted": "", - "timeZone": "", - "reportedOffset": "", - "zoneOffset": "", - "samples": "", - "expectedScripts": "", - "installedScripts": "", - "expectedLanguages": "", - "voiceLanguages": "", - "voiceCount": "", - "expectedLayout": "", - "actualLayout": "", - "headerPrimary": "", - "scriptPrimary": "", - "headerLanguages": "", - "ipZone": "", - "browserZone": "", - "ipOffset": "", - "browserOffset": "", - "colo": "", - "gpsTimezone": "", - "accuracyMetres": "", - "cardIssuer": "", - "cardNetwork": "", - "cardTier": "", - "cardType": "" + "expected": "Esperado", + "actual": "Atual", + "disputed": "Fontes discordam", + "v4": "IPv4", + "v6": "IPv6", + "candidateCount": "Candidatos", + "matching": "Corresponde ao esperado", + "ipType": "Tipo de IP", + "expectedOffset": "Offset esperado", + "actualOffset": "Offset atual", + "sameOffset": "Mesmo offset", + "primary": "Idioma principal", + "primaryExpected": "Principal esperado", + "demoted": "Listado, mas não em primeiro", + "timeZone": "Fuso horário", + "reportedOffset": "Offset de Date", + "zoneOffset": "Offset da zona", + "samples": "Amostras", + "expectedScripts": "Sistemas de escrita esperados", + "installedScripts": "Sistemas de escrita instalados", + "expectedLanguages": "Idiomas esperados", + "voiceLanguages": "Idiomas das vozes", + "voiceCount": "Vozes", + "expectedLayout": "Layout esperado", + "actualLayout": "Layout atual", + "headerPrimary": "Cabeçalho principal", + "scriptPrimary": "Script principal", + "headerLanguages": "Idiomas do cabeçalho", + "ipZone": "Fuso horário do IP", + "browserZone": "Fuso horário do navegador", + "ipOffset": "Offset do IP", + "browserOffset": "Offset do navegador", + "colo": "PoP da Cloudflare", + "gpsTimezone": "Fuso horário na sua posição", + "accuracyMetres": "Precisão (m)", + "cardIssuer": "Banco emissor", + "cardNetwork": "Bandeira do cartão", + "cardTier": "Categoria do cartão", + "cardType": "Tipo do cartão" }, "checks": { "ip-country": { - "title": "", - "fix": "", - "what": "" + "title": "País do IP", + "fix": "Parte das suas saídas cai fora do país-alvo — com várias saídas de proxy, cartões diferentes realmente podem mostrar países diferentes, e qualquer uma dessas saídas é o que algum site verá. As frações acima dizem quantas saídas concordam. Roteie tudo por nós no país-alvo; se uma única fonte discordar sobre um IP, esse nó pode apenas estar rotulado incorretamente no banco de dados, algo que você não consegue corrigir do seu lado.", + "what": "Em qual país os cartões de consulta de IP da página inicial colocam seu IP de saída — toda fonte é lida, e toda saída precisa estar no país-alvo." }, "webrtc-leak": { - "title": "", - "fix": "", - "what": "" + "title": "Exposição WebRTC", + "fix": "Bloqueie o WebRTC ou use uma configuração em que ele só consiga enxergar o IP do proxy. O custo é real: chamadas de vídeo e conferências de voz deixam de funcionar. Uma extensão de navegador que filtra WebRTC é mais suave do que desativá-lo completamente, mas a própria extensão deixa rastros.", + "what": "Se o WebRTC entrega um IP de país diferente do da sua persona." }, "dns-resolver-country": { - "title": "", - "fix": "", - "what": "" + "title": "País do resolvedor DNS", + "fix": "Use um resolvedor no país-alvo ou deixe o proxy cuidar da resolução DNS. Observe que um resolvedor público como 1.1.1.1 não expõe sua localização, mas também não dá suporte à sua persona — ele é igualmente neutro para todos os países.", + "what": "Em qual país ficam os resolvedores DNS que respondem para você." }, "asn-type": { - "title": "", - "fix": "", - "what": "" + "title": "Tipo de IP", + "fix": "Uma faixa de datacenter é o sinal mais forte de \"isto é um proxy\" que um IP carrega. Uma saída residencial corrige isso diretamente, mas custa consideravelmente mais e varia muito em qualidade.", + "what": "Se seu IP pertence a um ISP residencial ou a uma faixa de datacenter." }, "timezone-vs-persona": { - "title": "", - "fix": "", - "what": "" + "title": "Fuso horário vs persona", + "fix": "Altere o fuso horário no nível do sistema operacional — assim, toda API do navegador permanece consistente consigo mesma. Extensões que substituem o fuso horário frequentemente alteram apenas parte dele, criando as contradições sinalizadas nas verificações de coerência, o que é pior do que não alterá-lo.", + "what": "Se o fuso horário do seu navegador é o que uma pessoa local teria." }, "language-match": { - "title": "", - "fix": "", - "what": "" + "title": "Idioma do navegador", + "fix": "Altere o idioma nas configurações do sistema ou do navegador, em vez de reescrever um único cabeçalho HTTP. A ordem também importa: sites leem a lista inteira, e uma pessoa local raramente começaria por um idioma estrangeiro.", + "what": "Se o primeiro idioma informado pelo navegador é um idioma que uma pessoa local usaria." }, "script-fonts": { - "title": "", - "fix": "", - "what": "" + "title": "Fontes do sistema de escrita", + "fix": "Uma máquina configurada para este locale inclui fontes desse sistema de escrita. Instalá-las manualmente é possível, mas raramente vale a pena: isso altera um sinal enquanto o restante do sistema permanece no idioma original, o que normalmente cria mais contradições do que resolve. Se isso precisa se sustentar, o idioma do próprio sistema é o ponto a alterar.", + "what": "Se fontes para o sistema de escrita deste país estão instaladas na máquina." }, "speech-voices": { - "title": "", - "fix": "", - "what": "" + "title": "Vozes de síntese de fala", + "fix": "Pacotes de texto para fala acompanham o idioma com que seu sistema operacional foi instalado, e praticamente nenhum proxy ou extensão de spoofing toca neles. Adicionar um pacote de voz para o idioma-alvo é uma correção real, com a mesma ressalva das fontes: um sinal alterado dentro de um sistema que continua inalterado costuma parecer deliberado.", + "what": "Quais idiomas os pacotes de texto para fala do seu sistema cobrem." }, "keyboard-layout": { - "title": "", - "fix": "", - "what": "" + "title": "Layout do teclado", + "fix": "Seu layout físico de teclado não é o típico do país-alvo. Apenas navegadores Chromium expõem isso, então é um sinal mais restrito que os outros — mas, onde é legível, é difícil falsificá-lo sem trocar o teclado real.", + "what": "Qual layout físico de teclado sua máquina informa." }, "edge-geo-consensus": { - "title": "", - "fix": "", - "what": "" + "title": "Geolocalização de borda", + "fix": "A leitura da própria Cloudflare sobre seu IP de saída discorda do país-alvo. Quando isso e a consulta de IP se contradizem, é mais provável que o IP esteja rotulado incorretamente em um banco de dados do que realmente se movendo — mas todo site atrás da Cloudflare vê esse veredito, não o outro.", + "what": "A leitura própria da Cloudflare sobre seu IP de saída, formada independentemente dos bancos de dados acima." }, "accept-language-header": { - "title": "", - "fix": "", - "what": "" + "title": "Cabeçalho de idioma vs script", + "fix": "O cabeçalho Accept-Language que seu navegador envia pela rede não corresponde ao que navigator.languages informa aos scripts. Quase nada produz isso, exceto uma extensão que reescreveu um canal e deixou o outro intacto. Altere o idioma nas configurações do navegador ou do sistema — isso move ambos de uma vez.", + "what": "Se o cabeçalho de idioma enviado pela rede corresponde ao que os scripts recebem. Deliberadamente independente do país-alvo: passar aqui só diz que seus dois canais de idioma concordam — se o idioma em si combina é papel da verificação Idioma do navegador. Spoofs de idioma aplicados pela metade falham exatamente aqui." }, "ip-timezone-vs-browser": { - "title": "", - "fix": "", - "what": "" + "title": "Fuso horário do IP vs navegador", + "fix": "O fuso horário do seu IP de saída e o informado pelo navegador são lugares diferentes. Isso vale qualquer que seja o país-alvo: uma máquina real está em algum lugar, e ambos deveriam dizer o mesmo lugar. Normalmente significa que o proxy moveu seu IP, mas não o relógio do sistema.", + "what": "Se o fuso horário do seu IP e o informado pelo navegador concordam." }, "gps-location": { - "title": "", - "fix": "", - "what": "" + "title": "Localização do dispositivo (GPS)", + "fix": "Seu dispositivo informa estar fisicamente em um país diferente daquele que você está tentando apresentar. Nenhum proxy toca nisso: qualquer site que peça sua localização recebe a resposta real, e extensões de navegador que falsificam isso normalmente deixam padrões de precisão e tempo intactos. As opções realistas são negar permissão de localização para sites que pedirem ou aceitar que este sinal não se sustentará.", + "what": "Qual país seu dispositivo informa como sua localização física." }, "payment-region": { - "title": "", - "fix": "", - "what": "" + "title": "País emissor do cartão", + "fix": "Seu cartão foi emitido em um país diferente daquele que você está apresentando. Comerciantes veem o país emissor antes de quase qualquer outra coisa, e sistemas de risco de pagamento pesam isso fortemente contra o endereço e o IP. Não há correção no navegador para isso — é uma propriedade do próprio cartão.", + "what": "Qual país emitiu o cartão por trás do prefixo informado." }, "intl-number-format": { - "title": "", - "what": "", - "fix": "" + "title": "Formato de número", + "what": "Como seu navegador agrupa dígitos e marca decimais.", + "fix": "Seu navegador agrupa números da forma da própria região, não da forma do país-alvo. Isso é separado do idioma: uma máquina pode carregar a tag de idioma correta enquanto a região ainda aponta para outro lugar. Altere a região — não apenas o idioma — nas configurações do sistema: Região no Windows, Idioma e Região no macOS." }, "intl-date-format": { - "title": "", - "what": "", - "fix": "" + "title": "Formato de data", + "what": "A ordem e os separadores que seu navegador usa ao escrever uma data.", + "fix": "Seu navegador escreve datas em uma ordem diferente ou com separadores diferentes dos de uma pessoa local. Como o formato de número, isso segue a região do sistema, não o idioma, e alterar a região é o que move esse sinal." }, "intl-hour-cycle": { - "title": "", - "what": "", - "fix": "" + "title": "Formato de relógio", + "what": "Se seu navegador apresenta relógio de 12 ou 24 horas — a convenção que os sites veem ao formatar horários.", + "fix": "O formato do relógio segue o idioma do navegador, não a opção de 24 horas do sistema — a maioria dos navegadores ignora completamente essa chave (Safari é a principal exceção). Para apresentar a convenção do país-alvo, altere o idioma ou a região do navegador para um locale desse país." }, "timezone-authenticity": { - "title": "", - "what": "", - "fix": "" + "title": "Autenticidade do fuso horário", + "what": "Se o fuso horário informado pelo navegador é o real da máquina.", + "fix": "A zona informada pelo navegador não é a que a máquina realmente usa: Intl e Date discordam, ou datas passadas não seguem as regras de horário de verão dessa zona. Isso importa porque significa que a verificação de fuso horário acima passou com um valor que não é real — um site que lê o relógio diretamente percebe a inconsistência. Defina a zona no sistema operacional, em vez de usar uma extensão, e tanto o offset atual quanto as regras históricas mudam juntos." } }, "reason": { - "no-marker-script": "", - "bin-lookup-failed": "", - "bin-not-recognized": "", - "font-probe-unavailable": "", - "no-voice-packs": "", - "keyboard-api-unavailable": "", - "no-layout-expectation": "", - "no-persona-languages": "", - "geolocation-unsupported": "", - "ip-no-country": "", - "ip-no-type": "", - "ip-no-timezone": "", - "webrtc-no-candidates": "", - "dns-no-country": "" + "no-marker-script": "Nenhum sistema de escrita marcador", + "bin-lookup-failed": "Não foi possível acessar o serviço de consulta de cartão — os dígitos ainda podem estar corretos; tente novamente mais tarde.", + "bin-not-recognized": "O banco de dados de cartões não reconheceu este prefixo — erro de digitação é a causa mais comum; confira os dígitos e execute novamente.", + "font-probe-unavailable": "Canvas indisponível", + "no-voice-packs": "Nenhum pacote de voz instalado", + "keyboard-api-unavailable": "Não exposto por este navegador", + "no-layout-expectation": "Nenhuma expectativa de layout para este país", + "no-persona-languages": "A persona não tem idioma definido", + "geolocation-unsupported": "Geolocalização sem suporte", + "ip-no-country": "A consulta de IP não retornou país", + "ip-no-type": "Nenhuma fonte informou o tipo de rede", + "ip-no-timezone": "Nenhuma fonte informou fuso horário", + "webrtc-no-candidates": "O WebRTC não produziu candidatos", + "dns-no-country": "Nenhum resolvedor pôde ser localizado" }, "optional": { - "gpsNote": "", - "gpsButton": "", - "gpsGranted": "", + "gpsNote": "A posição GPS é uma das dimensões que a verificação pode medir — compartilhe-a se também quiser compará-la.", + "gpsButton": "Compartilhar localização", + "gpsGranted": "Localização recebida.", "gpsFailed": { - "denied": "", - "timeout": "", - "unavailable": "", - "unsupported": "" + "denied": "Permissão negada — esta verificação será relatada como não medida.", + "timeout": "A solicitação de localização excedeu o tempo limite.", + "unavailable": "Seu dispositivo não conseguiu determinar uma posição.", + "unsupported": "Este navegador não oferece suporte a geolocalização." }, - "cardNote": "", - "cardError": "", - "cardReady": "" + "cardNote": "Às vezes, você também quer pagar como uma pessoa local. Digite os 6 a 8 primeiros dígitos do seu cartão, e a verificação dirá se o cartão parece ter sido emitido naquele país.", + "cardError": "São necessários 6 a 8 dígitos — digite pelo menos 6 para consultar o emissor.", + "cardReady": "Prefixo pronto — o país emissor será consultado quando a verificação for executada." }, "dependencies": { - "note": "", - "runButton": "", - "runButtonRunning": "", - "measured": "", - "allReady": "" + "note": "A verificação precisa dos resultados de todos estes testes base — execute os que estiverem faltando antes de começar:", + "runButton": "Executar os testes faltantes", + "runButtonRunning": "Executando os testes faltantes…", + "measured": "{n} de {total} medidos", + "allReady": "Todos os testes foram executados" }, "step": { - "country": "", - "language": "", - "location": "", - "card": "", - "awaitingCountry": "", - "otherTests": "" + "country": "País-alvo", + "language": "Idioma principal", + "location": "Localização", + "card": "Prefixo do cartão", + "awaitingCountry": "Escolha um país primeiro", + "otherTests": "Testes base" }, - "emptyHint": "", - "readyHint": "", + "emptyHint": "Escolha o país no qual você quer parecer local e execute a verificação.", + "readyHint": "Pronto. Execute a verificação para ver o quão local você parece.", "value": { - "yes": "", - "no": "" + "yes": "Sim", + "no": "Não" } }, "advancedtools": { - "OpenInNewTab": "", - "BackToHome": "", - "Title": "", - "Note": "", - "PingTestNote": "", - "MTRTestNote": "", - "DNSResolverNote": "", - "EnhancedDnsLeakTest": "", - "RuleTestNote": "", - "CensorshipCheck": "", - "ServiceStatus": "", - "Whois": "", - "InvisibilityTest": "", - "MacChecker": "", - "BrowserInfo": "", - "SecurityChecklist": "", - "PersonaCheck": "" + "OpenInNewTab": "Abrir em nova aba", + "BackToHome": "Voltar para o início", + "Title": "Ferramentas avançadas", + "Note": "Ferramentas usadas com menos frequência, mas muito úteis para testes de rede.", + "PingTestNote": "Teste de ping global", + "MTRTestNote": "Teste global de rota MTR", + "DNSResolverNote": "Resolução DNS multicanal em tempo real", + "EnhancedDnsLeakTest": "Captura todos os resolvedores recursivos, com análise de ECS e DNSSEC", + "RuleTestNote": "Verifique as regras do software de proxy", + "CensorshipCheck": "Verifique em quais países um site está bloqueado", + "ServiceStatus": "Verifique a disponibilidade ao vivo de serviços conhecidos de IA e desenvolvimento, e se um incidente recente explica a falha que você está vendo", + "Whois": "Pesquise informações de registro de domínio/IP", + "InvisibilityTest": "Verifique se você está usando proxy ou VPN", + "MacChecker": "Consultar informações de um endereço físico", + "BrowserInfo": "Verificar informações e impressão digital do navegador", + "SecurityChecklist": "Guia para proteger sua vida digital", + "PersonaCheck": "Compare o que os sites veem com o país no qual você quer parecer local" }, "macchecker": { - "Title": "", - "Note": "", - "Note2": "", - "Placeholder": "", - "invalidMAC": "", - "fetchError": "", - "company": "", - "macPrefix": "", - "address": "", - "country": "", - "blockStart": "", - "blockEnd": "", - "blockSize": "", - "blockType": "", - "blockRange": "", - "isRand": "", - "isPrivate": "", - "isMulticast": "", - "isUnicast": "", - "isGlobal": "", - "isLocal": "", - "property": "", - "value": "", - "manufacturer": "" + "Title": "Consulta de MAC", + "Note": "Consulte o fabricante de um endereço físico (endereço MAC), se ele é unicast/multicast, global/local etc. Aceita consulta com endereço completo de 12 dígitos ou apenas o prefixo. Você também pode usar esta ferramenta para verificar se o endereço físico gerado atende às suas necessidades.", + "Note2": "Digite um endereço físico para iniciar a consulta:", + "Placeholder": "F0:2F:4B:01:0A:AA", + "invalidMAC": "Endereço físico inválido", + "fetchError": "Não foi possível obter os resultados da consulta", + "company": "Fabricante", + "macPrefix": "Prefixo do endereço físico", + "address": "Endereço registrado", + "country": "País/região registrado", + "blockStart": "Início do bloco", + "blockEnd": "Fim do bloco", + "blockSize": "Tamanho do bloco", + "blockType": "Tipo de bloco", + "blockRange": "Intervalo do bloco", + "isRand": "Endereço aleatório", + "isPrivate": "Dados privados", + "isMulticast": "Endereço multicast", + "isUnicast": "Endereço unicast", + "isGlobal": "Endereço global", + "isLocal": "Endereço local", + "property": "Propriedade do endereço", + "value": "Valor", + "manufacturer": "Detalhes do fabricante" }, "whois": { - "Title": "", - "Note": "", - "Note2": "", - "Note3": "", - "Placeholder": "", - "invalidURL": "", - "reservedIP": "", - "noData": "", - "fetchError": "", - "Provider": "" + "Title": "Pesquisa Whois", + "Note": "A pesquisa Whois é um serviço usado para recuperar informações de registro de domínios. Ao inserir um nome de domínio ou IP, você pode recuperar informações como detalhes de registro do domínio, registrador, data de registro, data de expiração e mais. As informações Whois para IPs e nomes de domínio podem ter campos diferentes. Além disso, alguns domínios de topo (TLDs) menos comuns podem não ter informações disponíveis para consultas Whois.", + "Note2": "Digite um nome de domínio ou IP para iniciar a consulta:", + "Note3": "As informações Whois contêm vários tipos de campos. A seguir está o texto bruto", + "Placeholder": "Nome de domínio ou IP", + "invalidURL": "Nome de domínio ou IP inválido", + "reservedIP": "IPs privados ou reservados não têm registro público, então não há nada para consultar.", + "noData": "Nenhum dado Whois ou RDAP foi publicado para esta consulta. Alguns domínios de topo não oferecem serviço público de consulta.", + "fetchError": "Não foi possível obter os resultados da consulta, ou os resultados estão vazios", + "Provider": "Provedor das informações" }, "enhanceddnsleaktest": { - "Title": "", - "Note": "", - "Note2": "", - "Note3": "", - "Run": "", - "RunAgain": "", - "RunningProbes": "", - "PollingResult": "", - "FetchError": "", - "Empty": "", - "TableTitle": "", - "DedupeToggle": "", + "Title": "Teste aprofundado de vazamento de DNS", + "Note": "O teste de vazamento de DNS da página inicial já deu uma ideia geral de se você está vazando e por onde seu tráfego DNS sai. Este teste mais profundo adiciona mais dimensões: captura mais IPs de saída dos seus resolvedores, verifica se algum deles encaminha upstream um prefixo do seu IP real via ECS e verifica se eles oferecem suporte a DNSSEC — assim você obtém um quadro mais completo da postura de segurança da sua cadeia DNS.", + "Note2": "Clique em Executar para começar. O teste geralmente termina em poucos segundos. Disparamos várias sondas DNS em segundo plano e deixamos nosso servidor DNS autoritativo capturar os IPs de saída dos resolvedores recursivos que você realmente usa — uma execução típica captura 20 a 40 registros. Se nenhum resolvedor aparecer, o cache DNS do navegador provavelmente absorveu as solicitações; aguarde um momento e tente novamente.", + "Note3": "Como este é um teste aprofundado, você encontrará alguns conceitos especializados nesta página (ECS, DNSSEC etc.). Sem problema — após executar o teste, consulte a \"Referência dos campos\" no fim da página para uma explicação curta de cada coluna.", + "Run": "Executar teste", + "RunAgain": "Executar novamente", + "RunningProbes": "Enviando sondas DNS...", + "PollingResult": "Coletando resultados...", + "FetchError": "Falha ao obter os resultados do teste de vazamento de DNS. Tente novamente mais tarde.", + "Empty": "Nenhum resolvedor DNS foi capturado nesta sessão. Isso geralmente significa que o cache DNS do navegador absorveu a solicitação — tente novamente em instantes.", + "TableTitle": "Consultas DNS capturadas", + "DedupeToggle": "Ocultar duplicatas", "Flow": { - "Probing": "", - "Waiting": "", - "Fetching": "", - "Done": "" + "Probing": "Enviando sonda {current} / {total}...", + "Waiting": "Aguardando o DNS recursivo chegar ao nosso servidor de nomes (geralmente 1–3 s).", + "Fetching": "Buscando consultas DNS capturadas...", + "Done": "Teste concluído." }, "DnssecChip": { - "Signed": "", - "Unsigned": "", - "Validated": "", - "CdSet": "" + "Signed": "Assinaturas DNSSEC solicitadas", + "Unsigned": "Assinaturas DNSSEC não solicitadas", + "Validated": "Validação DNSSEC normal", + "CdSet": "Validação DNSSEC desativada" }, "Summary": { - "ResolverCount": "", - "RawCount": "", - "DnssecPosture": "", - "DnssecGood": "", - "DnssecNoDo": "", - "DnssecCdSet": "", - "DnssecNone": "" + "ResolverCount": "Resolvedores únicos", + "RawCount": "Capturas brutas", + "DnssecPosture": "Postura DNSSEC", + "DnssecGood": "Compatível com DNSSEC — todos os resolvedores solicitaram assinaturas e nenhum pulou a validação.", + "DnssecNoDo": "Fraca — pelo menos um resolvedor não solicitou assinaturas DNSSEC.", + "DnssecCdSet": "Suspeita — pelo menos um resolvedor pediu para pular a validação DNSSEC.", + "DnssecNone": "Nenhum sinal ainda." }, "Fields": { - "ResolverIP": "", - "Location": "", - "ASN": "", - "QueryType": "", - "Transport": "", - "ECS": "" + "ResolverIP": "IP do resolvedor", + "Location": "Localização", + "ASN": "Org / ASN", + "QueryType": "Tipo de registro", + "Transport": "Protocolo de transporte", + "ECS": "ECS" }, "Legend": { - "Title": "", - "ResolverIP": "", - "QueryType": "", - "Transport": "", - "ECS": "", - "DO": "", - "CD": "" + "Title": "Referência dos campos", + "ResolverIP": "O IP de saída do resolvedor DNS recursivo que realmente chegou ao nosso servidor de nomes autoritativo. Se você usa VPN, compare o ASN dele com o ASN da saída da sua VPN — uma divergência significa que suas consultas DNS estão contornando o túnel da VPN (ou seja, vazando).", + "QueryType": "O tipo de registro que o resolvedor solicitou em nome do seu navegador (A, AAAA, HTTPS etc.). Navegadores geralmente disparam consultas A e AAAA juntas.", + "Transport": "DNS usa UDP por padrão. TCP geralmente significa que a resposta era grande — e respostas tendem a ser grandes quando carregam registros DNSSEC.", + "ECS": "EDNS Client Subnet — alguns resolvedores recursivos anexam um prefixo do seu IP real (normalmente IPv4 /24 ou IPv6 /48) ao encaminhar consultas para servidores autoritativos upstream. Isso significa que um segmento do seu IP fica conhecido por servidores ao longo da cadeia DNS, o que pode ser usado para georroteamento de CDN, mas também reduz seu anonimato.", + "DO": "DNSSEC OK — uma marca indica que o resolvedor recursivo solicitou ativamente assinaturas DNSSEC nesta consulta, então a cadeia carrega verificação criptográfica; um x indica que a consulta saiu sem assinaturas, então uma resposta adulterada em trânsito poderia passar despercebida.", + "CD": "Checking Disabled — uma marca indica que o chamador não pediu para pular a validação DNSSEC, então o resolvedor recursivo executa a verificação normal de assinatura; um x indica que o chamador pediu explicitamente para pular a validação e aceitará respostas mesmo quando as assinaturas não corresponderem. Normalmente isso deve ser uma marca; se você vir um x, geralmente é por causa de uma ferramenta de depuração ou comportamento de rebaixamento de segurança de um cliente e, em casos raros, pode sinalizar adulteração man-in-the-middle no caminho." } }, "dnsresolver": { - "Title": "", - "Note": "", - "Note2": "", - "Placeholder": "", - "invalidURL": "", - "fetchError": "", - "Provider": "", - "Result": "", - "Record": "", - "AllRegions": "" + "Title": "Resolução DNS", + "Note": "Em algumas regiões, por motivos políticos ou comerciais, algumas operadoras podem contaminar determinados nomes de domínio, resultando em respostas incorretas quando acessados diretamente. Usar verificações de resolução DNS ajuda a inspecionar os resultados de nomes de domínio em provedores DNS conhecidos no mundo todo. Entre os testes DNS integrados, alguns provedores são da China, cujos resultados de resolução DNS podem estar contaminados. Analise com critério.", + "Note2": "Digite uma URL ou nome de domínio para iniciar a resolução:", + "Placeholder": "URL ou nome de domínio", + "invalidURL": "URL ou nome de domínio inválido", + "fetchError": "Não foi possível obter os resultados da resolução", + "Provider": "Provedor DNS", + "Result": "Resultado da resolução", + "Record": "Registro", + "AllRegions": "Todas as regiões" }, "ruletest": { - "Title": "", - "Name": "", - "Note": "", - "StatusWait": "", - "StatusError": "", - "Country": "", - "RefreshAll": "" + "Title": "Teste de regras", + "Name": "Teste", + "Note": "O Teste de regras é usado para determinar se as regras baseadas em domínio do software de proxy estão configuradas corretamente. Antes de executar o teste, consulte o cartão abaixo e configure regras de roteamento baseadas em domínio diferentes para as 8 URLs diferentes do IPCheck.ing no seu software de proxy. Normalmente, as regras podem usar um formato como: DOMAIN, ptest-3.ipcheck.ing, POLICY. O formato específico pode variar conforme o software de proxy usado.", + "StatusWait": "Aguardando teste ou erro no teste", + "StatusError": "Erro no teste", + "Country": "Região do proxy", + "RefreshAll": "Atualizar tudo" }, "Tooltips": { - "RefreshIPCard": "", - "SaveAsImage": "", - "RefreshConnectivityTests": "", - "RefreshWebRTC": "", - "RefreshDNSLeakTest": "", - "SpeedTestButton": "", - "ShowASNInfo": "", - "ShowASNHistory": "", - "ShowASNConnectivity": "", - "CopyIP": "", - "CopySdpLog": "", - "ViewOnMap": "", - "InfoMask": "", - "IPHistory": "", - "ShareReport": "", - "QueryIP": "", - "GithubLink": "" + "RefreshIPCard": "Atualizar cartão de IP", + "SaveAsImage": "Salvar como imagem", + "RefreshConnectivityTests": "Atualizar testes de conectividade", + "RefreshWebRTC": "Atualizar teste de vazamento WebRTC", + "RefreshDNSLeakTest": "Atualizar teste de vazamento de DNS", + "SpeedTestButton": "Iniciar/pausar teste de velocidade", + "ShowASNInfo": "Mostrar detalhes do AS", + "ShowASNHistory": "Mostrar histórico de ASN", + "ShowASNConnectivity": "Mostrar conectividade do ASN", + "CopyIP": "Copiar IP", + "CopySdpLog": "Copiar log SDP", + "ViewOnMap": "Ver localização no mapa", + "InfoMask": "Ocultar informações de IP", + "IPHistory": "Ver histórico de IP", + "ShareReport": "Criar relatório diagnóstico", + "QueryIP": "Consultar IP", + "GithubLink": "Ver código-fonte no GitHub" }, "report": { - "Title": "", - "Note": "", - "SectionsLabel": "", - "NotTested": "", - "MaskTail": "", - "TtlLabel": "", - "Ttl1": "", - "Ttl3": "", - "Ttl7": "", - "Preview": "", - "CreateLink": "", - "CopyAI": "", - "DownloadJson": "", - "CopyLink": "", - "NoSelection": "", - "CreateFailed": "", - "LinkOnce": "", - "ExpiresAt": "", - "CopiedTitle": "", - "CopiedMessage": "", + "Title": "Relatório diagnóstico", + "Note": "Agrupe seus resultados de testes concluídos em um relatório e compartilhe com uma pessoa amiga ou com um assistente de IA para diagnóstico de rede. Nada sai deste dispositivo até você copiar, baixar ou criar um link.", + "SectionsLabel": "Testes a incluir", + "NotTested": "não testado", + "MaskTail": "Mascarar finais dos IPs", + "TtlLabel": "Excluir automaticamente após", + "Ttl1": "1 dia", + "Ttl3": "3 dias", + "Ttl7": "7 dias", + "Preview": "Prévia do conteúdo compartilhado", + "CreateLink": "Criar link compartilhável", + "CopyAI": "Copiar para perguntar à IA", + "DownloadJson": "Baixar JSON", + "CopyLink": "Copiar link", + "NoSelection": "Selecione pelo menos um teste primeiro.", + "CreateFailed": "Falha ao criar o link compartilhável. Tente novamente mais tarde.", + "LinkOnce": "Este link é mostrado apenas uma vez — copie-o agora. Qualquer pessoa com o link poderá ver o relatório até ele expirar.", + "ExpiresAt": "O relatório será excluído automaticamente em {time}.", + "CopiedTitle": "Copiado", + "CopiedMessage": "Relatório copiado — cole-o em qualquer assistente de IA.", "ai": { - "Heading": "", - "Intro": "", - "Masked": "", - "Instruction": "", + "Heading": "Relatório diagnóstico de rede MyIP", + "Intro": "Este relatório foi gerado pelo MyIP ({origin}) em {time}, a partir de testes de rede executados no navegador do usuário. Os nomes dos campos são identificadores estáveis de esquema; todos os valores de latência estão em milissegundos.", + "Masked": "Observação: os IPs estão parcialmente mascarados por privacidade (IPv4: último octeto; IPv6: metade de interface; exibidos como \"x\").", + "Instruction": "Com base nos dados acima, analise a saúde desta rede. Aponte anomalias (vazamentos de DNS, exposição WebRTC, traços de proxy/VPN, perda de pacotes, alta latência, problemas de roteamento) e dê sugestões concretas de solução.", "Sections": { - "ipinfo": "", - "connectivity": "", - "webrtc": "", - "dnsleak": "", - "speedtest": "", - "pingtest": "", - "mtrtest": "", - "ruletest": "", - "browserinfo": "", - "invisibility": "", - "enhanceddnsleak": "", - "persona": "" + "ipinfo": "IPs públicos vistos por várias fontes independentes (as fontes podem divergir quando há proxy ou roteamento dividido).", + "connectivity": "Sondas de acessibilidade do navegador contra sites conhecidos; timeMs é o tempo de ida e volta exibido, minTimeMs é o melhor de rodadas repetidas.", + "webrtc": "IPs expostos por WebRTC STUN — se forem diferentes do IP público principal, o IP real pode estar vazando para além de um proxy/VPN.", + "dnsleak": "IPs de saída dos resolvedores DNS que realmente responderam às consultas do navegador — resolvedores que não pertencem ao ISP/provedor de VPN do usuário indicam vazamento de DNS.", + "speedtest": "Largura de banda, latência e latência sob carga (bufferbloat) medidas contra a Cloudflare; as pontuações avaliam experiência de streaming/jogos/RTC, quanto maior, melhor.", + "pingtest": "Estatísticas de ping ICMP até o IP do usuário a partir de sondas Globalping ao redor do mundo.", + "mtrtest": "Traceroutes MTR por salto até o IP do usuário a partir de sondas pelo mundo; perda em saltos intermediários pode ser apenas limitação de ICMP, mas perda sustentada a partir de um salto indica problema real.", + "ruletest": "IPs de saída observados ao buscar 8 endpoints de teste — mais de um IP único significa que o tráfego está dividido entre rotas diferentes (ex.: regras de proxy).", + "browserinfo": "Ambiente de navegador, sistema operacional e tela do dispositivo testado.", + "invisibility": "Pontuações de detecção de proxy/VPN (0–100, maior = mais provável de ser detectado) com os sinais individuais de detecção que foram sinalizados.", + "enhanceddnsleak": "Captura DNS em nível de resolvedor: cada IP de resolvedor que consultou o domínio de teste, com transporte, exposição ECS e flags DNSSEC (do/cd).", + "persona": "O quão próximo o navegador está de uma pessoa local do país escolhido: uma nota geral mais o veredito de cada verificação. Apenas vereditos — os valores subjacentes deliberadamente não são armazenados em relatórios compartilhados." } } }, "reportPage": { - "Loading": "", - "NotFound": "", - "NotFoundNote": "", - "LoadFailed": "", - "Unsupported": "", - "GeneratedBy": "", - "Stale": "", - "TestMine": "", + "Loading": "Carregando relatório…", + "NotFound": "Relatório não encontrado ou expirado", + "NotFoundNote": "Relatórios compartilhados são excluídos automaticamente quando expiram.", + "LoadFailed": "Falha ao carregar o relatório. Tente novamente mais tarde.", + "Unsupported": "Este relatório foi criado com uma versão mais recente e não pode ser exibido.", + "GeneratedBy": "Relatório diagnóstico gerado em {origin} em {time}", + "Stale": "Este relatório tem mais de 3 dias — a rede pode ter mudado desde então.", + "TestMine": "Testar minha própria rede", "Col": { - "Source": "", - "Name": "", - "IP": "", - "Location": "", - "ISP": "", - "Status": "", - "Server": "", - "Provider": "", - "Target": "", - "UniqueIPs": "", - "Resolvers": "" + "Source": "Fonte", + "Name": "Nome", + "IP": "IP", + "Location": "Localização", + "ISP": "ISP", + "Status": "Status", + "Server": "Servidor", + "Provider": "Provedor", + "Target": "Alvo", + "UniqueIPs": "IPs de saída únicos", + "Resolvers": "Resolvedores" }, "Dnssec": { - "ok": "", - "partial": "", - "none": "" + "ok": "DNSSEC ativado", + "partial": "DNSSEC parcialmente ativado", + "none": "DNSSEC não ativado" } }, "ipHistory": { - "Title": "", - "Notes": "", - "Empty": "", - "FilterByType": "", - "FilterByCountry": "", - "UnknownCountry": "", - "NoMatch": "", - "ClearAll": "", - "ClearConfirm": "" + "Title": "Histórico de IP", + "Notes": "IPs detectados enquanto você usa este site são registrados aqui. Os dados ficam armazenados somente neste navegador — eles nunca são sincronizados com sua conta nem saem do seu dispositivo. Você pode ajustar o tempo de retenção dos registros em \"Preferências\".", + "Empty": "Nenhum registro ainda. Os IPs aparecerão aqui quando os testes da página inicial os detectarem.", + "FilterByType": "Filtrar por tipo de IP", + "FilterByCountry": "Filtrar por país / região", + "UnknownCountry": "Desconhecido", + "NoMatch": "Nenhum registro corresponde aos filtros selecionados.", + "ClearAll": "Limpar histórico", + "ClearConfirm": "Clique novamente para confirmar" }, "ipInfos": { - "Title": "", - "Notes": "", - "Source": "", - "Country": "", - "Region": "", - "City": "", - "TimeZone": "", - "ISP": "", - "ASN": "", - "Coordinates": "", - "IPv4Error": "", - "IPv6Error": "", - "type": "", - "isProxy": "", - "qualityScore": "", - "qualityScoreUnknown": "", - "advancedUnlockCta": "", - "advancedQuotaCta": "", + "Title": "Informações de IP", + "Notes": "O programa verificará primeiro seu IP em diferentes fontes (incluindo IPv4 e IPv6) e depois consultará os dados geográficos correspondentes na fonte de geolocalização de IP selecionada. Se houver apenas 1 pilha de IP, a fonte sem dados será exibida como vazia.", + "Source": "Fonte de IP", + "Country": "Região", + "Region": "Estado", + "City": "Cidade", + "TimeZone": "Fuso horário", + "ISP": "ISP", + "ASN": "ASN", + "Coordinates": "Coordenadas", + "IPv4Error": "Busca falhou ou não há IPv4", + "IPv6Error": "Busca falhou ou não há IPv6", + "type": "Tipo", + "isProxy": "Proxy", + "qualityScore": "Qualidade do IP", + "qualityScoreUnknown": "Pontuação desconhecida", + "advancedUnlockCta": "Para evitar abuso, faça login para ver as informações a seguir", + "advancedQuotaCta": "Cota mensal para novos IPs esgotada", "ASNInfo": { - "trafficPercentage": "", - "note": "", - "asnName": "", - "asnOrgName": "", - "asnCountryCode": "", - "estimatedUsers": "", - "IPv4_Pct": "", - "IPv6_Pct": "", - "HTTP_Pct": "", - "HTTPS_Pct": "", - "Desktop_Pct": "", - "Mobile_Pct": "", - "Bot_Pct": "", - "Human_Pct": "", - "moreData": "" + "trafficPercentage": "Porcentagem de tráfego: ", + "note": "Dados deste AS: ", + "asnName": "Nome do AS: ", + "asnOrgName": "Organização do AS: ", + "asnCountryCode": "País do AS: ", + "estimatedUsers": "Usuários estimados: ", + "IPv4_Pct": "IPv4", + "IPv6_Pct": "IPv6", + "HTTP_Pct": "HTTP", + "HTTPS_Pct": "HTTPS", + "Desktop_Pct": "Desktop", + "Mobile_Pct": "Mobile", + "Bot_Pct": "Bot", + "Human_Pct": "Humano", + "moreData": "Mais dados: " }, "ASNHistory": { - "announcedPrefixes": "", - "note": "", - "empty": "", - "error": "", - "seenBy": "" + "announcedPrefixes": "Prefixos anunciados: ", + "note": "Anúncios históricos de ASN para este IP: ", + "empty": "Nenhum dado histórico de roteamento encontrado para este IP.", + "error": "Falha ao carregar o histórico de ASN.", + "seenBy": "Visibilidade RIPE RIS: {pct}% — pico de {peers} peers full-feed" }, "ASNConnectivity": { - "note": "", - "empty": "", - "error": "", - "expand": "", - "dialogTitle": "", - "legendOrigin": "", - "legendTier1": "", - "legendIntermediate": "", - "zoomIn": "", - "zoomOut": "", - "zoomReset": "" + "note": "Caminhos upstream deste AS até os provedores globais de trânsito Tier 1: ", + "empty": "Nenhum dado de conectividade upstream disponível para este AS.", + "error": "Falha ao carregar a conectividade do ASN.", + "expand": "Expandir", + "dialogTitle": "Conectividade de rede para AS{asn}", + "legendOrigin": "Origem", + "legendTier1": "ISP Tier 1", + "legendIntermediate": "AS intermediário", + "zoomIn": "Aproximar", + "zoomOut": "Afastar", + "zoomReset": "Redefinir zoom" }, "advancedData": { - "proxyYes": "", - "proxyMaybe": "", - "proxyNo": "", - "proxyUnknown": "", + "proxyYes": "Sinalizado como proxy / VPN", + "proxyMaybe": "Possível proxy / VPN", + "proxyNo": "Não sinalizado", + "proxyUnknown": "Tipo desconhecido", "type": { - "Residential": "", - "Wireless": "", - "Business": "", - "Hosting": "", - "unknownType": "" + "Residential": "Residencial", + "Wireless": "Sem fio", + "Business": "Empresarial", + "Hosting": "Hospedagem", + "unknownType": "Tipo desconhecido" }, - "Nativeness": "", - "NativeIPYes": "", - "NativeIPNo": "", - "nativenessTooltip": "" + "Nativeness": "Alinhamento ASN-Geo", + "NativeIPYes": "Alinhado", + "NativeIPNo": "Divergente", + "nativenessTooltip": "Se o país registrado do ASN corresponde ao país de geolocalização do IP" } }, "serviceStatus": { - "Title": "", - "Note": "", - "Refresh": "", - "LastChecked": "", - "AllOperational": "", - "ComponentsTitle": "", - "IncidentsTitle": "", - "NoComponents": "", - "NoRecentIncidents": "", - "Loading": "", - "Checking": "", - "IncidentsError": "", - "ComponentsError": "", + "Title": "Status dos serviços", + "Note": "Quando um serviço não abre, talvez o problema não seja seu — o próprio serviço pode estar fora do ar. Atualmente, muitas pessoas usam produtos de IA, e quando o produto de IA não responde, elas não sabem se é problema delas ou do serviço. Esta seção mostra a disponibilidade ao vivo de serviços conhecidos de IA; expanda um cartão para ver o status de cada subserviço e incidentes recentes.", + "Refresh": "Atualizar status dos serviços", + "LastChecked": "Última verificação", + "AllOperational": "Todos os sistemas operacionais", + "ComponentsTitle": "Serviços", + "IncidentsTitle": "Incidentes recentes", + "NoComponents": "Nenhum detalhe de serviço disponível.", + "NoRecentIncidents": "Nenhum incidente recente.", + "Loading": "Carregando…", + "Checking": "Verificando…", + "IncidentsError": "Não foi possível carregar incidentes.", + "ComponentsError": "Não foi possível carregar serviços.", "indicator": { - "minor": "", - "major": "", - "critical": "", - "maintenance": "", - "unknown": "" + "minor": "Problemas menores", + "major": "Grande indisponibilidade", + "critical": "Indisponibilidade crítica", + "maintenance": "Em manutenção", + "unknown": "Status indisponível" }, "component": { - "operational": "", - "degraded_performance": "", - "partial_outage": "", - "major_outage": "", - "under_maintenance": "" + "operational": "Operacional", + "degraded_performance": "Degradado", + "partial_outage": "Indisponibilidade parcial", + "major_outage": "Grande indisponibilidade", + "under_maintenance": "Manutenção" }, "incidentStatus": { - "investigating": "", - "identified": "", - "monitoring": "", - "resolved": "", - "postmortem": "", - "scheduled": "", - "in_progress": "", - "verifying": "", - "completed": "" + "investigating": "Investigando", + "identified": "Identificado", + "monitoring": "Monitorando", + "resolved": "Resolvido", + "postmortem": "Postmortem", + "scheduled": "Agendado", + "in_progress": "Em andamento", + "verifying": "Verificando", + "completed": "Concluído" } }, "connectivity": { - "Title": "", - "Note": "", - "StatusWait": "", - "StatusAvailable": "", - "StatusUnavailable": "", - "StatusTimeout": "", - "checking": "", - "minTestTime": "", - "RefreshThisTest": "", - "RoundCount": "", + "Title": "Conectividade de rede", + "Note": "Este teste verifica se você consegue acessar um site informado. Ele funciona carregando uma pequena imagem daquele site. Os valores de atraso são apenas referência e serão menores na prática. Você pode adicionar outros sites que usa com frequência à lista de testes para verificações rápidas.", + "StatusWait": "Aguardando teste", + "StatusAvailable": "OK", + "StatusUnavailable": "Indisponível", + "StatusTimeout": "Tempo esgotado ou indisponível", + "checking": "Verificando...", + "minTestTime": "Tempo mínimo do teste: ", + "RefreshThisTest": "Atualizar este teste", + "RoundCount": "Rodada {n}: ", "addCustom": { - "AddCard": "", - "Title": "", - "Hint": "", - "NameLabel": "", - "NamePlaceholder": "", - "UrlLabel": "", - "UrlPlaceholder": "", - "Add": "", - "Remove": "", - "NameRequired": "", - "UrlRequired": "", - "InvalidUrl": "", - "LimitReached": "", - "LastTargetTitle": "", - "LastTargetMessage": "" + "AddCard": "Adicionar teste", + "Title": "Adicionar um teste personalizado", + "Hint": "Testamos carregando o favicon do site via HTTPS. Alguns sites podem não ser acessíveis dessa forma. Testes personalizados são salvos localmente no seu navegador.", + "NameLabel": "Nome", + "NamePlaceholder": "ex.: Wikipédia", + "UrlLabel": "URL", + "UrlPlaceholder": "ex.: wikipedia.org", + "Add": "Adicionar", + "Remove": "Remover teste", + "NameRequired": "Informe um nome.", + "UrlRequired": "Informe uma URL.", + "InvalidUrl": "Isso não parece uma URL válida.", + "LimitReached": "Você atingiu o limite de 60 testes.", + "LastTargetTitle": "Não é possível remover", + "LastTargetMessage": "Pelo menos um site de teste deve permanecer." }, "importDialog": { - "Title": "", - "CustomOption": "", - "ImportOption": "", - "SiteCount": "", - "Import": "", - "Capacity": "", - "NotEnoughCapacity": "" + "Title": "Adicionar / importar testes", + "CustomOption": "Adicionar personalizado", + "ImportOption": "Importar uma lista", + "SiteCount": "{n} sites", + "Import": "Importar", + "Capacity": "{used}/{limit} usados", + "NotEnoughCapacity": "Capacidade insuficiente — esta lista precisa de {need} vagas, mas só restam {free}." }, "importLists": { - "defaults": "", - "iran": "", - "russia": "", - "china": "", - "brazil": "", - "ai": "", - "social": "", - "streaming": "", - "gaming": "", - "developer": "", - "cloud": "", - "crypto": "", - "ecommerce": "", - "news": "" + "defaults": "Padrões do sistema", + "iran": "Essenciais do Irã", + "russia": "Essenciais da Rússia", + "china": "Essenciais da China", + "brazil": "Essenciais do Brasil", + "ai": "Serviços de IA", + "social": "Redes sociais e mensagens", + "streaming": "Streaming", + "gaming": "Jogos", + "developer": "Desenvolvimento", + "cloud": "Nuvem e CDN", + "crypto": "Corretoras cripto", + "ecommerce": "E-commerce global", + "news": "Notícias e wiki" } }, "webrtc": { "id": "webrtc", - "Title": "", - "Name": "", - "Note": "", - "StatusWait": "", - "StatusError": "", - "StatusUnavailable": "", - "SdpLog": "", + "Title": "Teste de vazamento WebRTC", + "Name": "Conexão WebRTC", + "Note": "WebRTC costuma estabelecer conexões diretamente via UDP. Se o teste retornar seu IP real, isso significa que suas configurações de proxy não cobrem essas conexões. Além de detectar o IP usado ao se conectar via WebRTC, também detectamos seu tipo de NAT. No entanto, a detecção do tipo de NAT não é 100% precisa e serve apenas como referência.", + "StatusWait": "Aguardando teste", + "StatusError": "Erro de conexão", + "StatusUnavailable": "WebRTC indisponível", + "SdpLog": "Log SDP", "NATType": { - "srflx": "", - "prflx": "", - "relay": "", - "host": "", - "unknown": "" + "srflx": "Cone restrito por porta ou simétrico", + "prflx": "Cone restrito por porta", + "relay": "Simétrico", + "host": "Cone completo", + "unknown": "Tipo desconhecido" } }, "dnsleaktest": { - "Name": "", - "Title": "", - "Note": "", - "Note2": "", - "EndpointCountry": "", - "StatusWait": "", - "StatusError": "" + "Name": "Teste de endpoint DNS", + "Title": "Teste de vazamento de DNS", + "Note": "Um vazamento de DNS significa que, quando você está conectado a uma VPN/proxy, suas resoluções de nomes de domínio ainda são feitas pelo seu ISP local, criando risco de vazamento de DNS.", + "Note2": "O método para testar vazamentos de DNS envolve acessar um nome de domínio recém-gerado para detectar por qual saída DNS regional você está resolvendo. Se a região do endpoint retornado corresponder à região do seu provedor local, há risco de vazamento de DNS. Nesse caso, talvez seja necessário ajustar suas configurações de VPN/proxy.", + "EndpointCountry": "Região do endpoint", + "StatusWait": "Aguardando teste", + "StatusError": "Erro no teste" }, "speedtest": { - "Title": "", - "Note": "", - "Download": "", - "Upload": "", - "Latency": "", - "Jitter": "", - "loadedLatency": "", - "videoStreaming": "", - "gaming": "", - "rtc": "", - "score": "", - "connectionFrom": "", - "connectionTo": "", - "connectionEnd": "", - "testFailed": "", + "Title": "Teste de velocidade", + "Note": "O teste de velocidade será realizado usando a rede de borda da Cloudflare. Você pode escolher o tamanho dos pacotes para download e upload conforme suas necessidades. Durante o teste, os pacotes serão enviados e baixados 4 vezes — tenha atenção ao consumo de dados. Como a Cloudflare não possui nós de borda em todos os países do mundo, os resultados servem apenas como referência; as velocidades reais podem ser maiores.", + "Download": "Download", + "Upload": "Upload", + "Latency": "Latência", + "Jitter": "Jitter", + "loadedLatency": "Latência sob carga: ", + "videoStreaming": "Streaming de vídeo", + "gaming": "Jogos", + "rtc": "Videochamadas", + "score": "Com base no teste de velocidade, foram obtidas as seguintes pontuações de qualidade:", + "connectionFrom": "Teste de velocidade concluído a partir da sua máquina, IP: ", + "connectionTo": " para o servidor de teste:", + "connectionEnd": " .", + "testFailed": "Não foi possível acessar o servidor de teste de velocidade. Verifique sua conexão e tente novamente.", "quality": { - "bad": "", - "poor": "", - "average": "", - "good": "", - "great": "" + "bad": "Ruim", + "poor": "Fraca", + "average": "Média", + "good": "Boa", + "great": "Excelente" } }, "globalping": { - "Selected": "", - "Suggested": "", - "NoProbe": "", - "GroupFull": "", - "UnreachableTarget": "", - "ResultsHint": "", - "Asia": "", - "Europe": "", - "Africa": "", - "Americas": "", - "Oceania": "" + "Selected": "Regiões selecionadas: ", + "Suggested": "Regiões sugeridas", + "NoProbe": "Nenhuma sonda disponível neste país/região", + "GroupFull": "Até {max} países/regiões — desmarque alguns primeiro", + "UnreachableTarget": "As sondas rodam na internet pública, portanto IPs privados e reservados não podem ser acessados de nenhum país.", + "ResultsHint": "Os resultados aparecerão aqui depois que você selecionar sondas e executar o teste.", + "Asia": "Ásia", + "Europe": "Europa", + "Africa": "África", + "Americas": "Américas", + "Oceania": "Oceania" }, "pingtest": { - "EnterIPLabel": "", - "EnterIPPlaceholder": "", - "UseStored": "", + "EnterIPLabel": "Digite um IP para testar", + "EnterIPPlaceholder": "ex.: 1.1.1.1", + "UseStored": "Usar um IP salvo", "id": "pingtest", - "Title": "", - "Note": "", - "Note2": "", - "Note3": "", - "SelectIP": "", - "Region": "", - "MinDelay": "", - "MaxDelay": "", - "AvgDelay": "", - "TotalPackets": "", - "PacketLoss": "", - "ReceivedPackets": "", - "DroppedPackets": "", - "Error": "" + "Title": "Teste de latência global", + "Note": "O teste de ping é uma ferramenta de rede usada para medir o tempo de ida e volta de pacotes de dados entre dois endpoints (servidores). É uma métrica essencial para avaliar o desempenho da rede.", + "Note2": "No teste abaixo, o IPCheck.ing usa sondas mundiais do Globalping para enviar pequenos pacotes de dados de servidores ao redor do mundo para seu IP, medindo o tempo que esses pacotes levam para ir do servidor ao seu dispositivo e voltar. Por meio desses testes de ping de diferentes localizações geográficas, você obtém uma visão abrangente da velocidade e estabilidade da conexão do seu dispositivo com a rede global.", + "Note3": "Selecione seu IP para o teste.", + "SelectIP": "Selecionar um IP", + "Region": "Região", + "MinDelay": "Atraso mín. (ms)", + "MaxDelay": "Atraso máx. (ms)", + "AvgDelay": "Atraso médio (ms)", + "TotalPackets": "Pacotes totais", + "PacketLoss": "Perda de pacotes", + "ReceivedPackets": "Recebidos", + "DroppedPackets": "Perdidos", + "Error": "O teste falhou; parece que seu IP não tem permissão para executar teste de ping." }, "mtrtest": { - "EnterIPLabel": "", - "EnterIPPlaceholder": "", - "UseStored": "", - "Title": "", - "Note": "", - "Note2": "", - "Note3": "", - "SelectIP": "", - "Region": "", - "Error": "", - "ColHost": "", - "ColIP": "", - "ColASN": "", - "ColLoss": "", - "ColSent": "", - "ColDrop": "", - "ColRecv": "", - "ColLast": "", - "ColAvg": "", - "ColBest": "", - "ColWorst": "", - "ColStDev": "", - "ColJitter": "", - "NoReply": "" + "EnterIPLabel": "Digite um IP para testar", + "EnterIPPlaceholder": "ex.: 1.1.1.1", + "UseStored": "Usar um IP salvo", + "Title": "Teste MTR", + "Note": "O teste MTR (My Traceroute) usa sondas mundiais do Globalping para realizar rastreamento detalhado de rota até seu IP a partir de servidores localizados em diferentes continentes e regiões do mundo. O MTR combina funcionalidades dos comandos tradicionais traceroute e ping para diagnosticar a qualidade e o desempenho de conexões de rede em tempo real.", + "Note2": "Esse processo cobre o caminho completo de várias localizações geográficas até seu ambiente de rede, fornecendo informações de latência e perda de pacotes para cada salto. Seja congestionamento no caminho de roteamento, problemas de latência em um nó específico ou perda de pacotes durante a transmissão, o teste MTR fornece informações diagnósticas importantes.", + "Note3": "Selecione seu IP para o teste.", + "SelectIP": "Selecionar um IP", + "Region": "Região", + "Error": "O teste falhou; parece que seu IP não tem permissão para executar teste MTR.", + "ColHost": "Host", + "ColIP": "IP", + "ColASN": "ASN", + "ColLoss": "Perda", + "ColSent": "Enviados", + "ColDrop": "Perdidos", + "ColRecv": "Recebidos", + "ColLast": "Último", + "ColAvg": "Médio", + "ColBest": "Melhor", + "ColWorst": "Pior", + "ColStDev": "Desvio padrão", + "ColJitter": "Jitter", + "NoReply": "Sem resposta" }, "ipcheck": { - "Title": "", - "Placeholder": "", - "Error": "", - "reservedIP": "", - "NoData": "" + "Title": "Verificação de IP", + "Placeholder": "ex.: 1.1.1.1", + "Error": "Informe um IPv4 ou IPv6 válido.", + "reservedIP": "IPs privados, loopback e outros reservados não têm geolocalização, então não há nada para consultar.", + "NoData": "Não foi possível obter informações para este IP. Ele pode não ser roteável (ex.: 0.0.0.0, IP privado) ou a fonte upstream pode não ter dados para ele." }, "alert": { - "refreshEverythingMessage": "", - "refreshEverythingTitle": "", - "OhNo": "", - "Congrats": "", - "OhNo_Message": "", - "Congrats_Message": "", - "maskedInfoTitle": "", - "maskedInfoMessage": "", - "unmaskedInfoTitle": "", - "unmaskedInfoMessage": "", - "SignInFailed": "", - "SignInFailedReason": "", - "IpGeoSourceFallbackTitle": "", - "IpGeoSourceFallbackMessage": "", - "SignInEmailTakenTitle": "", - "SignInEmailTakenMessage": "" + "refreshEverythingMessage": "Atualizando todos os dados, aguarde...", + "refreshEverythingTitle": "Atualizando", + "OhNo": "Ah, não!", + "Congrats": "Parabéns!", + "OhNo_Message": "O teste de conectividade falhou; alguns sites não estão acessíveis.", + "Congrats_Message": "Todos os testes de conectividade passaram; você consegue acessar todos os sites.", + "maskedInfoTitle": "IPs mascarados", + "maskedInfoMessage": "Capturas de tela não revelarão seus IPs.", + "unmaskedInfoTitle": "IPs não mascarados", + "unmaskedInfoMessage": "Tenha cuidado ao compartilhar capturas de tela.", + "SignInFailed": "Falha ao entrar", + "SignInFailedReason": "Motivo", + "IpGeoSourceFallbackTitle": "Fonte de geolocalização de IP alterada", + "IpGeoSourceFallbackMessage": "{from} está temporariamente indisponível — este resultado vem de {to} por enquanto.", + "SignInEmailTakenTitle": "Este e-mail já está em uso", + "SignInEmailTakenMessage": "Já existe uma conta para este e-mail. Entre com {other} em vez disso." }, "shortcutKeys": { - "GoToTop": "", - "GoToBottom": "", - "ToggleDarkMode": "", - "RefreshEverything": "", - "RefreshIPCard": "", - "RefreshConnectivityTests": "", - "RefreshWebRTC": "", - "RefreshDNSLeakTest": "", - "SpeedTestButton": "", - "PingTest": "", - "MTRTest": "", - "RuleTest": "", - "DNSResolver": "", - "IPCheck": "", - "ToggleInfoMask": "", - "IPHistory": "", - "ShareReport": "", - "Help": "", - "HelpNote": "", - "GoNext": "", - "GoPrevious": "", - "OpenHighlightedTool": "", - "RefreshRuleTests": "", - "CensorshipCheck": "", - "Pulse": "", - "Whois": "", - "ServiceStatus": "", - "InvisibilityTest": "", - "EnhancedDnsLeakTest": "", - "MacChecker": "", - "BrowserInfo": "", - "SecurityChecklist": "", - "PersonaCheck": "" + "GoToTop": "Ir para o topo", + "GoToBottom": "Ir para o fim", + "ToggleDarkMode": "Alternar modo escuro", + "RefreshEverything": "Atualizar tudo", + "RefreshIPCard": "Atualizar cartão de IP", + "RefreshConnectivityTests": "Atualizar testes de conectividade", + "RefreshWebRTC": "Atualizar teste de vazamento WebRTC", + "RefreshDNSLeakTest": "Atualizar teste de vazamento de DNS", + "SpeedTestButton": "Iniciar/pausar teste de velocidade", + "PingTest": "Abrir painel de teste de latência global", + "MTRTest": "Abrir painel de teste MTR", + "RuleTest": "Abrir painel de teste de regras", + "DNSResolver": "Abrir painel de resolução DNS", + "IPCheck": "Consultar IP", + "ToggleInfoMask": "Alternar mascaramento de informações", + "IPHistory": "Abrir painel de histórico de IP", + "ShareReport": "Abrir diálogo de relatório compartilhável", + "Help": "Mostrar atalhos de teclado", + "HelpNote": "Pessoas que usam atalhos são realmente incríveis!", + "GoNext": "Próximo cartão", + "GoPrevious": "Cartão anterior", + "OpenHighlightedTool": "Abrir ferramenta avançada destacada", + "RefreshRuleTests": "Atualizar testes de regras", + "CensorshipCheck": "Abrir painel de verificação de censura", + "Pulse": "Abrir painel Earth Online", + "Whois": "Abrir painel de pesquisa Whois", + "ServiceStatus": "Abrir painel de status dos serviços", + "InvisibilityTest": "Abrir painel de teste de invisibilidade", + "EnhancedDnsLeakTest": "Abrir painel de teste aprofundado de vazamento de DNS", + "MacChecker": "Abrir painel de consulta MAC", + "BrowserInfo": "Abrir painel de informações do navegador", + "SecurityChecklist": "Abrir painel de checklist de segurança", + "PersonaCheck": "Abrir painel de verificação aprofundada de persona" }, "page": { "title": "IPCheck.ing - Verifique meu IP e geolocalização - Teste de vazamento de IP - Teste de vazamento de DNS - Verificação de qualidade de IP - Teste de velocidade da rede - Caixa de ferramentas de IP tudo em um", @@ -1427,50 +1427,50 @@ "copyRightLinkName": "IPCheck.ing" }, "helpModal": { - "Title": "" + "Title": "Atalhos de teclado" }, "about": { - "Title": "", + "Title": "Sobre", "project": [ - "", - "", - "", - "" + "IPCheck.ing é uma caixa de ferramentas de IP gratuita e tudo em um que ajuda você a verificar suas próprias informações de IP, testar disponibilidade de sites, examinar informações de saída DNS e executar testes de velocidade, latência global, MTR e muito mais.", + "Este é um projeto de código aberto — clique no ícone do GitHub no rodapé para navegar pelo código-fonte.", + "Se você não tiver certeza de como um recurso funciona, abra o link \"Central de ajuda\" no rodapé. A documentação é completa: além de descrever cada recurso, ela também explica conceitos de rede e como solucionar problemas de rede.", + "Se quiser participar do projeto de código aberto, clique no botão \"Contribuir com código\" no rodapé para ver como." ], - "founderTitle": "", + "founderTitle": "Sobre o fundador", "founder": [ - "", - "", - "" + "Isto começou como um projeto paralelo de Jason Ng (阿禅) para aprender Vue.js e escrever código com IA. À medida que reuniu cada vez mais estrelas no GitHub, ele refatorou o projeto profundamente e adicionou muitos recursos realmente úteis. Esperamos que você goste.", + "Ele não é programador profissional. Na maior parte do tempo trabalha como gerente de produto, blogueiro, motociclista e geek — mas ama programação, código aberto e compartilhamento.", + "Você pode encontrar uma apresentação mais detalhada sobre ele nos links abaixo:" ], - "contactTitle": "", + "contactTitle": "Fale conosco", "contact": [ - "", - "", - "" + "Se tiver dificuldades para usar o IPCheck.ing ou implantar o projeto MyIP, acesse primeiro a \"Central de ajuda\". Se não encontrar a resposta e continuar travado, abra uma issue no GitHub ou fale conosco por e-mail.", + "E, se encontrar um bug e gostar de programar (ou programar com IA), será muito bem-vindo enviar um PR com a correção.", + "Nosso e-mail: i[AT]ipcheck.ing." ], - "personal": "", - "blog": "", - "linkedIn": "", - "twitter": "", - "Sponsor": "", - "Privacy": "", - "HelpCenter": "", - "Contribute": "", - "Close": "" + "personal": "Site pessoal", + "blog": "Posts do blog", + "linkedIn": "LinkedIn", + "twitter": "Twitter", + "Sponsor": "Patrocine-nos", + "Privacy": "Privacidade", + "HelpCenter": "Central de ajuda", + "Contribute": "Contribuir com código", + "Close": "Fechar" }, "acknowledgement": { - "Title": "", - "Note1": "" + "Title": "Agradecimentos", + "Note1": "Este projeto não poderia ter sido concluído sem o apoio e a ajuda de pessoas e organizações entusiasmadas, bem como de quem forneceu inspiração ou abriu o código de seus projetos. Muito obrigado a todos:" }, "changelog": { - "Title": "", - "add": "", - "improve": "", - "fix": "" + "Title": "Registro de alterações", + "add": "Adicionado", + "improve": "Melhorado", + "fix": "Corrigido" }, "pwa": { - "appDescription": "", - "installDescription": "" + "appDescription": "Caixa de ferramentas de IP tudo em um", + "installDescription": "Você já usou o IPCheck.ing {count} vezes — instale-o como app para uma experiência mais fluida e acesso mais rápido." } } From 89cbb3540176b5572efc402e97daf2b94071324b Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:00:10 +0800 Subject: [PATCH 05/15] Chore(deps): bump @cloudflare/speedtest and vite --- package.json | 4 +- pnpm-lock.yaml | 205 ++++++++++++++++++++++++++++--------------------- 2 files changed, 118 insertions(+), 91 deletions(-) diff --git a/package.json b/package.json index a5cc42ab9..21199480b 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "i18n-sync": "node scripts/i18n-scaffold.js sync" }, "dependencies": { - "@cloudflare/speedtest": "^1.13.0", + "@cloudflare/speedtest": "^1.13.1", "@iconify-json/circle-flags": "^1.2.10", "@iconify/vue": "^5.0.1", "@khmyznikov/pwa-install": "^0.6.4", @@ -76,6 +76,6 @@ "nodemon": "^3.1.14", "tailwindcss": "^4.3.3", "vconsole": "^3.15.1", - "vite": "^8.2.1" + "vite": "^8.2.2" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9ea86a10b..d7332407a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,8 @@ importers: .: dependencies: '@cloudflare/speedtest': - specifier: ^1.13.0 - version: 1.13.0 + specifier: ^1.13.1 + version: 1.13.1 '@iconify-json/circle-flags': specifier: ^1.2.10 version: 1.2.10 @@ -136,7 +136,7 @@ importers: version: 2.3.1(vue@3.5.41) vue-router: specifier: ^5.2.0 - version: 5.2.0(@vue/compiler-sfc@3.5.41)(pinia@4.0.3(@vue/devtools-api@8.1.5)(vue@3.5.41))(rolldown@1.2.2)(vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41) + version: 5.2.0(@vue/compiler-sfc@3.5.41)(pinia@4.0.3(@vue/devtools-api@8.1.5)(vue@3.5.41))(rolldown@1.2.5)(vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41) vue-sonner: specifier: ^2.0.9 version: 2.0.9 @@ -152,10 +152,10 @@ importers: version: 5.4.0(supports-color@5.5.0) '@tailwindcss/vite': specifier: ^4.3.3 - version: 4.3.3(vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0)) + version: 4.3.3(vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0)) '@vitejs/plugin-vue': specifier: ^6.0.8 - version: 6.0.8(vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41) + version: 6.0.8(vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41) code-inspector-plugin: specifier: ^2.0.7 version: 2.0.7(supports-color@5.5.0) @@ -169,8 +169,8 @@ importers: specifier: ^3.15.1 version: 3.15.1 vite: - specifier: ^8.2.1 - version: 8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + specifier: ^8.2.2 + version: 8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) packages: @@ -286,8 +286,8 @@ packages: resolution: {integrity: sha512-eY+Yn3dCqTGmyiq2QRU66lA5FL8lqqqvecHt0fF3uHONIa7ToYsaCiWV8lOKqAs0Rb2SjixiKFROngnulPtt2g==} engines: {node: ^22.18.0 || >=24.11.0} - '@cloudflare/speedtest@1.13.0': - resolution: {integrity: sha512-Nu3Hc9JlUvuEj/sc47gcRWIUiXRNpuXuNQFSeERhx+87sX214gndmZ7TEWl4TZoAKJyBiEk1jtafUwoWtHkgzg==} + '@cloudflare/speedtest@1.13.1': + resolution: {integrity: sha512-PLk4OqbozJHEutX4xZs2Jg08JYr8ml2HEKbhpooZhYdZzgDy2Cc8RnKl20WewhzaOENZFC8/JQMF7ypw1R8Kaw==} engines: {node: '>=18'} '@code-inspector/core@2.0.7': @@ -690,8 +690,8 @@ packages: resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==} engines: {node: '>=14'} - '@oxc-project/types@0.142.0': - resolution: {integrity: sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==} + '@oxc-project/types@0.146.0': + resolution: {integrity: sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==} '@photostructure/tz-lookup@11.6.1': resolution: {integrity: sha512-azPdhOkwEyNFed8hSR7/U5Xe9eMO3YfbGYUnW3R9wlseimxSw6IkFaYAInDTb1KlbuwTOMzpa5oPcpAOXUM29A==} @@ -726,92 +726,98 @@ packages: '@protobufjs/utf8@1.1.1': resolution: {integrity: sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==} - '@rolldown/binding-android-arm64@1.2.2': - resolution: {integrity: sha512-l7x215OGvo1s52JWmR8U/DAVzEDWBCIbTm28aeJV/WDTSHgcKXaZTuBT0hJMs5NggilfJTW3clZVvd24yfKJxA==} + '@rolldown/binding-android-arm-eabi@1.2.5': + resolution: {integrity: sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [android] + + '@rolldown/binding-android-arm64@1.2.5': + resolution: {integrity: sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rolldown/binding-darwin-arm64@1.2.2': - resolution: {integrity: sha512-9u9Xv6c1AJZT0FfwH5vrMG5Jjcwhc1MlyrPu0XfTqkzsmqfks2M6W/o5XwAJgVVN/jHpqqngC1WevHKKTIUtIA==} + '@rolldown/binding-darwin-arm64@1.2.5': + resolution: {integrity: sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rolldown/binding-darwin-x64@1.2.2': - resolution: {integrity: sha512-9W1mbGZAfW3oqd85bhBkmpyHCCzL1TeG/zFFP3vg7b0rlly8cxOcre5nXwz+LHazCwac2MNWgPdPCHndABjpWQ==} + '@rolldown/binding-darwin-x64@1.2.5': + resolution: {integrity: sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rolldown/binding-freebsd-x64@1.2.2': - resolution: {integrity: sha512-0p1lhiCSCyaerFwtrdZQUx7NqGk6LQnaRKWX7tFQqwQgvX0rjM15cIkm3pax1UpEakK14C4mOxx/jSqCBdBRqQ==} + '@rolldown/binding-freebsd-x64@1.2.5': + resolution: {integrity: sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [freebsd] - '@rolldown/binding-linux-arm-gnueabihf@1.2.2': - resolution: {integrity: sha512-e+cOJXrJ2L3zx6YzqPg+f6Wbk3V1cKB8bOhbaYdVYN3DdquzNdRAmrbETz1qnt5yp/c7JNlNjmITiA2cVneQ7w==} + '@rolldown/binding-linux-arm-gnueabihf@1.2.5': + resolution: {integrity: sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rolldown/binding-linux-arm64-gnu@1.2.2': - resolution: {integrity: sha512-JsSMsj6sNat/MuhG5fnBD7QgbtpHKVe30x5/bAVirDHdhoQRXJkF6xc0Jqk8O4fiCUQAzMOoH9wZi3m60c8wtg==} + '@rolldown/binding-linux-arm64-gnu@1.2.5': + resolution: {integrity: sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-arm64-musl@1.2.2': - resolution: {integrity: sha512-B5G/zJdHaoJn9vD50eGHWkiWfmq8Uhi3IiLPJTzmZTrAalk1bztUikSXo0qga18ibE0IXboyeMUnhPjhAJ45wQ==} + '@rolldown/binding-linux-arm64-musl@1.2.5': + resolution: {integrity: sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] libc: [musl] - '@rolldown/binding-linux-ppc64-gnu@1.2.2': - resolution: {integrity: sha512-6mC/awzKka8W6EoekjegpfGkjz8jXWDX63pqu/HYVpyKtZfu65Jsh4QAH3Kej3CAv/c1oGX7psTmFEbr0mDxLA==} + '@rolldown/binding-linux-ppc64-gnu@1.2.5': + resolution: {integrity: sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-s390x-gnu@1.2.2': - resolution: {integrity: sha512-412MX9fJLdA1IK28EZnc8jYv2HRTleOZgfLQumJ5zy7OeJLZlg/CETwFaXjNmGVxG51cFHpKLqb5LKvBC+HsHA==} + '@rolldown/binding-linux-s390x-gnu@1.2.5': + resolution: {integrity: sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-gnu@1.2.2': - resolution: {integrity: sha512-Q/+HI/ToJafZ1iCqGgVQXUEkIjufHCTF0gBQ2a5o3cg7GJ2h0qyq3nvvSmU+bGda2/7ygXpTY4TM6gO9OhQ0ZA==} + '@rolldown/binding-linux-x64-gnu@1.2.5': + resolution: {integrity: sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [glibc] - '@rolldown/binding-linux-x64-musl@1.2.2': - resolution: {integrity: sha512-ZKp/w41n6wCvxzxQHtQSbuphfX3Y4cCvbjkKHusrLx4lh+JWLTU7StSltO/DKARISzbj368d+qUaCjI8K2wzXw==} + '@rolldown/binding-linux-x64-musl@1.2.5': + resolution: {integrity: sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] libc: [musl] - '@rolldown/binding-openharmony-arm64@1.2.2': - resolution: {integrity: sha512-pxE6xD4KS3eAROkKK5yrhB9/3+vhlhVGMvlQLbdpzrBGDbKrnzx3RLwPaHvasLo6jgaiBLn7e04Df9C4tYhjmA==} + '@rolldown/binding-openharmony-arm64@1.2.5': + resolution: {integrity: sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [openharmony] - '@rolldown/binding-win32-arm64-msvc@1.2.2': - resolution: {integrity: sha512-4MqEue5re+xIZzAWsB8sj0P1kqZySWqIuN4t6QaIO/YA6SFwySOLruvWQFzfmqk8LBK2P30KCSJwf6mJCZZ5/A==} + '@rolldown/binding-win32-arm64-msvc@1.2.5': + resolution: {integrity: sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [win32] - '@rolldown/binding-win32-x64-msvc@1.2.2': - resolution: {integrity: sha512-NweNxxD0Nf9t8v7kodun45Ijp3EIwYY+uydPP6qBEYvfBqhIjN6dZMzlQja3tqX/aLs3F3Uz+AxDpKgRhpOZQg==} + '@rolldown/binding-win32-x64-msvc@1.2.5': + resolution: {integrity: sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] @@ -2121,6 +2127,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@1.0.0: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} @@ -2255,6 +2266,10 @@ packages: resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.26: + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} + process-warning@5.0.0: resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} @@ -2334,8 +2349,8 @@ packages: resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} - rolldown@1.2.2: - resolution: {integrity: sha512-opwpo1tQBAcpSUJDt94B7hhLNGOKjCdE//XXjeLrnx9b83bjnw45tXdg1b09yEw/VLFBJGZpwRULMmOZo7ol+A==} + rolldown@1.2.5: + resolution: {integrity: sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true @@ -2612,13 +2627,13 @@ packages: vconsole@3.15.1: resolution: {integrity: sha512-KH8XLdrq9T5YHJO/ixrjivHfmF2PC2CdVoK6RWZB4yftMykYIaXY1mxZYAic70vADM54kpMQF+dYmvl5NRNy1g==} - vite@8.2.1: - resolution: {integrity: sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==} + vite@8.2.2: + resolution: {integrity: sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==} engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 - '@vitejs/devtools': ^0.4.0 + '@vitejs/devtools': ^0.4.0 || ^0.5.0 esbuild: ^0.27.0 || ^0.28.0 jiti: '>=1.21.0' less: ^4.0.0 @@ -2972,7 +2987,7 @@ snapshots: '@babel/helper-string-parser': 8.0.0 '@babel/helper-validator-identifier': 8.0.4 - '@cloudflare/speedtest@1.13.0': {} + '@cloudflare/speedtest@1.13.1': {} '@code-inspector/core@2.0.7(supports-color@5.5.0)': dependencies: @@ -3521,7 +3536,7 @@ snapshots: '@opentelemetry/semantic-conventions@1.43.0': {} - '@oxc-project/types@0.142.0': {} + '@oxc-project/types@0.146.0': {} '@photostructure/tz-lookup@11.6.1': {} @@ -3547,46 +3562,49 @@ snapshots: '@protobufjs/utf8@1.1.1': {} - '@rolldown/binding-android-arm64@1.2.2': + '@rolldown/binding-android-arm-eabi@1.2.5': optional: true - '@rolldown/binding-darwin-arm64@1.2.2': + '@rolldown/binding-android-arm64@1.2.5': optional: true - '@rolldown/binding-darwin-x64@1.2.2': + '@rolldown/binding-darwin-arm64@1.2.5': optional: true - '@rolldown/binding-freebsd-x64@1.2.2': + '@rolldown/binding-darwin-x64@1.2.5': optional: true - '@rolldown/binding-linux-arm-gnueabihf@1.2.2': + '@rolldown/binding-freebsd-x64@1.2.5': optional: true - '@rolldown/binding-linux-arm64-gnu@1.2.2': + '@rolldown/binding-linux-arm-gnueabihf@1.2.5': optional: true - '@rolldown/binding-linux-arm64-musl@1.2.2': + '@rolldown/binding-linux-arm64-gnu@1.2.5': optional: true - '@rolldown/binding-linux-ppc64-gnu@1.2.2': + '@rolldown/binding-linux-arm64-musl@1.2.5': optional: true - '@rolldown/binding-linux-s390x-gnu@1.2.2': + '@rolldown/binding-linux-ppc64-gnu@1.2.5': optional: true - '@rolldown/binding-linux-x64-gnu@1.2.2': + '@rolldown/binding-linux-s390x-gnu@1.2.5': optional: true - '@rolldown/binding-linux-x64-musl@1.2.2': + '@rolldown/binding-linux-x64-gnu@1.2.5': optional: true - '@rolldown/binding-openharmony-arm64@1.2.2': + '@rolldown/binding-linux-x64-musl@1.2.5': optional: true - '@rolldown/binding-win32-arm64-msvc@1.2.2': + '@rolldown/binding-openharmony-arm64@1.2.5': optional: true - '@rolldown/binding-win32-x64-msvc@1.2.2': + '@rolldown/binding-win32-arm64-msvc@1.2.5': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.2.5': optional: true '@rolldown/pluginutils@1.0.1': {} @@ -3817,12 +3835,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 - '@tailwindcss/vite@4.3.3(vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))': + '@tailwindcss/vite@4.3.3(vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@tailwindcss/node': 4.3.3 '@tailwindcss/oxide': 4.3.3 tailwindcss: 4.3.3 - vite: 8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) '@tanstack/store@0.11.0': {} @@ -3884,10 +3902,10 @@ snapshots: '@types/web-bluetooth@0.0.21': {} - '@vitejs/plugin-vue@6.0.8(vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41)': + '@vitejs/plugin-vue@6.0.8(vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41)': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) vue: 3.5.41 '@vue-macros/common@3.1.3(vue@3.5.41)': @@ -4899,6 +4917,8 @@ snapshots: nanoid@3.3.16: {} + nanoid@3.3.18: {} + negotiator@1.0.0: {} node-addon-api@7.1.1: @@ -5049,6 +5069,12 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.26: + dependencies: + nanoid: 3.3.18 + picocolors: 1.1.1 + source-map-js: 1.2.1 + process-warning@5.0.0: {} progress@2.0.3: {} @@ -5137,25 +5163,26 @@ snapshots: transitivePeerDependencies: - supports-color - rolldown@1.2.2: + rolldown@1.2.5: dependencies: - '@oxc-project/types': 0.142.0 + '@oxc-project/types': 0.146.0 '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rolldown/binding-android-arm64': 1.2.2 - '@rolldown/binding-darwin-arm64': 1.2.2 - '@rolldown/binding-darwin-x64': 1.2.2 - '@rolldown/binding-freebsd-x64': 1.2.2 - '@rolldown/binding-linux-arm-gnueabihf': 1.2.2 - '@rolldown/binding-linux-arm64-gnu': 1.2.2 - '@rolldown/binding-linux-arm64-musl': 1.2.2 - '@rolldown/binding-linux-ppc64-gnu': 1.2.2 - '@rolldown/binding-linux-s390x-gnu': 1.2.2 - '@rolldown/binding-linux-x64-gnu': 1.2.2 - '@rolldown/binding-linux-x64-musl': 1.2.2 - '@rolldown/binding-openharmony-arm64': 1.2.2 - '@rolldown/binding-win32-arm64-msvc': 1.2.2 - '@rolldown/binding-win32-x64-msvc': 1.2.2 + '@rolldown/binding-android-arm-eabi': 1.2.5 + '@rolldown/binding-android-arm64': 1.2.5 + '@rolldown/binding-darwin-arm64': 1.2.5 + '@rolldown/binding-darwin-x64': 1.2.5 + '@rolldown/binding-freebsd-x64': 1.2.5 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.5 + '@rolldown/binding-linux-arm64-gnu': 1.2.5 + '@rolldown/binding-linux-arm64-musl': 1.2.5 + '@rolldown/binding-linux-ppc64-gnu': 1.2.5 + '@rolldown/binding-linux-s390x-gnu': 1.2.5 + '@rolldown/binding-linux-x64-gnu': 1.2.5 + '@rolldown/binding-linux-x64-musl': 1.2.5 + '@rolldown/binding-openharmony-arm64': 1.2.5 + '@rolldown/binding-win32-arm64-msvc': 1.2.5 + '@rolldown/binding-win32-x64-msvc': 1.2.5 router@2.2.0(supports-color@5.5.0): dependencies: @@ -5384,14 +5411,14 @@ snapshots: pathe: 2.0.3 picomatch: 4.0.5 - unplugin@3.3.0(rolldown@1.2.2)(vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0)): + unplugin@3.3.0(rolldown@1.2.5)(vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0)): dependencies: '@jridgewell/remapping': 2.3.5 picomatch: 4.0.5 webpack-virtual-modules: 0.6.2 optionalDependencies: - rolldown: 1.2.2 - vite: 8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + rolldown: 1.2.5 + vite: 8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) update-browserslist-db@1.2.3(browserslist@4.28.5): dependencies: @@ -5416,12 +5443,12 @@ snapshots: core-js: 3.49.0 mutation-observer: 1.0.3 - vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0): + vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: lightningcss: 1.33.0 picomatch: 4.0.5 - postcss: 8.5.25 - rolldown: 1.2.2 + postcss: 8.5.26 + rolldown: 1.2.5 tinyglobby: 0.2.17 optionalDependencies: '@types/node': 24.1.0 @@ -5455,7 +5482,7 @@ snapshots: markdown-it: 14.2.0 vue: 3.5.41 - vue-router@5.2.0(@vue/compiler-sfc@3.5.41)(pinia@4.0.3(@vue/devtools-api@8.1.5)(vue@3.5.41))(rolldown@1.2.2)(vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41): + vue-router@5.2.0(@vue/compiler-sfc@3.5.41)(pinia@4.0.3(@vue/devtools-api@8.1.5)(vue@3.5.41))(rolldown@1.2.5)(vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0))(vue@3.5.41): dependencies: '@babel/generator': 8.0.0 '@vue-macros/common': 3.1.3(vue@3.5.41) @@ -5472,14 +5499,14 @@ snapshots: picomatch: 4.0.5 scule: 1.3.0 tinyglobby: 0.2.17 - unplugin: 3.3.0(rolldown@1.2.2)(vite@8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0)) + unplugin: 3.3.0(rolldown@1.2.5)(vite@8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0)) unplugin-utils: 0.3.2 vue: 3.5.41 yaml: 2.9.0 optionalDependencies: '@vue/compiler-sfc': 3.5.41 pinia: 4.0.3(@vue/devtools-api@8.1.5)(vue@3.5.41) - vite: 8.2.1(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) + vite: 8.2.2(@types/node@24.1.0)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - '@farmfe/core' - '@rspack/core' From 8f2751b15fe8daefc7926f4d1becdd51321976bd Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:00:10 +0800 Subject: [PATCH 06/15] Fix(logging): log 403 IP-source fail-over as warning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A source declining with 403 is a fail-over, not a defect — keep it below the level error monitoring captures, while every other failure stays an error so a real outage still surfaces. --- frontend/components/IpInfos.vue | 4 +- frontend/components/widgets/QueryIP.vue | 4 +- frontend/utils/authenticated-fetch.js | 6 +++ tests/authenticated-fetch.test.js | 54 ++++++++++++++++++++++++- 4 files changed, 63 insertions(+), 5 deletions(-) diff --git a/frontend/components/IpInfos.vue b/frontend/components/IpInfos.vue index e53dda9d4..f70f94484 100644 --- a/frontend/components/IpInfos.vue +++ b/frontend/components/IpInfos.vue @@ -41,7 +41,7 @@ import { transformDataFromIPapi } from '@/utils/transform-ip-data.js'; import { getIPFromIPIP, getIPFromCloudflare_V4, getIPFromCloudflare_V6, getIPFromIPChecking64, getIPFromIPChecking4, getIPFromIPChecking6 } from '@/utils/getips'; import { emitAppEvent, waitForAppEvent } from '@/utils/app-events'; import { useAppCommand } from '@/composables/use-app-command.js'; -import { authenticatedFetch, fetchErrorLabel } from '@/utils/authenticated-fetch'; +import { authenticatedFetch, fetchErrorLabel, logSourceFetchFailure } from '@/utils/authenticated-fetch'; import IPCard from './ip-infos/IPCard.vue'; import InfoBanner from './widgets/InfoBanner.vue'; @@ -348,7 +348,7 @@ const fetchIPDetails = async (cardIndex, ip, sourceID = null) => { return; } } catch (error) { - console.error(`Error fetching IP details from source ${source.id} (${fetchErrorLabel(error)}):`, error); + logSourceFetchFailure(`Error fetching IP details from source ${source.id} (${fetchErrorLabel(error)}):`, error); currentSourceIndex = (currentSourceIndex + 1) % sources.length; attempts++; } diff --git a/frontend/components/widgets/QueryIP.vue b/frontend/components/widgets/QueryIP.vue index 870a988f4..565633078 100644 --- a/frontend/components/widgets/QueryIP.vue +++ b/frontend/components/widgets/QueryIP.vue @@ -69,7 +69,7 @@ import { HERO_TIERS } from '@/composables/use-fit-text.js'; import { transformDataFromIPapi } from '@/utils/transform-ip-data.js'; import { useI18n } from 'vue-i18n'; import { trackEvent } from '@/utils/analytics'; -import { authenticatedFetch, fetchErrorLabel } from '@/utils/authenticated-fetch'; +import { authenticatedFetch, fetchErrorLabel, logSourceFetchFailure } from '@/utils/authenticated-fetch'; import { Dialog, DialogContent, DialogHeader } from '@/components/ui/dialog'; import { JnTooltip } from '@/components/ui/tooltip'; import { Button } from '@/components/ui/button'; @@ -164,7 +164,7 @@ const fetchIPForModal = async (ip) => { isChecking.value = 'idle'; return; } catch (error) { - console.error(`Error fetching IP details from source ${source.id} (${fetchErrorLabel(error)}):`, error); + logSourceFetchFailure(`Error fetching IP details from source ${source.id} (${fetchErrorLabel(error)}):`, error); currentIdx = (currentIdx + 1) % sources.length; attempts++; } diff --git a/frontend/utils/authenticated-fetch.js b/frontend/utils/authenticated-fetch.js index 295e4b98e..98665551c 100644 --- a/frontend/utils/authenticated-fetch.js +++ b/frontend/utils/authenticated-fetch.js @@ -9,6 +9,12 @@ import { fetchWithTimeout } from './fetch-with-timeout.js'; // Bounded label for a failed authenticatedFetch export const fetchErrorLabel = (error) => (error?.status ? `HTTP ${error.status}` : 'network'); +// Log one IP-geolocation source failing, at the level it deserves. 403 should be logged as a warning, everything else as an error. +export const logSourceFetchFailure = (message, error) => { + if (error?.status === 403) console.warn(message, error); + else console.error(message, error); +}; + export async function authenticatedFetch(url, method = 'GET', body = null, timeoutMs = 10000) { const store = useMainStore(); const options = { diff --git a/tests/authenticated-fetch.test.js b/tests/authenticated-fetch.test.js index 5a546ab87..aa9270568 100644 --- a/tests/authenticated-fetch.test.js +++ b/tests/authenticated-fetch.test.js @@ -29,7 +29,7 @@ import assert from 'node:assert/strict'; import { describe, it, beforeEach, afterEach } from 'node:test'; import { createPinia, setActivePinia } from 'pinia'; -const { authenticatedFetch, fetchErrorLabel } = await import('../frontend/utils/authenticated-fetch.js'); +const { authenticatedFetch, fetchErrorLabel, logSourceFetchFailure } = await import('../frontend/utils/authenticated-fetch.js'); const realFetch = globalThis.fetch; @@ -113,3 +113,55 @@ describe('authenticatedFetch — error shape', () => { assert.deepEqual(await authenticatedFetch('/api/ipchecking?ip=1.1.1.1'), { ip: '1.1.1.1' }); }); }); + +// A source declining with 403 is a fail-over, not a defect: it must log below +// the level error monitoring captures, while every other failure stays an +// error so a real outage still surfaces. +describe('logSourceFetchFailure', () => { + const captured = { warn: [], error: [] }; + const realWarn = console.warn; + const realError = console.error; + + beforeEach(() => { + captured.warn = []; + captured.error = []; + console.warn = (...args) => captured.warn.push(args); + console.error = (...args) => captured.error.push(args); + }); + + afterEach(() => { + console.warn = realWarn; + console.error = realError; + }); + + const failWith = async (status, opts) => { + globalThis.fetch = async () => httpResponse(status, null, opts); + return authenticatedFetch('/api/ipchecking?ip=1.1.1.1').catch((e) => e); + }; + + it('warns on 403', async () => { + logSourceFetchFailure('source 0', await failWith(403, { json: false })); + assert.equal(captured.warn.length, 1); + assert.equal(captured.error.length, 0); + }); + + it('keeps 500 at error level', async () => { + logSourceFetchFailure('source 0', await failWith(500, { json: true })); + assert.equal(captured.error.length, 1); + assert.equal(captured.warn.length, 0); + }); + + it('keeps other 4xx at error level', async () => { + logSourceFetchFailure('source 0', await failWith(400, { json: true })); + assert.equal(captured.error.length, 1); + assert.equal(captured.warn.length, 0); + }); + + it('keeps a status-less network failure at error level', async () => { + globalThis.fetch = async () => { throw new TypeError('Failed to fetch'); }; + const error = await authenticatedFetch('/api/ipchecking?ip=1.1.1.1').catch((e) => e); + logSourceFetchFailure('source 0', error); + assert.equal(captured.error.length, 1); + assert.equal(captured.warn.length, 0); + }); +}); From 4a2085abac2ab1a756dfd67faa610a769625f394 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:00:10 +0800 Subject: [PATCH 07/15] Style(preferences): switch Beta badge to outline variant --- frontend/components/widgets/Preferences.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/components/widgets/Preferences.vue b/frontend/components/widgets/Preferences.vue index 41f35492c..a1debffe2 100644 --- a/frontend/components/widgets/Preferences.vue +++ b/frontend/components/widgets/Preferences.vue @@ -42,8 +42,8 @@ {{ lang.label }} - Beta + Beta From 29ae0ff45fafe4bc85bf10f67b7a3e72a6f450c5 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:00:21 +0800 Subject: [PATCH 08/15] Fix(i18n): refine four pt-BR strings Post-merge polish on #426: 'Visitor share' and the persona-check header/script labels named the wrong referent, and the service-status banner read as 'operating systems'; keep the banner short enough for its card. Co-Authored-By: Claude Fable 5 --- frontend/locales/pt-BR.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/locales/pt-BR.json b/frontend/locales/pt-BR.json index 8e6037b72..6837916b0 100644 --- a/frontend/locales/pt-BR.json +++ b/frontend/locales/pt-BR.json @@ -229,7 +229,7 @@ "noVisitors": "Nenhum visitante ainda", "quiet": "Tudo tranquilo agora — seja a primeira pessoa a compartilhar", "loadError": "Não foi possível carregar a atividade mais recente — tente novamente em instantes", - "share": "Compartilhamento de visitante", + "share": "Participação de visitantes", "latest": "Mais recente", "sent": "Enviado", "alreadySent": "Este status já foi enviado", @@ -691,8 +691,8 @@ "voiceCount": "Vozes", "expectedLayout": "Layout esperado", "actualLayout": "Layout atual", - "headerPrimary": "Cabeçalho principal", - "scriptPrimary": "Script principal", + "headerPrimary": "Idioma principal (cabeçalho)", + "scriptPrimary": "Idioma principal (script)", "headerLanguages": "Idiomas do cabeçalho", "ipZone": "Fuso horário do IP", "browserZone": "Fuso horário do navegador", @@ -1159,7 +1159,7 @@ "Note": "Quando um serviço não abre, talvez o problema não seja seu — o próprio serviço pode estar fora do ar. Atualmente, muitas pessoas usam produtos de IA, e quando o produto de IA não responde, elas não sabem se é problema delas ou do serviço. Esta seção mostra a disponibilidade ao vivo de serviços conhecidos de IA; expanda um cartão para ver o status de cada subserviço e incidentes recentes.", "Refresh": "Atualizar status dos serviços", "LastChecked": "Última verificação", - "AllOperational": "Todos os sistemas operacionais", + "AllOperational": "Sistemas operando normalmente", "ComponentsTitle": "Serviços", "IncidentsTitle": "Incidentes recentes", "NoComponents": "Nenhum detalhe de serviço disponível.", From 46a42b1e65b5de6108396f9fd043e83e4d55f224 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:00:21 +0800 Subject: [PATCH 09/15] Feat(ci): report translation status on locale PRs CI renders the i18n-status dashboard into the run summary and, when a PR touches locale data, hands it to a workflow_run job as an artifact; that job upserts one sticky PR comment. Split in two on purpose: the report is computed under the read-only token fork PRs get, and only trusted default-branch code holds the write token. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 49 ++++++++++++++++++++ .github/workflows/i18n-comment.yml | 73 ++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 .github/workflows/i18n-comment.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c85244fa6..4faa80861 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,55 @@ jobs: # out of sync with package.json, rather than silently rewriting it. run: pnpm install --frozen-lockfile + - name: Translation status + # Locale progress dashboard into the run summary, so a translation PR's + # author and reviewer see coverage and the next keys to translate without + # a local run. Report only — the script always exits 0; the mechanical + # gate for locale packs lives in pnpm test below. + run: | + { + echo '```text' + node scripts/i18n-status.js + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Prepare translation report artifact + # Locale-touching PRs get a sticky status comment, posted by + # i18n-comment.yml (this job's token is read-only, and fork PRs never + # see secrets — so the report is computed here and handed over as an + # artifact). No locale changes → no artifact → no comment. + if: github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + if ! gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate \ + --jq '.[].filename' \ + | grep -qE '^(frontend/locales/|common/locale-registry\.js|frontend/data/changelog\.json)'; then + exit 0 + fi + mkdir -p i18n-report + { + echo '' + echo '### 🌍 Translation status' + echo '' + echo '```text' + node scripts/i18n-status.js + echo '```' + echo "Untranslated keys fall back to English — partial PRs are welcome, and every extra key helps. Guide: [TRANSLATING.md](https://github.com/$GITHUB_REPOSITORY/blob/dev/TRANSLATING.md)." + } > i18n-report/report.md + printf '%s\n' "$PR_NUMBER" > i18n-report/pr-number + printf '%s\n' "$HEAD_SHA" > i18n-report/head-sha + + - name: Upload translation report + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v7 + with: + name: i18n-report + path: i18n-report/ + if-no-files-found: ignore + - name: Run tests run: pnpm test diff --git a/.github/workflows/i18n-comment.yml b/.github/workflows/i18n-comment.yml new file mode 100644 index 000000000..a2b368fb6 --- /dev/null +++ b/.github/workflows/i18n-comment.yml @@ -0,0 +1,73 @@ +name: i18n PR comment + +# Posts / updates one sticky translation-status comment on locale PRs. +# +# Deliberately split from CI: the report is computed there, under the +# read-only token fork PRs get, and handed over as an artifact. This +# workflow runs only trusted code from the default branch and holds the +# pull-requests: write token — never check out or execute anything from +# the PR here. +on: + workflow_run: + workflows: [CI] + types: [completed] + +permissions: + pull-requests: write + actions: read + +jobs: + comment: + # Only PR runs can carry the artifact; push runs are skipped outright. + if: github.event.workflow_run.event == 'pull_request' + runs-on: ubuntu-latest + + steps: + - name: Download translation report + id: download + # No artifact = the PR didn't touch locale data; treat as a clean skip. + continue-on-error: true + uses: actions/download-artifact@v8 + with: + name: i18n-report + path: i18n-report + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Upsert sticky comment + if: steps.download.outcome == 'success' + uses: actions/github-script@v9 + with: + script: | + const fs = require('fs'); + const read = (f) => fs.readFileSync(`i18n-report/${f}`, 'utf8').trim(); + + const prNumber = Number.parseInt(read('pr-number'), 10); + const headSha = read('head-sha'); + // GitHub caps comment bodies at 65536 chars; the report is far + // smaller, but the artifact is untrusted input — cap it anyway. + const report = read('report.md').slice(0, 60000); + if (!Number.isInteger(prNumber) || prNumber <= 0) return; + + // The artifact came from an untrusted run: only write to the PR + // whose head actually triggered that run. + const { data: pr } = await github.rest.pulls.get({ + ...context.repo, pull_number: prNumber, + }); + const runHead = context.payload.workflow_run.head_sha; + if (pr.head.sha !== runHead || headSha !== runHead) return; + + const marker = ''; + const { data: comments } = await github.rest.issues.listComments({ + ...context.repo, issue_number: prNumber, per_page: 100, + }); + const existing = comments.find((c) => c.body?.startsWith(marker)); + if (existing) { + await github.rest.issues.updateComment({ + ...context.repo, comment_id: existing.id, body: report, + }); + } else { + await github.rest.issues.createComment({ + ...context.repo, issue_number: prNumber, body: report, + }); + } From 09b63acc127d4c9690377d9233b62d5b9f278d94 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:00:21 +0800 Subject: [PATCH 10/15] Feat(ci): greet newly opened PRs First-time contributors get the full onboarding message, returning contributors a shorter welcome back; maintainer and bot PRs are skipped. No PR code is ever checked out or executed here, which is what makes pull_request_target safe for this job. Co-Authored-By: Claude Fable 5 --- .github/workflows/pr-welcome.yml | 67 ++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/workflows/pr-welcome.yml diff --git a/.github/workflows/pr-welcome.yml b/.github/workflows/pr-welcome.yml new file mode 100644 index 000000000..68dc67453 --- /dev/null +++ b/.github/workflows/pr-welcome.yml @@ -0,0 +1,67 @@ +name: PR welcome + +# Greets every newly opened PR from an outside contributor: first-time +# contributors get the full onboarding message, returning contributors a +# shorter welcome-back one. Maintainer PRs (owner / member / collaborator) +# and bot PRs are skipped. +# +# pull_request_target is safe here precisely because this workflow never +# checks out or executes anything from the PR — it only posts a comment +# with trusted code from the base branch. Keep it that way: no checkout, +# no build steps in this file, ever. It also means the greeting posts +# immediately, even while CI still sits behind the fork-approval gate. +on: + pull_request_target: + types: [opened] + +permissions: + pull-requests: write + +jobs: + welcome: + if: >- + !contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.pull_request.author_association) + && github.event.pull_request.user.type != 'Bot' + runs-on: ubuntu-latest + + steps: + - name: Post greeting + uses: actions/github-script@v9 + with: + script: | + const pr = context.payload.pull_request; + const author = pr.user.login; + const repoUrl = `https://github.com/${process.env.GITHUB_REPOSITORY}`; + + // The PR just opened is included in the count, so 1 = first ever. + const { data: search } = await github.rest.search.issuesAndPullRequests({ + q: `repo:${context.repo.owner}/${context.repo.repo} is:pr author:${author}`, + per_page: 1, + }); + const isFirstTime = search.total_count <= 1; + + const firstTime = [ + `Thanks for your first PR here, @${author} — contributions like this are what keep MyIP going! 🙏`, + '', + 'This project has a single maintainer, so reviews are usually quick but not instant — expect a response within a day or two. A couple of things that speed a merge up:', + '', + '- `pnpm check` green locally (tests + build) is the fastest path through review.', + `- Conventions live in [AGENTS.md](${repoUrl}/blob/dev/AGENTS.md); translation PRs additionally in [TRANSLATING.md](${repoUrl}/blob/dev/TRANSLATING.md).`, + "- If the checks tab says a workflow is *awaiting approval*, that's normal for first-time contributors — the maintainer approves it as part of the review.", + '', + '_This is an automated greeting; a human review follows._', + ]; + + const returning = [ + `Thanks for another PR, @${author} — always good to see you back! 🙌`, + '', + 'The maintainer will take a look shortly (usually within a day or two). As ever, `pnpm check` green locally is the fastest path through review.', + '', + '_This is an automated greeting; a human review follows._', + ]; + + await github.rest.issues.createComment({ + ...context.repo, + issue_number: pr.number, + body: (isFirstTime ? firstTime : returning).join('\n'), + }); From b411af164b97294c35f4f4ce3b83a47dd0c7f129 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:27:45 +0800 Subject: [PATCH 11/15] Fix(i18n): shorten pt-BR navbar labels Four labels wrapped to two lines and misaligned the desktop navbar; bring them down to the length the fr/ru packs already fit in. Co-Authored-By: Claude Fable 5 --- frontend/locales/pt-BR.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/locales/pt-BR.json b/frontend/locales/pt-BR.json index 6837916b0..bf347f931 100644 --- a/frontend/locales/pt-BR.json +++ b/frontend/locales/pt-BR.json @@ -193,12 +193,12 @@ "id": "nav", "Title": "IPCheck.ing", "Navigation": "Navegação", - "IPInfo": "Informações de IP", + "IPInfo": "Dados de IP", "Connectivity": "Conectividade", "ServiceStatus": "Status dos serviços", - "WebRTC": "Teste de vazamento WebRTC", - "DNSLeakTest": "Teste de vazamento de DNS", - "SpeedTest": "Teste de velocidade", + "WebRTC": "Vazamento WebRTC", + "DNSLeakTest": "Vazamento de DNS", + "SpeedTest": "Velocidade", "PingTest": "Latência global", "MTRTest": "Teste MTR", "RuleTest": "Teste de regras", @@ -342,7 +342,7 @@ "ipDB": "Banco de geolocalização de IP", "ipDBTips": "Você pode selecionar a fonte padrão de geolocalização de IP. Se a fonte selecionada estiver indisponível, o sistema usará as fontes seguintes em ordem.", "language": "Configuração de idioma", - "languageTips": "Atualize o navegador para a alteração entrar em vigor." + "languageTips": "Atualize o navegador para a alteração entrar em vigor. Idiomas em beta podem conter traduções incompletas." }, "DocsPrompt": "Digite sua pergunta…", "DocsSignInTitle": "Login necessário", From 16d3e1675c02f6a52827a458566bfc8c606a0544 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:27:45 +0800 Subject: [PATCH 12/15] Fix(ci): treat the i18n report artifact as untrusted text The report is produced by the PR's own code, so the privileged comment workflow no longer republishes its Markdown: the artifact now carries raw text, and the trusted side strips backticks, caps the size and renders it inside a code fence it authors itself. Also paginate the sticky-comment lookup past 100 comments, and move both new workflows' purpose headers to line one per the file-header convention. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 14 +++++-------- .github/workflows/i18n-comment.yml | 33 ++++++++++++++++++++++-------- .github/workflows/pr-welcome.yml | 4 ++-- 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4faa80861..3139698ba 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -54,6 +54,10 @@ jobs: # i18n-comment.yml (this job's token is read-only, and fork PRs never # see secrets — so the report is computed here and handed over as an # artifact). No locale changes → no artifact → no comment. + # + # The artifact carries RAW TEXT only. This run executes the PR's own + # code, so nothing produced here may reach a comment as Markdown — + # the privileged side sanitizes the text and owns all formatting. if: github.event_name == 'pull_request' env: GH_TOKEN: ${{ github.token }} @@ -66,15 +70,7 @@ jobs: exit 0 fi mkdir -p i18n-report - { - echo '' - echo '### 🌍 Translation status' - echo '' - echo '```text' - node scripts/i18n-status.js - echo '```' - echo "Untranslated keys fall back to English — partial PRs are welcome, and every extra key helps. Guide: [TRANSLATING.md](https://github.com/$GITHUB_REPOSITORY/blob/dev/TRANSLATING.md)." - } > i18n-report/report.md + node scripts/i18n-status.js > i18n-report/report.txt printf '%s\n' "$PR_NUMBER" > i18n-report/pr-number printf '%s\n' "$HEAD_SHA" > i18n-report/head-sha diff --git a/.github/workflows/i18n-comment.yml b/.github/workflows/i18n-comment.yml index a2b368fb6..96c78095c 100644 --- a/.github/workflows/i18n-comment.yml +++ b/.github/workflows/i18n-comment.yml @@ -1,5 +1,3 @@ -name: i18n PR comment - # Posts / updates one sticky translation-status comment on locale PRs. # # Deliberately split from CI: the report is computed there, under the @@ -7,6 +5,14 @@ name: i18n PR comment # workflow runs only trusted code from the default branch and holds the # pull-requests: write token — never check out or execute anything from # the PR here. +# +# The artifact is untrusted input (a PR can change scripts/i18n-status.js +# to print whatever it likes). It is therefore treated as opaque text: +# backticks stripped, length capped, always rendered inside a code fence. +# Every piece of Markdown around it is authored here, by trusted code — +# nothing from the artifact can become a link, an image or formatting. +name: i18n PR comment + on: workflow_run: workflows: [CI] @@ -44,9 +50,9 @@ jobs: const prNumber = Number.parseInt(read('pr-number'), 10); const headSha = read('head-sha'); - // GitHub caps comment bodies at 65536 chars; the report is far - // smaller, but the artifact is untrusted input — cap it anyway. - const report = read('report.md').slice(0, 60000); + // Neutralize the untrusted report: no backticks (so it can never + // escape the fence below), bounded size (comment cap is 65536). + const report = read('report.txt').replace(/`/g, '').slice(0, 55000); if (!Number.isInteger(prNumber) || prNumber <= 0) return; // The artifact came from an untrusted run: only write to the PR @@ -58,16 +64,27 @@ jobs: if (pr.head.sha !== runHead || headSha !== runHead) return; const marker = ''; - const { data: comments } = await github.rest.issues.listComments({ + const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`; + const body = [ + marker, + '### 🌍 Translation status', + '', + '```text', + report, + '```', + `Untranslated keys fall back to English — partial PRs are welcome, and every extra key helps. Guide: [TRANSLATING.md](${repoUrl}/blob/dev/TRANSLATING.md).`, + ].join('\n'); + + const comments = await github.paginate(github.rest.issues.listComments, { ...context.repo, issue_number: prNumber, per_page: 100, }); const existing = comments.find((c) => c.body?.startsWith(marker)); if (existing) { await github.rest.issues.updateComment({ - ...context.repo, comment_id: existing.id, body: report, + ...context.repo, comment_id: existing.id, body, }); } else { await github.rest.issues.createComment({ - ...context.repo, issue_number: prNumber, body: report, + ...context.repo, issue_number: prNumber, body, }); } diff --git a/.github/workflows/pr-welcome.yml b/.github/workflows/pr-welcome.yml index 68dc67453..7a24352aa 100644 --- a/.github/workflows/pr-welcome.yml +++ b/.github/workflows/pr-welcome.yml @@ -1,5 +1,3 @@ -name: PR welcome - # Greets every newly opened PR from an outside contributor: first-time # contributors get the full onboarding message, returning contributors a # shorter welcome-back one. Maintainer PRs (owner / member / collaborator) @@ -10,6 +8,8 @@ name: PR welcome # with trusted code from the base branch. Keep it that way: no checkout, # no build steps in this file, ever. It also means the greeting posts # immediately, even while CI still sits behind the fork-approval gate. +name: PR welcome + on: pull_request_target: types: [opened] From 5217815f545b089f44f6988b774ca8cd41a294be Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:28:43 +0800 Subject: [PATCH 13/15] Improvements --- frontend/components/widgets/Preferences.vue | 471 ++++++++++---------- frontend/locales/en.json | 2 +- frontend/locales/fr.json | 2 +- frontend/locales/ru.json | 2 +- frontend/locales/zh.json | 2 +- 5 files changed, 233 insertions(+), 246 deletions(-) diff --git a/frontend/components/widgets/Preferences.vue b/frontend/components/widgets/Preferences.vue index a1debffe2..2deb43054 100644 --- a/frontend/components/widgets/Preferences.vue +++ b/frontend/components/widgets/Preferences.vue @@ -1,173 +1,162 @@ diff --git a/frontend/locales/en.json b/frontend/locales/en.json index c7398c0a1..0c4b71e32 100644 --- a/frontend/locales/en.json +++ b/frontend/locales/en.json @@ -342,7 +342,7 @@ "ipDB": "IP Geolocation Database", "ipDBTips": "You can select the default IP geolocation source to use. If the selected source is unavailable, the system will use the subsequent sources in order.", "language": "Language Setting", - "languageTips": "Refreshing the browser will take effect." + "languageTips": "Refreshing the browser will take effect. Languages in beta may contain incomplete translations." }, "DocsPrompt": "Type your question…", "DocsSignInTitle": "Sign In Required", diff --git a/frontend/locales/fr.json b/frontend/locales/fr.json index 3b5a86722..08208b2af 100644 --- a/frontend/locales/fr.json +++ b/frontend/locales/fr.json @@ -342,7 +342,7 @@ "ipDB": "Base de données de géolocalisation IP", "ipDBTips": "Vous pouvez sélectionner la source de données de géolocalisation IP par défaut. Si la source sélectionnée n'est pas disponible, le système utilisera les sources suivantes.", "language": "Paramètres de langue", - "languageTips": "Rafraîchissement du navigateur pour prendre effet." + "languageTips": "Rafraîchissement du navigateur pour prendre effet. Les idiomes en bêta peuvent contenir des traductions incomplètes." }, "DocsPrompt": "Saisissez votre question…", "DocsSignInTitle": "Connexion requise", diff --git a/frontend/locales/ru.json b/frontend/locales/ru.json index 6fa20462f..08d4d4922 100644 --- a/frontend/locales/ru.json +++ b/frontend/locales/ru.json @@ -342,7 +342,7 @@ "ipDB": "База геолокации IP", "ipDBTips": "Можно выбрать основной источник геолокации IP. Если он недоступен, система по очереди использует следующие источники.", "language": "Язык", - "languageTips": "Изменение вступит в силу после обновления страницы." + "languageTips": "Изменение вступит в силу после обновления страницы. Языки в бета-версии могут содержать неполные переводы." }, "DocsPrompt": "Введите свой вопрос…", "DocsSignInTitle": "Требуется вход", diff --git a/frontend/locales/zh.json b/frontend/locales/zh.json index c0ce6849a..47e9aea8c 100644 --- a/frontend/locales/zh.json +++ b/frontend/locales/zh.json @@ -342,7 +342,7 @@ "ipDB": "IP 解析数据源", "ipDBTips": "你可以选择你默认使用的 IP 地理位置数据源,如果你选定的不可用,系统会依次使用后续的数据源。", "language": "语言设置", - "languageTips": "刷新浏览器后生效。" + "languageTips": "刷新浏览器后生效。正在 Beta 测试中的语言可能会存在翻译不完整的情况。" }, "DocsPrompt": "请输入你要询问的问题", "DocsSignInTitle": "请先登录", From 10b3368f67962d1f9855379c6929868f3ee81fd1 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 12:41:00 +0800 Subject: [PATCH 14/15] Fix(ci): render the i18n comment from validated numbers only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Even fenced plain text from the PR's run let a hostile PR speak in the bot's voice. The artifact now carries a structured --json snapshot (counts, locale codes, key paths); the privileged side validates every field against enums, integer bounds and tight regexes, then renders the comment text entirely with trusted code. A hostile PR can at most misstate its own coverage numbers — it cannot emit prose, links or instructions as the bot. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 10 +++--- .github/workflows/i18n-comment.yml | 58 +++++++++++++++++++++++++----- scripts/i18n-status.js | 24 ++++++++++++- 3 files changed, 79 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3139698ba..d9226ce1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,9 +55,11 @@ jobs: # see secrets — so the report is computed here and handed over as an # artifact). No locale changes → no artifact → no comment. # - # The artifact carries RAW TEXT only. This run executes the PR's own - # code, so nothing produced here may reach a comment as Markdown — - # the privileged side sanitizes the text and owns all formatting. + # The artifact carries STRUCTURED NUMBERS only (--json: counts, locale + # codes, key paths). This run executes the PR's own code, so nothing + # produced here reaches a comment as text — the privileged side + # validates every field against strict shapes and renders the comment + # entirely with its own trusted code. if: github.event_name == 'pull_request' env: GH_TOKEN: ${{ github.token }} @@ -70,7 +72,7 @@ jobs: exit 0 fi mkdir -p i18n-report - node scripts/i18n-status.js > i18n-report/report.txt + node scripts/i18n-status.js --json > i18n-report/report.json printf '%s\n' "$PR_NUMBER" > i18n-report/pr-number printf '%s\n' "$HEAD_SHA" > i18n-report/head-sha diff --git a/.github/workflows/i18n-comment.yml b/.github/workflows/i18n-comment.yml index 96c78095c..a971d0f12 100644 --- a/.github/workflows/i18n-comment.yml +++ b/.github/workflows/i18n-comment.yml @@ -7,10 +7,13 @@ # the PR here. # # The artifact is untrusted input (a PR can change scripts/i18n-status.js -# to print whatever it likes). It is therefore treated as opaque text: -# backticks stripped, length capped, always rendered inside a code fence. -# Every piece of Markdown around it is authored here, by trusted code — -# nothing from the artifact can become a link, an image or formatting. +# to emit whatever it likes), so no artifact byte ever reaches the comment. +# It carries structured claims — counts, locale codes, key paths — and each +# field is validated against a strict shape (enums, integer bounds, tight +# regexes) before trusted code here renders the comment text from scratch. +# A hostile PR can at most misstate its own coverage numbers; it cannot put +# a single word of prose, a link, or an instruction into the bot's mouth. +# (The numbers are advisory anyway — the merge gate is pnpm test.) name: i18n PR comment on: @@ -50,11 +53,11 @@ jobs: const prNumber = Number.parseInt(read('pr-number'), 10); const headSha = read('head-sha'); - // Neutralize the untrusted report: no backticks (so it can never - // escape the fence below), bounded size (comment cap is 65536). - const report = read('report.txt').replace(/`/g, '').slice(0, 55000); if (!Number.isInteger(prNumber) || prNumber <= 0) return; + let data; + try { data = JSON.parse(read('report.json')); } catch { return; } + // The artifact came from an untrusted run: only write to the PR // whose head actually triggered that run. const { data: pr } = await github.rest.pulls.get({ @@ -63,6 +66,45 @@ jobs: const runHead = context.payload.workflow_run.head_sha; if (pr.head.sha !== runHead || headSha !== runHead) return; + // Validate-then-render: every value below either matches a strict + // shape or its row is dropped. Only validated numbers, enum labels + // and regex-tight identifiers make it into the rendered text. + const CODE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})?$/; + const KEY = /^[\w./-]{1,120}$/; + const DATASET_LABELS = ['main pack', 'privacy copy', 'checklist']; + const int = (v, max) => Number.isInteger(v) && v >= 0 && v <= max; + const pct = (done, total) => ((done / (total || 1)) * 100).toFixed(1); + const bar = (done, total) => { + const filled = Math.min(24, Math.max(0, Math.round((done / (total || 1)) * 24))); + return '█'.repeat(filled) + '░'.repeat(24 - filled); + }; + + const lines = []; + const locales = Array.isArray(data?.locales) ? data.locales.slice(0, 50) : []; + for (const loc of locales) { + if (typeof loc?.code !== 'string' || !CODE.test(loc.code)) continue; + if (!['full', 'beta'].includes(loc.status)) continue; + const rows = (Array.isArray(loc.datasets) ? loc.datasets.slice(0, 10) : []) + .filter((d) => DATASET_LABELS.includes(d?.name) && typeof d.file === 'boolean' + && int(d.done, 1e6) && int(d.total, 1e6) && d.done <= d.total); + if (rows.length === 0) continue; + const done = rows.reduce((sum, d) => sum + d.done, 0); + const total = rows.reduce((sum, d) => sum + d.total, 0); + lines.push(`${loc.code} [${loc.status}] ${bar(done, total)} ${pct(done, total)}% (${done}/${total})`); + for (const d of rows) { + const note = d.file ? '' : ' (no file yet)'; + lines.push(` ${d.name.padEnd(12)} ${pct(d.done, d.total).padStart(5)}% ${d.done}/${d.total}${note}`); + } + if (int(loc.sameAsEn, 1e6) && loc.sameAsEn > 0) { + lines.push(` identical to en: ${loc.sameAsEn} value(s) — worth a spot check`); + } + const nextUp = (Array.isArray(loc.nextUp) ? loc.nextUp.slice(0, 10) : []) + .filter((k) => typeof k === 'string' && KEY.test(k)); + if (nextUp.length > 0) lines.push(` next up: ${nextUp.join(', ')}`); + lines.push(''); + } + if (lines.length === 0) return; + const marker = ''; const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`; const body = [ @@ -70,7 +112,7 @@ jobs: '### 🌍 Translation status', '', '```text', - report, + lines.join('\n').trimEnd(), '```', `Untranslated keys fall back to English — partial PRs are welcome, and every extra key helps. Guide: [TRANSLATING.md](${repoUrl}/blob/dev/TRANSLATING.md).`, ].join('\n'); diff --git a/scripts/i18n-status.js b/scripts/i18n-status.js index b90760d56..c8d7312bf 100644 --- a/scripts/i18n-status.js +++ b/scripts/i18n-status.js @@ -54,11 +54,33 @@ const bar = (ratio, width = 24) => { }; const { values } = parseArgs({ - options: { locale: { type: 'string' }, limit: { type: 'string', default: '10' } }, + options: { + locale: { type: 'string' }, + limit: { type: 'string', default: '10' }, + json: { type: 'boolean', default: false }, + }, }); const limit = Math.max(0, Number.parseInt(values.limit, 10) || 0); const targets = LOCALES.filter((l) => l.code !== FALLBACK_LOCALE && (!values.locale || l.code === values.locale)); +// --json: machine-readable snapshot of the same numbers, for CI's PR-comment +// pipeline. Counts and key paths only, no prose — the consumer treats every +// field as untrusted input and validates it before rendering anything. +if (values.json) { + const locales = targets.map(({ code, status }) => { + const reports = DATASETS.map((dataset) => ({ ...dataset, ...compare(dataset.dir, code) })); + return { + code, + status, + sameAsEn: reports.reduce((sum, r) => sum + r.sameAsEn, 0), + datasets: reports.map((r) => ({ name: r.name, done: r.done, total: r.total, file: r.file })), + nextUp: reports.flatMap((r) => r.missing.map((key) => `${r.dir}${key}`)).slice(0, limit), + }; + }); + console.log(JSON.stringify({ locales })); + process.exit(0); +} + console.log(`\nTranslation status — reference locale: ${FALLBACK_LOCALE}\n`); if (targets.length === 0) { From 58bd271d8ba3342f3fba10d724d11dda719e03c5 Mon Sep 17 00:00:00 2001 From: jason5ng32 Date: Sun, 23 Aug 2026 13:26:28 +0800 Subject: [PATCH 15/15] Refactor(ci): drop the i18n PR comment bot MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Republishing PR-computed content under the bot identity needed ever more laundering (fences, then schema validation) and the residue was still reviewer-flagged — the data originates from code the PR controls, so no amount of validation closes that. The convenience isn't worth the privileged surface: the CI job summary already shows the same dashboard from inside the PR's own sandbox, so the sticky comment, its artifact hand-off and the --json mode all go. Co-Authored-By: Claude Fable 5 --- .github/workflows/ci.yml | 35 -------- .github/workflows/i18n-comment.yml | 132 ----------------------------- scripts/i18n-status.js | 24 +----- 3 files changed, 1 insertion(+), 190 deletions(-) delete mode 100644 .github/workflows/i18n-comment.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9226ce1b..6bda661a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,41 +49,6 @@ jobs: echo '```' } >> "$GITHUB_STEP_SUMMARY" - - name: Prepare translation report artifact - # Locale-touching PRs get a sticky status comment, posted by - # i18n-comment.yml (this job's token is read-only, and fork PRs never - # see secrets — so the report is computed here and handed over as an - # artifact). No locale changes → no artifact → no comment. - # - # The artifact carries STRUCTURED NUMBERS only (--json: counts, locale - # codes, key paths). This run executes the PR's own code, so nothing - # produced here reaches a comment as text — the privileged side - # validates every field against strict shapes and renders the comment - # entirely with its own trusted code. - if: github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ github.token }} - PR_NUMBER: ${{ github.event.pull_request.number }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - run: | - if ! gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/files" --paginate \ - --jq '.[].filename' \ - | grep -qE '^(frontend/locales/|common/locale-registry\.js|frontend/data/changelog\.json)'; then - exit 0 - fi - mkdir -p i18n-report - node scripts/i18n-status.js --json > i18n-report/report.json - printf '%s\n' "$PR_NUMBER" > i18n-report/pr-number - printf '%s\n' "$HEAD_SHA" > i18n-report/head-sha - - - name: Upload translation report - if: github.event_name == 'pull_request' - uses: actions/upload-artifact@v7 - with: - name: i18n-report - path: i18n-report/ - if-no-files-found: ignore - - name: Run tests run: pnpm test diff --git a/.github/workflows/i18n-comment.yml b/.github/workflows/i18n-comment.yml deleted file mode 100644 index a971d0f12..000000000 --- a/.github/workflows/i18n-comment.yml +++ /dev/null @@ -1,132 +0,0 @@ -# Posts / updates one sticky translation-status comment on locale PRs. -# -# Deliberately split from CI: the report is computed there, under the -# read-only token fork PRs get, and handed over as an artifact. This -# workflow runs only trusted code from the default branch and holds the -# pull-requests: write token — never check out or execute anything from -# the PR here. -# -# The artifact is untrusted input (a PR can change scripts/i18n-status.js -# to emit whatever it likes), so no artifact byte ever reaches the comment. -# It carries structured claims — counts, locale codes, key paths — and each -# field is validated against a strict shape (enums, integer bounds, tight -# regexes) before trusted code here renders the comment text from scratch. -# A hostile PR can at most misstate its own coverage numbers; it cannot put -# a single word of prose, a link, or an instruction into the bot's mouth. -# (The numbers are advisory anyway — the merge gate is pnpm test.) -name: i18n PR comment - -on: - workflow_run: - workflows: [CI] - types: [completed] - -permissions: - pull-requests: write - actions: read - -jobs: - comment: - # Only PR runs can carry the artifact; push runs are skipped outright. - if: github.event.workflow_run.event == 'pull_request' - runs-on: ubuntu-latest - - steps: - - name: Download translation report - id: download - # No artifact = the PR didn't touch locale data; treat as a clean skip. - continue-on-error: true - uses: actions/download-artifact@v8 - with: - name: i18n-report - path: i18n-report - run-id: ${{ github.event.workflow_run.id }} - github-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Upsert sticky comment - if: steps.download.outcome == 'success' - uses: actions/github-script@v9 - with: - script: | - const fs = require('fs'); - const read = (f) => fs.readFileSync(`i18n-report/${f}`, 'utf8').trim(); - - const prNumber = Number.parseInt(read('pr-number'), 10); - const headSha = read('head-sha'); - if (!Number.isInteger(prNumber) || prNumber <= 0) return; - - let data; - try { data = JSON.parse(read('report.json')); } catch { return; } - - // The artifact came from an untrusted run: only write to the PR - // whose head actually triggered that run. - const { data: pr } = await github.rest.pulls.get({ - ...context.repo, pull_number: prNumber, - }); - const runHead = context.payload.workflow_run.head_sha; - if (pr.head.sha !== runHead || headSha !== runHead) return; - - // Validate-then-render: every value below either matches a strict - // shape or its row is dropped. Only validated numbers, enum labels - // and regex-tight identifiers make it into the rendered text. - const CODE = /^[A-Za-z]{2,3}(-[A-Za-z0-9]{2,8})?$/; - const KEY = /^[\w./-]{1,120}$/; - const DATASET_LABELS = ['main pack', 'privacy copy', 'checklist']; - const int = (v, max) => Number.isInteger(v) && v >= 0 && v <= max; - const pct = (done, total) => ((done / (total || 1)) * 100).toFixed(1); - const bar = (done, total) => { - const filled = Math.min(24, Math.max(0, Math.round((done / (total || 1)) * 24))); - return '█'.repeat(filled) + '░'.repeat(24 - filled); - }; - - const lines = []; - const locales = Array.isArray(data?.locales) ? data.locales.slice(0, 50) : []; - for (const loc of locales) { - if (typeof loc?.code !== 'string' || !CODE.test(loc.code)) continue; - if (!['full', 'beta'].includes(loc.status)) continue; - const rows = (Array.isArray(loc.datasets) ? loc.datasets.slice(0, 10) : []) - .filter((d) => DATASET_LABELS.includes(d?.name) && typeof d.file === 'boolean' - && int(d.done, 1e6) && int(d.total, 1e6) && d.done <= d.total); - if (rows.length === 0) continue; - const done = rows.reduce((sum, d) => sum + d.done, 0); - const total = rows.reduce((sum, d) => sum + d.total, 0); - lines.push(`${loc.code} [${loc.status}] ${bar(done, total)} ${pct(done, total)}% (${done}/${total})`); - for (const d of rows) { - const note = d.file ? '' : ' (no file yet)'; - lines.push(` ${d.name.padEnd(12)} ${pct(d.done, d.total).padStart(5)}% ${d.done}/${d.total}${note}`); - } - if (int(loc.sameAsEn, 1e6) && loc.sameAsEn > 0) { - lines.push(` identical to en: ${loc.sameAsEn} value(s) — worth a spot check`); - } - const nextUp = (Array.isArray(loc.nextUp) ? loc.nextUp.slice(0, 10) : []) - .filter((k) => typeof k === 'string' && KEY.test(k)); - if (nextUp.length > 0) lines.push(` next up: ${nextUp.join(', ')}`); - lines.push(''); - } - if (lines.length === 0) return; - - const marker = ''; - const repoUrl = `https://github.com/${context.repo.owner}/${context.repo.repo}`; - const body = [ - marker, - '### 🌍 Translation status', - '', - '```text', - lines.join('\n').trimEnd(), - '```', - `Untranslated keys fall back to English — partial PRs are welcome, and every extra key helps. Guide: [TRANSLATING.md](${repoUrl}/blob/dev/TRANSLATING.md).`, - ].join('\n'); - - const comments = await github.paginate(github.rest.issues.listComments, { - ...context.repo, issue_number: prNumber, per_page: 100, - }); - const existing = comments.find((c) => c.body?.startsWith(marker)); - if (existing) { - await github.rest.issues.updateComment({ - ...context.repo, comment_id: existing.id, body, - }); - } else { - await github.rest.issues.createComment({ - ...context.repo, issue_number: prNumber, body, - }); - } diff --git a/scripts/i18n-status.js b/scripts/i18n-status.js index c8d7312bf..b90760d56 100644 --- a/scripts/i18n-status.js +++ b/scripts/i18n-status.js @@ -54,33 +54,11 @@ const bar = (ratio, width = 24) => { }; const { values } = parseArgs({ - options: { - locale: { type: 'string' }, - limit: { type: 'string', default: '10' }, - json: { type: 'boolean', default: false }, - }, + options: { locale: { type: 'string' }, limit: { type: 'string', default: '10' } }, }); const limit = Math.max(0, Number.parseInt(values.limit, 10) || 0); const targets = LOCALES.filter((l) => l.code !== FALLBACK_LOCALE && (!values.locale || l.code === values.locale)); -// --json: machine-readable snapshot of the same numbers, for CI's PR-comment -// pipeline. Counts and key paths only, no prose — the consumer treats every -// field as untrusted input and validates it before rendering anything. -if (values.json) { - const locales = targets.map(({ code, status }) => { - const reports = DATASETS.map((dataset) => ({ ...dataset, ...compare(dataset.dir, code) })); - return { - code, - status, - sameAsEn: reports.reduce((sum, r) => sum + r.sameAsEn, 0), - datasets: reports.map((r) => ({ name: r.name, done: r.done, total: r.total, file: r.file })), - nextUp: reports.flatMap((r) => r.missing.map((key) => `${r.dir}${key}`)).slice(0, limit), - }; - }); - console.log(JSON.stringify({ locales })); - process.exit(0); -} - console.log(`\nTranslation status — reference locale: ${FALLBACK_LOCALE}\n`); if (targets.length === 0) {