From 991fd289a29abf1ce1f3b591bf369fed516c27be Mon Sep 17 00:00:00 2001 From: NetworkCats Date: Sun, 22 Mar 2026 18:21:29 +0800 Subject: [PATCH] fix: improve accessibility to meet WCAG 2.2 AA/AAA standards - Remove disruptive alert() on error pages, rely on role="alert" banner - Add skip navigation link for keyboard users (WCAG 2.4.1) - Fix heading hierarchy on error page (sr-only status text in h1) - Add "opens in a new tab" screen reader hint on external links - Wrap language selector in form with noscript submit button (WCAG 3.2.2) - Add aria-label to copy-to-clipboard buttons and footer landmarks - Use h2 for alt-IP section to maintain single h1 per page - Add persistent underline on links for non-color identification (WCAG 1.4.1) - Fix footer attribution contrast ratio to ~5.5:1 (WCAG 1.4.3) - Add aria-busy/role=status loading announcements for skeleton state - Add translations for new accessibility strings across all 14 locales - Update integration tests to assert role="alert" instead of alert() --- locales/ar.po | 12 +++++++ locales/de.po | 12 +++++++ locales/en.po | 12 +++++++ locales/es.po | 12 +++++++ locales/fr.po | 12 +++++++ locales/id.po | 12 +++++++ locales/it.po | 12 +++++++ locales/ja.po | 12 +++++++ locales/ko.po | 12 +++++++ locales/messages.pot | 16 ++++++++++ locales/nl.po | 12 +++++++ locales/pt.po | 12 +++++++ locales/ru.po | 12 +++++++ locales/zh-Hans.po | 12 +++++++ locales/zh-Hant.po | 12 +++++++ static/script.js | 26 ++++++++++++--- static/style.css | 64 ++++++++++++++++++++++++++++++++----- templates/_footer_lang.html | 7 ++-- templates/base.html | 3 +- templates/error_alert.html | 6 ++-- templates/error_page.html | 4 +-- templates/index.html | 6 ++-- tests/integration.rs | 12 +++---- 23 files changed, 281 insertions(+), 31 deletions(-) diff --git a/locales/ar.po b/locales/ar.po index 2e836a4..7d7920f 100644 --- a/locales/ar.po +++ b/locales/ar.po @@ -191,3 +191,15 @@ msgstr "فشل عرض القالب" msgid "Copied" msgstr "تم النسخ" + +msgid "Go" +msgstr "انتقال" + +msgid "opens in a new tab" +msgstr "يفتح في علامة تبويب جديدة" + +msgid "Copy IP address to clipboard" +msgstr "نسخ عنوان IP إلى الحافظة" + +msgid "Site footer" +msgstr "تذييل الموقع" diff --git a/locales/de.po b/locales/de.po index 703374d..61571c4 100644 --- a/locales/de.po +++ b/locales/de.po @@ -191,3 +191,15 @@ msgstr "Vorlagen-Rendering fehlgeschlagen" msgid "Copied" msgstr "Kopiert" + +msgid "Go" +msgstr "Los" + +msgid "opens in a new tab" +msgstr "öffnet in neuem Tab" + +msgid "Copy IP address to clipboard" +msgstr "IP-Adresse in die Zwischenablage kopieren" + +msgid "Site footer" +msgstr "Seitenfuß" diff --git a/locales/en.po b/locales/en.po index b047929..2c38c5b 100644 --- a/locales/en.po +++ b/locales/en.po @@ -191,3 +191,15 @@ msgstr "Template render failed" msgid "Copied" msgstr "Copied" + +msgid "Go" +msgstr "Go" + +msgid "opens in a new tab" +msgstr "opens in a new tab" + +msgid "Copy IP address to clipboard" +msgstr "Copy IP address to clipboard" + +msgid "Site footer" +msgstr "Site footer" diff --git a/locales/es.po b/locales/es.po index d3196a7..a664db8 100644 --- a/locales/es.po +++ b/locales/es.po @@ -191,3 +191,15 @@ msgstr "La renderizacion de la plantilla fallo" msgid "Copied" msgstr "Copiado" + +msgid "Go" +msgstr "Ir" + +msgid "opens in a new tab" +msgstr "se abre en una pestaña nueva" + +msgid "Copy IP address to clipboard" +msgstr "Copiar dirección IP al portapapeles" + +msgid "Site footer" +msgstr "Pie de página" diff --git a/locales/fr.po b/locales/fr.po index db3e9e7..4adf2ad 100644 --- a/locales/fr.po +++ b/locales/fr.po @@ -191,3 +191,15 @@ msgstr "Echec du rendu du modele" msgid "Copied" msgstr "Copie" + +msgid "Go" +msgstr "Aller" + +msgid "opens in a new tab" +msgstr "s'ouvre dans un nouvel onglet" + +msgid "Copy IP address to clipboard" +msgstr "Copier l'adresse IP dans le presse-papiers" + +msgid "Site footer" +msgstr "Pied de page" diff --git a/locales/id.po b/locales/id.po index 17cb003..16a6bbb 100644 --- a/locales/id.po +++ b/locales/id.po @@ -191,3 +191,15 @@ msgstr "Render template gagal" msgid "Copied" msgstr "Disalin" + +msgid "Go" +msgstr "Buka" + +msgid "opens in a new tab" +msgstr "dibuka di tab baru" + +msgid "Copy IP address to clipboard" +msgstr "Salin alamat IP ke papan klip" + +msgid "Site footer" +msgstr "Footer situs" diff --git a/locales/it.po b/locales/it.po index c59e1f2..7a48355 100644 --- a/locales/it.po +++ b/locales/it.po @@ -191,3 +191,15 @@ msgstr "Rendering del template fallito" msgid "Copied" msgstr "Copiato" + +msgid "Go" +msgstr "Vai" + +msgid "opens in a new tab" +msgstr "si apre in una nuova scheda" + +msgid "Copy IP address to clipboard" +msgstr "Copia l'indirizzo IP negli appunti" + +msgid "Site footer" +msgstr "Piè di pagina" diff --git a/locales/ja.po b/locales/ja.po index 7b8d056..475e8dc 100644 --- a/locales/ja.po +++ b/locales/ja.po @@ -191,3 +191,15 @@ msgstr "テンプレートのレンダリングに失敗しました" msgid "Copied" msgstr "コピーしました" + +msgid "Go" +msgstr "移動" + +msgid "opens in a new tab" +msgstr "新しいタブで開きます" + +msgid "Copy IP address to clipboard" +msgstr "IPアドレスをクリップボードにコピー" + +msgid "Site footer" +msgstr "サイトフッター" diff --git a/locales/ko.po b/locales/ko.po index e0c58bf..f40c434 100644 --- a/locales/ko.po +++ b/locales/ko.po @@ -191,3 +191,15 @@ msgstr "템플릿 렌더링 실패" msgid "Copied" msgstr "복사됨" + +msgid "Go" +msgstr "이동" + +msgid "opens in a new tab" +msgstr "새 탭에서 열림" + +msgid "Copy IP address to clipboard" +msgstr "IP 주소를 클립보드에 복사" + +msgid "Site footer" +msgstr "사이트 바닥글" diff --git a/locales/messages.pot b/locales/messages.pot index 51819ac..56f45b2 100644 --- a/locales/messages.pot +++ b/locales/messages.pot @@ -221,3 +221,19 @@ msgstr "" #: templates/base.html - language selector label msgid "Language" msgstr "" + +#: templates/_footer_lang.html - language submit button (noscript) +msgid "Go" +msgstr "" + +#: templates/index.html - ASN link new tab hint +msgid "opens in a new tab" +msgstr "" + +#: templates/index.html - copy IP button label +msgid "Copy IP address to clipboard" +msgstr "" + +#: templates/index.html, error_alert.html - footer landmark label +msgid "Site footer" +msgstr "" diff --git a/locales/nl.po b/locales/nl.po index 766a009..690185d 100644 --- a/locales/nl.po +++ b/locales/nl.po @@ -191,3 +191,15 @@ msgstr "Sjabloonweergave mislukt" msgid "Copied" msgstr "Gekopieerd" + +msgid "Go" +msgstr "Ga" + +msgid "opens in a new tab" +msgstr "opent in een nieuw tabblad" + +msgid "Copy IP address to clipboard" +msgstr "IP-adres kopiëren naar klembord" + +msgid "Site footer" +msgstr "Sitefooter" diff --git a/locales/pt.po b/locales/pt.po index a4295fc..9e468ca 100644 --- a/locales/pt.po +++ b/locales/pt.po @@ -191,3 +191,15 @@ msgstr "Falha na renderização do template" msgid "Copied" msgstr "Copiado" + +msgid "Go" +msgstr "Ir" + +msgid "opens in a new tab" +msgstr "abre em uma nova aba" + +msgid "Copy IP address to clipboard" +msgstr "Copiar endereço IP para a área de transferência" + +msgid "Site footer" +msgstr "Rodapé do site" diff --git a/locales/ru.po b/locales/ru.po index 7849d0b..1e82438 100644 --- a/locales/ru.po +++ b/locales/ru.po @@ -191,3 +191,15 @@ msgstr "Ошибка рендеринга шаблона" msgid "Copied" msgstr "Скопировано" + +msgid "Go" +msgstr "Перейти" + +msgid "opens in a new tab" +msgstr "откроется в новой вкладке" + +msgid "Copy IP address to clipboard" +msgstr "Скопировать IP-адрес в буфер обмена" + +msgid "Site footer" +msgstr "Подвал сайта" diff --git a/locales/zh-Hans.po b/locales/zh-Hans.po index 28dbf16..f218cf4 100644 --- a/locales/zh-Hans.po +++ b/locales/zh-Hans.po @@ -191,3 +191,15 @@ msgstr "模板渲染失败" msgid "Copied" msgstr "已复制" + +msgid "Go" +msgstr "前往" + +msgid "opens in a new tab" +msgstr "在新标签页中打开" + +msgid "Copy IP address to clipboard" +msgstr "复制 IP 地址到剪贴板" + +msgid "Site footer" +msgstr "网站页脚" diff --git a/locales/zh-Hant.po b/locales/zh-Hant.po index 98870e5..88d9153 100644 --- a/locales/zh-Hant.po +++ b/locales/zh-Hant.po @@ -191,3 +191,15 @@ msgstr "範本渲染失敗" msgid "Copied" msgstr "已複製" + +msgid "Go" +msgstr "前往" + +msgid "opens in a new tab" +msgstr "在新分頁中開啟" + +msgid "Copy IP address to clipboard" +msgstr "複製 IP 位址到剪貼簿" + +msgid "Site footer" +msgstr "網站頁尾" diff --git a/static/script.js b/static/script.js index 2e7214f..ed77c89 100644 --- a/static/script.js +++ b/static/script.js @@ -177,6 +177,8 @@ function initCopyIp(wrap) { wrap.style.cursor = "pointer"; wrap.setAttribute("role", "button"); wrap.setAttribute("tabindex", "0"); + const copyLabel = wrap.getAttribute("data-copy-label") || "Copy IP address to clipboard"; + wrap.setAttribute("aria-label", copyLabel); const copyIp = () => { const ip = h1.textContent.trim(); @@ -282,10 +284,11 @@ function initAltIp(root, section) { const copyWrap = document.createElement("div"); copyWrap.className = "ip-copy-wrap"; copyWrap.setAttribute("data-copied", copiedText); - const h1 = document.createElement("h1"); - h1.className = "ip-display"; - h1.textContent = ip; - copyWrap.appendChild(h1); + copyWrap.setAttribute("data-copy-label", root.querySelector(".ip-copy-wrap")?.getAttribute("data-copy-label") || "Copy IP address to clipboard"); + const h2 = document.createElement("h2"); + h2.className = "ip-display"; + h2.textContent = ip; + copyWrap.appendChild(h2); const tooltip = document.createElement("span"); tooltip.className = "ip-copied-tooltip"; tooltip.setAttribute("aria-live", "polite"); @@ -314,6 +317,10 @@ function initAltIp(root, section) { a.target = "_blank"; a.rel = "noopener noreferrer"; a.textContent = `AS${asnNum}`; + const newTabHint = document.createElement("span"); + newTabHint.className = "sr-only"; + newTabHint.textContent = ` (opens in a new tab)`; + a.appendChild(newTabHint); td.appendChild(a); addRow(tbody, labelFor("asn"), td); } @@ -345,14 +352,24 @@ function initAltIp(root, section) { } function showSkeleton() { + section.setAttribute("aria-busy", "true"); + section.setAttribute("aria-label", "Loading additional IP information"); + section.setAttribute("role", "status"); section.innerHTML = buildSkeleton(); section.hidden = false; section.classList.add("alt-visible"); } + function clearLoadingState() { + section.removeAttribute("aria-busy"); + section.removeAttribute("aria-label"); + section.removeAttribute("role"); + } + function showAltIp(ip, info) { section.classList.remove("alt-visible"); setTimeout(() => { + clearLoadingState(); section.replaceChildren(buildBlock(ip, info)); initCopyIp(section.querySelector(".ip-copy-wrap")); scaleIpv6(section.querySelector(".ip-display")); @@ -372,6 +389,7 @@ function initAltIp(root, section) { function hideSkeleton() { section.classList.remove("alt-visible"); setTimeout(() => { + clearLoadingState(); section.replaceChildren(); section.hidden = true; }, 300); diff --git a/static/style.css b/static/style.css index ee6bd76..98c8fe5 100644 --- a/static/style.css +++ b/static/style.css @@ -81,6 +81,25 @@ main { border: 0; } +.skip-link { + position: absolute; + inset-inline-start: -9999px; + inset-block-start: 0; + z-index: 10000; + padding: 8px 16px; + font-size: 14px; + font-weight: 600; + color: #FFF; + background: var(--color-blue); + border-radius: 0 0 6px 6px; + text-decoration: none; + + &:focus { + inset-inline-start: 50%; + translate: -50% 0; + } +} + .search-form { display: flex; @@ -207,10 +226,12 @@ main { a { color: var(--color-link); - text-decoration: none; + text-decoration: underline; + text-decoration-color: light-dark(rgb(0 122 255 / 0.4), rgb(9 132 255 / 0.4)); + text-underline-offset: 2px; &:hover { - text-decoration: underline; + text-decoration-color: currentColor; } } } @@ -267,10 +288,12 @@ main { a { color: var(--color-link); - text-decoration: none; + text-decoration: underline; + text-decoration-color: light-dark(rgb(0 122 255 / 0.4), rgb(9 132 255 / 0.4)); + text-underline-offset: 2px; &:hover { - text-decoration: underline; + text-decoration-color: currentColor; } } @@ -406,10 +429,12 @@ main { a { color: var(--color-link); - text-decoration: none; + text-decoration: underline; + text-decoration-color: light-dark(rgb(0 122 255 / 0.4), rgb(9 132 255 / 0.4)); + text-underline-offset: 2px; &:hover { - text-decoration: underline; + text-decoration-color: currentColor; } } @@ -430,11 +455,11 @@ main { p { margin-block-end: 6px; font-size: 11px; - color: var(--color-gray2); + color: light-dark(#6E6E73, #9A9A9F); } a { - color: var(--color-gray2); + color: light-dark(#6E6E73, #9A9A9F); font-size: 11px; } @@ -449,6 +474,9 @@ main { .footer-lang { margin-block-start: 0; + display: flex; + gap: 6px; + align-items: center; } .lang-select { @@ -470,6 +498,26 @@ main { outline-offset: -1px; } } + + .lang-submit { + padding: 6px 12px; + border: 1px solid var(--color-separator); + border-radius: 6px; + font-family: inherit; + font-size: 13px; + color: var(--color-label-secondary); + background: var(--color-bg); + cursor: pointer; + + &:hover { + background: var(--color-bg-secondary); + } + + &:focus-visible { + outline: 2px solid var(--color-blue); + outline-offset: -1px; + } + } } [dir="rtl"] .lang-select { diff --git a/templates/_footer_lang.html b/templates/_footer_lang.html index 42cc4e1..8a40843 100644 --- a/templates/_footer_lang.html +++ b/templates/_footer_lang.html @@ -1,8 +1,9 @@ - \ No newline at end of file + + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index c6644f6..bbf83f8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -19,7 +19,8 @@ {% block head_extra %}{% endblock %} -
+ +
{% block content %}{% endblock %}
{% block footer %}{% endblock %} diff --git a/templates/error_alert.html b/templates/error_alert.html index 863c82b..9dbefdc 100644 --- a/templates/error_alert.html +++ b/templates/error_alert.html @@ -14,9 +14,7 @@ {% endblock %} -{% block head_extra %} - -{% endblock %} +{% block head_extra %}{% endblock %} {% block content %} {% let autofocus = true %} @@ -32,7 +30,7 @@ {% endblock %} {% block footer %} -