From ca54ee07c3b81522a07aff18023baff4452e8067 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Sat, 23 May 2026 07:41:59 +0000 Subject: [PATCH 1/2] Add hero title, social preview metadata, offline i18n, and Node 24 actions opt-in * index.html: new hero title "Offline MAC Address Lookup", description, canonical, robots, color-scheme, Open Graph + Twitter card tags pointing at a new 1200x630 share image so X/Twitter, Facebook, iMessage, WhatsApp, WeChat, Signal, Telegram, LinkedIn, Messenger render rich previews when the URL is pasted. * web/icons/og-image.{svg,png}: lightweight, self-contained share image (no external dependency, generated PNG from local SVG). * manifest.webmanifest: updated name/description, added lang/dir/categories so the PWA install card and search engines see the new branding. * web/i18n.js: static, offline, dependency-free i18n. Auto-detects from navigator.languages/region (incl. zh-CN/SG -> Simplified, zh-HK/TW + yue -> Traditional, fil/tl -> Filipino), exposes a manual selector in the header, and persists via localStorage with a URL-hash fallback when storage is unavailable. Updates and (RTL for Arabic). Covers en, es, fr, de, it, pt, zh-Hans, zh-Hant, ja, ko, hi, fil, ar. IEEE vendor data stays untranslated. * app.js: status and empty-state messages now route through i18n.t() with inline English fallbacks so existing behavior is preserved when i18n has not loaded yet (and the fresh-load test continues to pass). * styles.css: language-selector chrome, RTL-friendly mirror. * sw.js: precaches i18n.js; bumped shell cache version so existing clients pick it up on next activation. * workflows: set FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true on ci.yml, pages.yml, and update-oui.yml so the official actions stop emitting the Node 20 deprecation warning. setup-node bumped to Node 22 for the test job. No action major versions changed. * tests/web/i18n_smoke.mjs: new smoke test (locale normalization, key coverage across every locale, RTL dir switch, interpolation) wired into the CI workflow. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/ci.yml | 11 +- .github/workflows/pages.yml | 5 + .github/workflows/update-oui.yml | 6 + tests/web/i18n_smoke.mjs | 146 ++++++ web/app.js | 90 +++- web/i18n.js | 798 +++++++++++++++++++++++++++++++ web/icons/og-image.png | Bin 0 -> 70259 bytes web/icons/og-image.svg | 26 + web/index.html | 67 ++- web/manifest.webmanifest | 7 +- web/styles.css | 24 + web/sw.js | 3 +- 12 files changed, 1139 insertions(+), 44 deletions(-) create mode 100644 tests/web/i18n_smoke.mjs create mode 100644 web/i18n.js create mode 100644 web/icons/og-image.png create mode 100644 web/icons/og-image.svg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 64eae7a..023da40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,12 @@ on: permissions: contents: read +# Opt the official JavaScript actions (checkout, setup-python, setup-node, …) +# into the Node 24 runtime so we don't keep seeing the Node 20 deprecation +# warning. Safe to remove once GitHub flips the default. +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: test: runs-on: ubuntu-latest @@ -18,12 +24,15 @@ jobs: python-version: "3.x" - uses: actions/setup-node@v4 with: - node-version: "20" + node-version: "22" - name: JS syntax check run: | node --check web/app.js node --check web/sw.js + node --check web/i18n.js - name: PWA fresh-load smoke test run: node tests/web/fresh_load.mjs + - name: PWA i18n smoke test + run: node tests/web/i18n_smoke.mjs - name: Python tests run: python3 -m unittest discover -s tests -v diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 311d39e..f8e5cab 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -25,6 +25,11 @@ permissions: pages: write id-token: write +# Opt the official JavaScript actions into the Node 24 runtime to silence the +# Node 20 deprecation warning. Drop this env block once Node 24 is the default. +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + concurrency: group: pages cancel-in-progress: false diff --git a/.github/workflows/update-oui.yml b/.github/workflows/update-oui.yml index 8e1c898..fa1cd01 100644 --- a/.github/workflows/update-oui.yml +++ b/.github/workflows/update-oui.yml @@ -20,6 +20,12 @@ permissions: contents: write pull-requests: write +# Opt the official JavaScript actions into the Node 24 runtime so we don't get +# the Node 20 deprecation warning in scheduled runs. Drop this env block once +# Node 24 becomes the default. +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: update: runs-on: ubuntu-latest diff --git a/tests/web/i18n_smoke.mjs b/tests/web/i18n_smoke.mjs new file mode 100644 index 0000000..2df6d99 --- /dev/null +++ b/tests/web/i18n_smoke.mjs @@ -0,0 +1,146 @@ +/* i18n smoke test for web/i18n.js + * + * Confirms: + * - All non-English locales define every key that English defines (no + * accidental missing-key gaps that would silently fall back to English). + * - normalizeLocale() maps representative real-world tags correctly: + * en-GB → en, fr-CA → fr, zh-CN/zh-SG → zh-Hans, + * zh-HK/zh-TW/yue-HK → zh-Hant, fil-PH/tl-PH → fil, + * unknown → null + * - Auto-detect picks the first supported tag from navigator.languages. + * + * Run: node tests/web/i18n_smoke.mjs + */ +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; +import { dirname, resolve } from 'path'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const I18N_JS = resolve(__dirname, '..', '..', 'web/i18n.js'); + +function assert(cond, msg) { + if (!cond) { + console.error('FAIL:', msg); + process.exit(1); + } +} + +function makeDom() { + const els = {}; + const make = () => ({ + _txt: '', _cls: '', value: '', disabled: false, + innerHTML: '', + setAttribute() {}, getAttribute() { return null; }, + appendChild() {}, addEventListener() {}, + querySelectorAll: () => [], + }); + return { + documentElement: { lang: 'en', dir: 'ltr' }, + readyState: 'complete', + getElementById: () => null, + addEventListener: () => {}, + querySelectorAll: () => [], + createElement: () => make(), + }; +} + +async function loadI18n({ languages = ['en'] } = {}) { + globalThis.document = makeDom(); + globalThis.window = { + addEventListener() {}, + dispatchEvent() {}, + }; + globalThis.location = { href: 'http://localhost/', hash: '', search: '' }; + globalThis.history = { replaceState() {} }; + globalThis.navigator = { languages, language: languages[0] }; + globalThis.localStorage = undefined; + globalThis.URL = URL; + globalThis.URLSearchParams = URLSearchParams; + globalThis.CustomEvent = class CustomEvent { constructor(n, o) { this.name = n; this.detail = o && o.detail; } }; + + const src = readFileSync(I18N_JS, 'utf8') + `\n//# salt=${Math.random()}\n`; + await import('data:text/javascript;base64,' + Buffer.from(src).toString('base64')); + return globalThis.window.i18n; +} + +async function main() { + const i18n = await loadI18n(); + assert(i18n, 'window.i18n must be exposed'); + + // ---- normalizeLocale ---- + const cases = [ + ['en-GB', 'en'], + ['en-US', 'en'], + ['fr-CA', 'fr'], + ['fr', 'fr'], + ['es-419', 'es'], + ['pt-BR', 'pt'], + ['de-AT', 'de'], + ['it-CH', 'it'], + ['ja-JP', 'ja'], + ['ko-KR', 'ko'], + ['hi-IN', 'hi'], + ['zh', 'zh-Hans'], + ['zh-CN', 'zh-Hans'], + ['zh-SG', 'zh-Hans'], + ['zh-Hans', 'zh-Hans'], + ['zh-HK', 'zh-Hant'], + ['zh-TW', 'zh-Hant'], + ['zh-Hant', 'zh-Hant'], + ['yue', 'zh-Hant'], + ['yue-HK', 'zh-Hant'], + ['fil', 'fil'], + ['fil-PH', 'fil'], + ['tl', 'fil'], + ['tl-PH', 'fil'], + ['ar', 'ar'], + ['ar-SA', 'ar'], + ['xx-YY', null], + ['', null], + ]; + for (const [tag, want] of cases) { + const got = i18n.normalizeLocale(tag); + assert(got === want, `normalizeLocale(${JSON.stringify(tag)}) → ${got}, want ${want}`); + } + + // ---- key coverage ---- + const L = i18n._LOCALES; + const enKeys = Object.keys(L.en).filter((k) => k[0] !== '_'); + for (const code of i18n.supported()) { + if (code === 'en') continue; + const missing = enKeys.filter((k) => !(k in L[code])); + assert(missing.length === 0, `locale ${code} missing keys: ${missing.join(', ')}`); + assert(L[code]._name, `locale ${code} missing _name`); + assert(L[code]._dir === 'ltr' || L[code]._dir === 'rtl', + `locale ${code} bad _dir: ${L[code]._dir}`); + } + + // ---- supported list contains the requested set ---- + const want = ['en', 'es', 'fr', 'de', 'it', 'pt', + 'zh-Hans', 'zh-Hant', 'ja', 'ko', 'hi', 'fil', 'ar']; + for (const code of want) { + assert(i18n.supported().indexOf(code) >= 0, `expected locale ${code} to be supported`); + } + + // ---- t() interpolates positional args ---- + i18n.setLocale('en'); + const s = i18n.t('refreshed', 1234); + assert(s.includes('1234'), `expected interpolation, got: ${s}`); + + // ---- t() falls back to English on missing key in non-English locale ---- + i18n.setLocale('fr'); + const fr = i18n.t('hero_title'); + assert(fr && fr !== 'hero_title' && fr !== 'Offline MAC Address Lookup', + `expected French hero_title, got: ${fr}`); + + // ---- direction switches on Arabic ---- + i18n.setLocale('ar'); + assert(document.documentElement.dir === 'rtl', 'expected dir=rtl for ar'); + assert(document.documentElement.lang === 'ar', 'expected lang=ar for ar'); + i18n.setLocale('en'); + assert(document.documentElement.dir === 'ltr', 'expected dir=ltr for en'); + + console.log('OK — i18n smoke test passes'); +} + +main().catch((e) => { console.error('test threw:', e); process.exit(1); }); diff --git a/web/app.js b/web/app.js index 1ca97fe..b984280 100644 --- a/web/app.js +++ b/web/app.js @@ -574,15 +574,42 @@ function subsequenceMatch(haystack, needle) { // ----------------------- UI rendering ----------------------- +// Translate via i18n module if available, otherwise return the fallback as-is. +// Keeping a thin local wrapper means status calls don't have to check for +// window.i18n every time. +function tr(key, fallback, ...args) { + try { + if (typeof window !== 'undefined' && window.i18n && typeof window.i18n.t === 'function') { + const out = window.i18n.t(key, ...args); + if (out && out !== key) return out; + } + } catch (_) { /* ignore */ } + // Local fallback formatting matches i18n.js {0}, {1}… + let s = fallback; + for (let i = 0; i < args.length; i++) { + s = s.split('{' + i + '}').join(String(args[i])); + } + return s; +} + function setStatus(text, cls = '') { els.statusText.textContent = text; els.statusText.className = cls; + // Remember the last status so we can re-render on language change. + state.lastStatus = { text, cls }; +} + +// For status calls that should be re-translatable when the user switches +// languages, pass a render function instead of pre-computed text. +function setStatusRender(fn, cls = '') { + state.lastStatusRender = { fn, cls }; + setStatus(fn(), cls); } function statusSuffix() { // Append a "no persistence" badge to status messages when running in a // degraded storage mode, so the user knows refreshing won't stick. - if (!state.persistent) return ' · in-memory only'; + if (!state.persistent) return ' · ' + tr('in_memory_only', 'in-memory only'); return ''; } @@ -598,7 +625,7 @@ function renderEmpty(msg) { function renderResults(entries, { exact = false } = {}) { els.results.innerHTML = ''; if (entries.length === 0) { - renderEmpty('No matches.'); + renderEmpty(tr('no_matches', 'No matches.')); return; } const frag = document.createDocumentFragment(); @@ -642,7 +669,7 @@ function buildCard(entry, exact) { function handleQuery(value) { if (!state.loaded) { - renderEmpty('Registry still loading…'); + renderEmpty(tr('registry_loading', 'Registry still loading…')); return; } const trimmed = value.trim(); @@ -657,7 +684,7 @@ function handleQuery(value) { if (hit) { renderResults([hit], { exact: true }); } else { - renderEmpty(`No registry entry for prefix ${hex.slice(0, 9)}.`); + renderEmpty(tr('no_prefix', 'No registry entry for prefix {0}.', hex.slice(0, 9))); } return; } @@ -684,7 +711,7 @@ els.refresh.addEventListener('click', async () => { try { activeRefresh.cancel(); } catch (_) {} } if (probablyOffline()) { - setStatus('Offline — cannot refresh.', 'warn'); + setStatus(tr('offline_cannot_refresh', 'Offline — cannot refresh.'), 'warn'); return; } els.refresh.disabled = true; @@ -695,23 +722,23 @@ els.refresh.addEventListener('click', async () => { }; activeRefresh = job; - setStatus('Checking for updates…'); + setStatus(tr('checking_updates', 'Checking for updates…')); try { const result = await syncWithRemote({ force: true, signal: ctrl && ctrl.signal }); if (activeRefresh !== job) return; // superseded if (result === 'updated') { - setStatus(`Refreshed — ${totalCount()} entries.${statusSuffix()}`, 'ok'); + setStatus(tr('refreshed', 'Refreshed — {0} entries.', totalCount()) + statusSuffix(), 'ok'); } else if (result === 'fresh') { - setStatus(`Already up to date — ${totalCount()} entries.${statusSuffix()}`, 'ok'); + setStatus(tr('already_up_to_date', 'Already up to date — {0} entries.', totalCount()) + statusSuffix(), 'ok'); } else if (result === 'cancelled') { - setStatus('Refresh cancelled.', 'warn'); + setStatus(tr('refresh_cancelled', 'Refresh cancelled.'), 'warn'); } else { // 'failed' or 'offline' — never clobber existing data. - setStatus(`Refresh failed — keeping cached data.${statusSuffix()}`, 'warn'); + setStatus(tr('refresh_failed_keeping', 'Refresh failed — keeping cached data.') + statusSuffix(), 'warn'); } if (els.query.value) handleQuery(els.query.value); } catch (e) { - setStatus(`Refresh failed: ${(e && e.message) || e}`, 'err'); + setStatus(tr('refresh_failed', 'Refresh failed: {0}', (e && e.message) || e), 'err'); } finally { if (activeRefresh === job) activeRefresh = null; els.refresh.disabled = false; @@ -720,21 +747,32 @@ els.refresh.addEventListener('click', async () => { window.addEventListener('online', () => { if (!state.loaded) return; - setStatus(`${totalCount()} entries · online${statusSuffix()}`, 'ok'); + setStatus(tr('entries_online', '{0} entries · online', totalCount()) + statusSuffix(), 'ok'); // Opportunistic background sync when connectivity returns. We don't await // here — the page stays responsive and any failure is silent. syncWithRemote().then((result) => { if (result === 'updated') { - setStatus(`Updated to latest — ${totalCount()} entries.${statusSuffix()}`, 'ok'); + setStatus(tr('updated_latest', 'Updated to latest — {0} entries.', totalCount()) + statusSuffix(), 'ok'); if (els.query.value) handleQuery(els.query.value); } }).catch(() => { /* keep cached data */ }); }); window.addEventListener('offline', () => { - if (state.loaded) setStatus(`${totalCount()} entries · offline${statusSuffix()}`, 'warn'); + if (state.loaded) setStatus(tr('entries_offline', '{0} entries · offline', totalCount()) + statusSuffix(), 'warn'); }); +// Re-render the last status message when the user switches languages so the +// visible text updates immediately. We re-evaluate translation rather than +// keep raw English around. +try { + window.addEventListener('i18n:changed', () => { + if (state.lastStatusRender && typeof state.lastStatusRender.fn === 'function') { + setStatus(state.lastStatusRender.fn(), state.lastStatusRender.cls); + } + }); +} catch (_) {} + // Catch otherwise-unhandled promise rejections so they don't dump raw stack // traces but DO show up in the console at warn level — we want to hear about // these in dev/QA, not silently swallow them. @@ -764,7 +802,7 @@ async function boot() { if (cached && payloadIsValid(cached)) { try { await applyPayload(cached); - setStatus(`Loaded ${totalCount()} entries from cache.${statusSuffix()}`, 'ok'); + setStatus(tr('loaded_from_cache', 'Loaded {0} entries from cache.', totalCount()) + statusSuffix(), 'ok'); if (els.query.value) handleQuery(els.query.value); } catch (e) { // Corrupt cache that passed validation but failed during parse — wipe @@ -778,7 +816,7 @@ async function boot() { // the content_hash differs from what we have locally. Failures here are // non-fatal — we keep whatever we just loaded from cache. if (!probablyOffline()) { - if (!state.loaded) setStatus('Downloading registry…'); + if (!state.loaded) setStatus(tr('downloading', 'Downloading registry…')); let result; try { result = await syncWithRemote(); @@ -787,23 +825,23 @@ async function boot() { } if (result === 'updated') { - setStatus(`Loaded ${totalCount()} entries (latest).${statusSuffix()}`, 'ok'); + setStatus(tr('loaded_latest', 'Loaded {0} entries (latest).', totalCount()) + statusSuffix(), 'ok'); } else if (result === 'failed' && !state.loaded) { const reason = state.lastSyncError ? ` (${state.lastSyncError})` : ''; - setStatus(`Could not load registry data${reason}.`, 'err'); - renderEmpty(`Could not load registry data${reason}. Connect to the network and reload.`); + setStatus(tr('could_not_load', 'Could not load registry data{0}.', reason), 'err'); + renderEmpty(tr('could_not_load_reload', 'Could not load registry data{0}. Connect to the network and reload.', reason)); els.refresh.disabled = false; // let the user retry return; } else if (result === 'fresh' && state.loaded) { - setStatus(`${totalCount()} entries · up to date${statusSuffix()}`, 'ok'); + setStatus(tr('up_to_date', '{0} entries · up to date', totalCount()) + statusSuffix(), 'ok'); } else if (result === 'failed' && state.loaded) { // Stale-but-usable: we have a cached copy, but the live check failed. - setStatus(`${totalCount()} entries · using cached data${statusSuffix()}`, 'warn'); + setStatus(tr('using_cached', '{0} entries · using cached data', totalCount()) + statusSuffix(), 'warn'); } if (els.query.value) handleQuery(els.query.value); } else if (!state.loaded) { - setStatus('Offline and no cached data.', 'err'); - renderEmpty('Offline and no cached data. Connect to the network and reload.'); + setStatus(tr('offline_no_cache', 'Offline and no cached data.'), 'err'); + renderEmpty(tr('offline_no_cache_reload', 'Offline and no cached data. Connect to the network and reload.')); els.refresh.disabled = false; // user might come back online } } @@ -814,12 +852,12 @@ try { boot().catch((e) => { try { console.info('[maclookup] boot failed:', e && e.message || e); } catch (_) {} if (!state.loaded) { - setStatus('Failed to start up.', 'err'); - renderEmpty('Failed to load. Try refreshing the page.'); + setStatus(tr('failed_startup', 'Failed to start up.'), 'err'); + renderEmpty(tr('failed_load_refresh', 'Failed to load. Try refreshing the page.')); els.refresh.disabled = false; } }); } catch (e) { try { console.info('[maclookup] boot threw:', e && e.message || e); } catch (_) {} - setStatus('Failed to start up.', 'err'); + setStatus(tr('failed_startup', 'Failed to start up.'), 'err'); } diff --git a/web/i18n.js b/web/i18n.js new file mode 100644 index 0000000..f959835 --- /dev/null +++ b/web/i18n.js @@ -0,0 +1,798 @@ +/* Static, offline-first i18n for the MAC Vendor Lookup PWA. + * + * Design constraints (per project guidance): + * - No runtime translation service. All strings are bundled. + * - Only translates UI chrome; IEEE vendor/address data stays as-is. + * - Auto-detects from navigator.languages / navigator.language, plus an + * explicit user selector. Falls back to English when a locale isn't + * supported or detection fails. + * - Persistence is best-effort: tries localStorage (cheap, fine here since + * the project already uses IndexedDB), then falls back to a `?lang=` + * URL hash so a chosen language survives a reload even in private mode. + * - Updates and when the language changes, so + * screen readers, search engines, and CSS :lang() selectors all behave. + * - Exposes window.i18n.t(key) for app.js so dynamic status messages can + * be translated. + * + * Adding a language: append to LOCALES with full key coverage. Missing keys + * fall back to English automatically. + */ + +const LOCALES = { + en: { + _name: 'English', + _dir: 'ltr', + hero_title: 'Offline MAC Address Lookup', + tagline_prefix: 'Offline lookup across IEEE', + tagline_suffix: 'registries.', + and: 'and', + lookup_heading: 'Lookup', + input_label: 'MAC address, prefix, or vendor name', + input_placeholder: '00:1A:7D… or cisco', + input_hint: 'Hex input does a longest-prefix lookup (MA-S → MA-M → MA-L). Text input does fuzzy vendor search.', + noscript: 'This app requires JavaScript to do the offline lookup.', + status_loading: 'Loading registry…', + refresh_button: 'Refresh data', + refresh_title: 'Fetch the latest IEEE registries (online only)', + refresh_aria: 'Refresh registry data', + data_version_label: 'Data version:', + source_link: 'source', + language_label: 'Language', + no_matches: 'No matches.', + registry_loading: 'Registry still loading…', + no_prefix: 'No registry entry for prefix {0}.', + loaded_from_cache: 'Loaded {0} entries from cache.', + loaded_latest: 'Loaded {0} entries (latest).', + up_to_date: '{0} entries · up to date', + using_cached: '{0} entries · using cached data', + could_not_load: 'Could not load registry data{0}.', + could_not_load_reload: 'Could not load registry data{0}. Connect to the network and reload.', + offline_no_cache: 'Offline and no cached data.', + offline_no_cache_reload: 'Offline and no cached data. Connect to the network and reload.', + offline_cannot_refresh: 'Offline — cannot refresh.', + checking_updates: 'Checking for updates…', + downloading: 'Downloading registry…', + refreshed: 'Refreshed — {0} entries.', + already_up_to_date: 'Already up to date — {0} entries.', + refresh_cancelled: 'Refresh cancelled.', + refresh_failed_keeping: 'Refresh failed — keeping cached data.', + refresh_failed: 'Refresh failed: {0}', + entries_online: '{0} entries · online', + entries_offline: '{0} entries · offline', + updated_latest: 'Updated to latest — {0} entries.', + in_memory_only: 'in-memory only', + failed_startup: 'Failed to start up.', + failed_load_refresh: 'Failed to load. Try refreshing the page.', + }, + es: { + _name: 'Español', + _dir: 'ltr', + hero_title: 'Búsqueda offline de direcciones MAC', + tagline_prefix: 'Búsqueda sin conexión en los registros IEEE', + tagline_suffix: '.', + and: 'y', + lookup_heading: 'Búsqueda', + input_label: 'Dirección MAC, prefijo o nombre del fabricante', + input_placeholder: '00:1A:7D… o cisco', + input_hint: 'Entrada hex: búsqueda por prefijo más largo (MA-S → MA-M → MA-L). Texto: búsqueda difusa del fabricante.', + noscript: 'Esta app requiere JavaScript para la búsqueda offline.', + status_loading: 'Cargando registro…', + refresh_button: 'Actualizar datos', + refresh_title: 'Obtener los registros IEEE más recientes (solo online)', + refresh_aria: 'Actualizar datos del registro', + data_version_label: 'Versión de los datos:', + source_link: 'código fuente', + language_label: 'Idioma', + no_matches: 'Sin coincidencias.', + registry_loading: 'Cargando registro…', + no_prefix: 'Sin entrada para el prefijo {0}.', + loaded_from_cache: '{0} entradas cargadas desde la caché.', + loaded_latest: '{0} entradas cargadas (más recientes).', + up_to_date: '{0} entradas · actualizado', + using_cached: '{0} entradas · usando caché', + could_not_load: 'No se pudieron cargar los datos{0}.', + could_not_load_reload: 'No se pudieron cargar los datos{0}. Conéctate a la red y recarga.', + offline_no_cache: 'Sin conexión y sin datos en caché.', + offline_no_cache_reload: 'Sin conexión y sin datos en caché. Conéctate y recarga.', + offline_cannot_refresh: 'Sin conexión — no se puede actualizar.', + checking_updates: 'Buscando actualizaciones…', + downloading: 'Descargando registro…', + refreshed: 'Actualizado — {0} entradas.', + already_up_to_date: 'Ya está actualizado — {0} entradas.', + refresh_cancelled: 'Actualización cancelada.', + refresh_failed_keeping: 'Actualización fallida — manteniendo datos en caché.', + refresh_failed: 'Actualización fallida: {0}', + entries_online: '{0} entradas · en línea', + entries_offline: '{0} entradas · sin conexión', + updated_latest: 'Actualizado a la última — {0} entradas.', + in_memory_only: 'solo en memoria', + failed_startup: 'Error al iniciar.', + failed_load_refresh: 'Error al cargar. Intenta recargar la página.', + }, + fr: { + _name: 'Français', + _dir: 'ltr', + hero_title: 'Recherche MAC hors ligne', + tagline_prefix: 'Recherche hors ligne dans les registres IEEE', + tagline_suffix: '.', + and: 'et', + lookup_heading: 'Recherche', + input_label: 'Adresse MAC, préfixe ou nom du fabricant', + input_placeholder: '00:1A:7D… ou cisco', + input_hint: 'Entrée hex : préfixe le plus long (MA-S → MA-M → MA-L). Texte : recherche floue du fabricant.', + noscript: 'Cette application nécessite JavaScript pour la recherche hors ligne.', + status_loading: 'Chargement du registre…', + refresh_button: 'Actualiser', + refresh_title: 'Récupérer les derniers registres IEEE (en ligne uniquement)', + refresh_aria: 'Actualiser les données du registre', + data_version_label: 'Version des données :', + source_link: 'source', + language_label: 'Langue', + no_matches: 'Aucune correspondance.', + registry_loading: 'Chargement du registre…', + no_prefix: 'Aucune entrée pour le préfixe {0}.', + loaded_from_cache: '{0} entrées chargées depuis le cache.', + loaded_latest: '{0} entrées chargées (à jour).', + up_to_date: '{0} entrées · à jour', + using_cached: '{0} entrées · cache utilisé', + could_not_load: 'Impossible de charger les données{0}.', + could_not_load_reload: 'Impossible de charger les données{0}. Connectez-vous au réseau et rechargez.', + offline_no_cache: 'Hors ligne et sans cache.', + offline_no_cache_reload: 'Hors ligne et sans cache. Connectez-vous et rechargez.', + offline_cannot_refresh: 'Hors ligne — actualisation impossible.', + checking_updates: 'Vérification des mises à jour…', + downloading: 'Téléchargement du registre…', + refreshed: 'Actualisé — {0} entrées.', + already_up_to_date: 'Déjà à jour — {0} entrées.', + refresh_cancelled: 'Actualisation annulée.', + refresh_failed_keeping: 'Échec de l\'actualisation — conservation du cache.', + refresh_failed: 'Échec de l\'actualisation : {0}', + entries_online: '{0} entrées · en ligne', + entries_offline: '{0} entrées · hors ligne', + updated_latest: 'Mise à jour vers la dernière version — {0} entrées.', + in_memory_only: 'mémoire uniquement', + failed_startup: 'Échec du démarrage.', + failed_load_refresh: 'Échec du chargement. Essayez de recharger la page.', + }, + de: { + _name: 'Deutsch', + _dir: 'ltr', + hero_title: 'Offline-MAC-Adresssuche', + tagline_prefix: 'Offline-Suche in den IEEE-Registern', + tagline_suffix: '.', + and: 'und', + lookup_heading: 'Suche', + input_label: 'MAC-Adresse, Präfix oder Herstellername', + input_placeholder: '00:1A:7D… oder cisco', + input_hint: 'Hex-Eingabe: längste Präfix-Suche (MA-S → MA-M → MA-L). Text: Fuzzy-Herstellersuche.', + noscript: 'Diese App benötigt JavaScript für die Offline-Suche.', + status_loading: 'Register wird geladen…', + refresh_button: 'Daten aktualisieren', + refresh_title: 'Neueste IEEE-Register abrufen (nur online)', + refresh_aria: 'Registerdaten aktualisieren', + data_version_label: 'Datenversion:', + source_link: 'Quellcode', + language_label: 'Sprache', + no_matches: 'Keine Treffer.', + registry_loading: 'Register wird geladen…', + no_prefix: 'Kein Eintrag für Präfix {0}.', + loaded_from_cache: '{0} Einträge aus dem Cache geladen.', + loaded_latest: '{0} Einträge geladen (aktuell).', + up_to_date: '{0} Einträge · aktuell', + using_cached: '{0} Einträge · Cache wird verwendet', + could_not_load: 'Daten konnten nicht geladen werden{0}.', + could_not_load_reload: 'Daten konnten nicht geladen werden{0}. Bitte verbinden und neu laden.', + offline_no_cache: 'Offline und keine Cache-Daten.', + offline_no_cache_reload: 'Offline und keine Cache-Daten. Bitte verbinden und neu laden.', + offline_cannot_refresh: 'Offline — Aktualisierung nicht möglich.', + checking_updates: 'Suche nach Updates…', + downloading: 'Register wird heruntergeladen…', + refreshed: 'Aktualisiert — {0} Einträge.', + already_up_to_date: 'Bereits aktuell — {0} Einträge.', + refresh_cancelled: 'Aktualisierung abgebrochen.', + refresh_failed_keeping: 'Aktualisierung fehlgeschlagen — Cache wird beibehalten.', + refresh_failed: 'Aktualisierung fehlgeschlagen: {0}', + entries_online: '{0} Einträge · online', + entries_offline: '{0} Einträge · offline', + updated_latest: 'Auf neueste Version aktualisiert — {0} Einträge.', + in_memory_only: 'nur im Speicher', + failed_startup: 'Start fehlgeschlagen.', + failed_load_refresh: 'Laden fehlgeschlagen. Bitte Seite neu laden.', + }, + it: { + _name: 'Italiano', + _dir: 'ltr', + hero_title: 'Ricerca MAC offline', + tagline_prefix: 'Ricerca offline nei registri IEEE', + tagline_suffix: '.', + and: 'e', + lookup_heading: 'Ricerca', + input_label: 'Indirizzo MAC, prefisso o nome del produttore', + input_placeholder: '00:1A:7D… o cisco', + input_hint: 'Hex: ricerca per prefisso più lungo (MA-S → MA-M → MA-L). Testo: ricerca fuzzy del produttore.', + noscript: 'Questa app richiede JavaScript per la ricerca offline.', + status_loading: 'Caricamento del registro…', + refresh_button: 'Aggiorna dati', + refresh_title: 'Scarica gli ultimi registri IEEE (solo online)', + refresh_aria: 'Aggiorna i dati del registro', + data_version_label: 'Versione dati:', + source_link: 'codice', + language_label: 'Lingua', + no_matches: 'Nessun risultato.', + registry_loading: 'Caricamento del registro…', + no_prefix: 'Nessuna voce per il prefisso {0}.', + loaded_from_cache: '{0} voci caricate dalla cache.', + loaded_latest: '{0} voci caricate (aggiornate).', + up_to_date: '{0} voci · aggiornato', + using_cached: '{0} voci · uso cache', + could_not_load: 'Impossibile caricare i dati{0}.', + could_not_load_reload: 'Impossibile caricare i dati{0}. Connettiti e ricarica.', + offline_no_cache: 'Offline e senza cache.', + offline_no_cache_reload: 'Offline e senza cache. Connettiti e ricarica.', + offline_cannot_refresh: 'Offline — aggiornamento non possibile.', + checking_updates: 'Ricerca aggiornamenti…', + downloading: 'Download del registro…', + refreshed: 'Aggiornato — {0} voci.', + already_up_to_date: 'Già aggiornato — {0} voci.', + refresh_cancelled: 'Aggiornamento annullato.', + refresh_failed_keeping: 'Aggiornamento fallito — mantengo la cache.', + refresh_failed: 'Aggiornamento fallito: {0}', + entries_online: '{0} voci · online', + entries_offline: '{0} voci · offline', + updated_latest: 'Aggiornato all\'ultima versione — {0} voci.', + in_memory_only: 'solo in memoria', + failed_startup: 'Avvio non riuscito.', + failed_load_refresh: 'Caricamento fallito. Ricarica la pagina.', + }, + pt: { + _name: 'Português', + _dir: 'ltr', + hero_title: 'Pesquisa MAC offline', + tagline_prefix: 'Pesquisa offline nos registos IEEE', + tagline_suffix: '.', + and: 'e', + lookup_heading: 'Pesquisa', + input_label: 'Endereço MAC, prefixo ou nome do fabricante', + input_placeholder: '00:1A:7D… ou cisco', + input_hint: 'Hex: pesquisa por prefixo mais longo (MA-S → MA-M → MA-L). Texto: pesquisa difusa do fabricante.', + noscript: 'Esta app precisa de JavaScript para a pesquisa offline.', + status_loading: 'A carregar registo…', + refresh_button: 'Atualizar dados', + refresh_title: 'Obter os registos IEEE mais recentes (apenas online)', + refresh_aria: 'Atualizar dados do registo', + data_version_label: 'Versão dos dados:', + source_link: 'código', + language_label: 'Idioma', + no_matches: 'Sem resultados.', + registry_loading: 'A carregar registo…', + no_prefix: 'Sem entrada para o prefixo {0}.', + loaded_from_cache: '{0} entradas carregadas da cache.', + loaded_latest: '{0} entradas carregadas (atualizadas).', + up_to_date: '{0} entradas · atualizado', + using_cached: '{0} entradas · usando cache', + could_not_load: 'Não foi possível carregar os dados{0}.', + could_not_load_reload: 'Não foi possível carregar os dados{0}. Conecte-se à rede e recarregue.', + offline_no_cache: 'Offline e sem cache.', + offline_no_cache_reload: 'Offline e sem cache. Conecte-se e recarregue.', + offline_cannot_refresh: 'Offline — não é possível atualizar.', + checking_updates: 'A verificar atualizações…', + downloading: 'A descarregar registo…', + refreshed: 'Atualizado — {0} entradas.', + already_up_to_date: 'Já está atualizado — {0} entradas.', + refresh_cancelled: 'Atualização cancelada.', + refresh_failed_keeping: 'Atualização falhou — a manter a cache.', + refresh_failed: 'Atualização falhou: {0}', + entries_online: '{0} entradas · online', + entries_offline: '{0} entradas · offline', + updated_latest: 'Atualizado para a versão mais recente — {0} entradas.', + in_memory_only: 'apenas em memória', + failed_startup: 'Falha ao iniciar.', + failed_load_refresh: 'Falha ao carregar. Tente recarregar a página.', + }, + 'zh-Hans': { + _name: '简体中文', + _dir: 'ltr', + hero_title: '离线 MAC 地址查询', + tagline_prefix: '在 IEEE', + tagline_suffix: '注册表中离线查询。', + and: '和', + lookup_heading: '查询', + input_label: 'MAC 地址、前缀或厂商名称', + input_placeholder: '00:1A:7D… 或 cisco', + input_hint: '十六进制输入按最长前缀匹配 (MA-S → MA-M → MA-L)。文本输入为模糊厂商搜索。', + noscript: '此应用需要 JavaScript 才能进行离线查询。', + status_loading: '正在加载注册表…', + refresh_button: '刷新数据', + refresh_title: '获取最新的 IEEE 注册表(需联网)', + refresh_aria: '刷新注册表数据', + data_version_label: '数据版本:', + source_link: '源代码', + language_label: '语言', + no_matches: '无匹配结果。', + registry_loading: '注册表仍在加载…', + no_prefix: '前缀 {0} 无对应记录。', + loaded_from_cache: '已从缓存加载 {0} 条记录。', + loaded_latest: '已加载 {0} 条记录(最新)。', + up_to_date: '{0} 条记录 · 已是最新', + using_cached: '{0} 条记录 · 使用缓存', + could_not_load: '无法加载注册表数据{0}。', + could_not_load_reload: '无法加载注册表数据{0}。请连接网络后重新加载。', + offline_no_cache: '离线且无缓存数据。', + offline_no_cache_reload: '离线且无缓存数据。请连接网络后重新加载。', + offline_cannot_refresh: '离线 — 无法刷新。', + checking_updates: '正在检查更新…', + downloading: '正在下载注册表…', + refreshed: '已刷新 — {0} 条记录。', + already_up_to_date: '已是最新 — {0} 条记录。', + refresh_cancelled: '刷新已取消。', + refresh_failed_keeping: '刷新失败 — 保留缓存数据。', + refresh_failed: '刷新失败: {0}', + entries_online: '{0} 条记录 · 在线', + entries_offline: '{0} 条记录 · 离线', + updated_latest: '已更新到最新 — {0} 条记录。', + in_memory_only: '仅内存', + failed_startup: '启动失败。', + failed_load_refresh: '加载失败。请尝试刷新页面。', + }, + 'zh-Hant': { + _name: '繁體中文 / 廣東話', + _dir: 'ltr', + hero_title: '離線 MAC 位址查詢', + tagline_prefix: '在 IEEE', + tagline_suffix: '註冊表中離線查詢。', + and: '和', + lookup_heading: '查詢', + input_label: 'MAC 位址、前綴或廠商名稱', + input_placeholder: '00:1A:7D… 或 cisco', + input_hint: '十六進位輸入按最長前綴比對 (MA-S → MA-M → MA-L)。文字輸入為模糊廠商搜尋。', + noscript: '此應用程式需要 JavaScript 才能進行離線查詢。', + status_loading: '正在載入註冊表…', + refresh_button: '重新整理資料', + refresh_title: '取得最新的 IEEE 註冊表(需連線)', + refresh_aria: '重新整理註冊表資料', + data_version_label: '資料版本:', + source_link: '原始碼', + language_label: '語言', + no_matches: '沒有相符結果。', + registry_loading: '註冊表仍在載入…', + no_prefix: '前綴 {0} 沒有對應紀錄。', + loaded_from_cache: '已從快取載入 {0} 筆紀錄。', + loaded_latest: '已載入 {0} 筆紀錄(最新)。', + up_to_date: '{0} 筆紀錄 · 已是最新', + using_cached: '{0} 筆紀錄 · 使用快取', + could_not_load: '無法載入註冊表資料{0}。', + could_not_load_reload: '無法載入註冊表資料{0}。請連線後重新載入。', + offline_no_cache: '離線且無快取資料。', + offline_no_cache_reload: '離線且無快取資料。請連線後重新載入。', + offline_cannot_refresh: '離線 — 無法重新整理。', + checking_updates: '正在檢查更新…', + downloading: '正在下載註冊表…', + refreshed: '已更新 — {0} 筆紀錄。', + already_up_to_date: '已是最新 — {0} 筆紀錄。', + refresh_cancelled: '重新整理已取消。', + refresh_failed_keeping: '重新整理失敗 — 保留快取資料。', + refresh_failed: '重新整理失敗: {0}', + entries_online: '{0} 筆紀錄 · 在線', + entries_offline: '{0} 筆紀錄 · 離線', + updated_latest: '已更新到最新 — {0} 筆紀錄。', + in_memory_only: '僅記憶體', + failed_startup: '啟動失敗。', + failed_load_refresh: '載入失敗。請嘗試重新整理頁面。', + }, + ja: { + _name: '日本語', + _dir: 'ltr', + hero_title: 'オフライン MAC アドレス検索', + tagline_prefix: 'IEEE', + tagline_suffix: 'レジストリのオフライン検索。', + and: 'と', + lookup_heading: '検索', + input_label: 'MAC アドレス、プレフィックス、またはベンダー名', + input_placeholder: '00:1A:7D… または cisco', + input_hint: '16 進入力は最長プレフィックス検索 (MA-S → MA-M → MA-L)。テキストはファジー検索。', + noscript: 'このアプリはオフライン検索のために JavaScript が必要です。', + status_loading: 'レジストリを読み込み中…', + refresh_button: 'データを更新', + refresh_title: '最新の IEEE レジストリを取得(オンライン時のみ)', + refresh_aria: 'レジストリデータを更新', + data_version_label: 'データバージョン:', + source_link: 'ソース', + language_label: '言語', + no_matches: '一致なし。', + registry_loading: 'レジストリを読み込み中…', + no_prefix: 'プレフィックス {0} のエントリがありません。', + loaded_from_cache: 'キャッシュから {0} 件を読み込みました。', + loaded_latest: '{0} 件を読み込みました(最新)。', + up_to_date: '{0} 件 · 最新', + using_cached: '{0} 件 · キャッシュ使用', + could_not_load: 'データを読み込めませんでした{0}。', + could_not_load_reload: 'データを読み込めませんでした{0}。ネットワークに接続して再読み込みしてください。', + offline_no_cache: 'オフラインかつキャッシュなし。', + offline_no_cache_reload: 'オフラインかつキャッシュなし。接続して再読み込みしてください。', + offline_cannot_refresh: 'オフライン — 更新できません。', + checking_updates: '更新を確認中…', + downloading: 'レジストリをダウンロード中…', + refreshed: '更新しました — {0} 件。', + already_up_to_date: '既に最新 — {0} 件。', + refresh_cancelled: '更新をキャンセルしました。', + refresh_failed_keeping: '更新失敗 — キャッシュを保持します。', + refresh_failed: '更新失敗: {0}', + entries_online: '{0} 件 · オンライン', + entries_offline: '{0} 件 · オフライン', + updated_latest: '最新に更新 — {0} 件。', + in_memory_only: 'メモリのみ', + failed_startup: '起動に失敗しました。', + failed_load_refresh: '読み込みに失敗しました。ページを再読み込みしてください。', + }, + ko: { + _name: '한국어', + _dir: 'ltr', + hero_title: '오프라인 MAC 주소 조회', + tagline_prefix: 'IEEE', + tagline_suffix: '레지스트리에서 오프라인 조회.', + and: '및', + lookup_heading: '조회', + input_label: 'MAC 주소, 접두사 또는 제조사 이름', + input_placeholder: '00:1A:7D… 또는 cisco', + input_hint: '16진 입력은 최장 접두사 조회 (MA-S → MA-M → MA-L). 텍스트는 퍼지 검색.', + noscript: '이 앱은 오프라인 조회를 위해 JavaScript가 필요합니다.', + status_loading: '레지스트리 로딩 중…', + refresh_button: '데이터 새로고침', + refresh_title: '최신 IEEE 레지스트리 가져오기 (온라인 전용)', + refresh_aria: '레지스트리 데이터 새로고침', + data_version_label: '데이터 버전:', + source_link: '소스', + language_label: '언어', + no_matches: '일치 항목 없음.', + registry_loading: '레지스트리 로딩 중…', + no_prefix: '접두사 {0}에 해당하는 항목이 없습니다.', + loaded_from_cache: '캐시에서 {0}개 항목 로드됨.', + loaded_latest: '{0}개 항목 로드됨 (최신).', + up_to_date: '{0}개 항목 · 최신', + using_cached: '{0}개 항목 · 캐시 사용', + could_not_load: '데이터를 로드할 수 없습니다{0}.', + could_not_load_reload: '데이터를 로드할 수 없습니다{0}. 네트워크 연결 후 다시 로드하세요.', + offline_no_cache: '오프라인이며 캐시 없음.', + offline_no_cache_reload: '오프라인이며 캐시 없음. 연결 후 다시 로드하세요.', + offline_cannot_refresh: '오프라인 — 새로고침 불가.', + checking_updates: '업데이트 확인 중…', + downloading: '레지스트리 다운로드 중…', + refreshed: '새로고침됨 — {0}개 항목.', + already_up_to_date: '이미 최신 — {0}개 항목.', + refresh_cancelled: '새로고침 취소됨.', + refresh_failed_keeping: '새로고침 실패 — 캐시 유지.', + refresh_failed: '새로고침 실패: {0}', + entries_online: '{0}개 항목 · 온라인', + entries_offline: '{0}개 항목 · 오프라인', + updated_latest: '최신으로 업데이트됨 — {0}개 항목.', + in_memory_only: '메모리 전용', + failed_startup: '시작 실패.', + failed_load_refresh: '로드 실패. 페이지를 새로고침하세요.', + }, + hi: { + _name: 'हिन्दी', + _dir: 'ltr', + hero_title: 'ऑफ़लाइन MAC पता खोज', + tagline_prefix: 'IEEE', + tagline_suffix: 'रजिस्ट्री में ऑफ़लाइन खोज।', + and: 'और', + lookup_heading: 'खोज', + input_label: 'MAC पता, उपसर्ग, या निर्माता का नाम', + input_placeholder: '00:1A:7D… या cisco', + input_hint: 'हेक्स इनपुट: सबसे लंबा उपसर्ग खोज (MA-S → MA-M → MA-L)। टेक्स्ट: फ़ज़ी निर्माता खोज।', + noscript: 'इस ऐप को ऑफ़लाइन खोज के लिए JavaScript चाहिए।', + status_loading: 'रजिस्ट्री लोड हो रही है…', + refresh_button: 'डेटा रिफ़्रेश करें', + refresh_title: 'नवीनतम IEEE रजिस्ट्री प्राप्त करें (केवल ऑनलाइन)', + refresh_aria: 'रजिस्ट्री डेटा रिफ़्रेश करें', + data_version_label: 'डेटा संस्करण:', + source_link: 'स्रोत', + language_label: 'भाषा', + no_matches: 'कोई मिलान नहीं।', + registry_loading: 'रजिस्ट्री अभी भी लोड हो रही है…', + no_prefix: 'उपसर्ग {0} के लिए कोई प्रविष्टि नहीं।', + loaded_from_cache: 'कैश से {0} प्रविष्टियाँ लोड हुईं।', + loaded_latest: '{0} प्रविष्टियाँ लोड हुईं (नवीनतम)।', + up_to_date: '{0} प्रविष्टियाँ · अद्यतन', + using_cached: '{0} प्रविष्टियाँ · कैश का उपयोग', + could_not_load: 'डेटा लोड नहीं हो सका{0}।', + could_not_load_reload: 'डेटा लोड नहीं हो सका{0}। नेटवर्क से जुड़ें और पुनः लोड करें।', + offline_no_cache: 'ऑफ़लाइन और कोई कैश डेटा नहीं।', + offline_no_cache_reload: 'ऑफ़लाइन और कोई कैश डेटा नहीं। जुड़ें और पुनः लोड करें।', + offline_cannot_refresh: 'ऑफ़लाइन — रिफ़्रेश संभव नहीं।', + checking_updates: 'अपडेट जाँचे जा रहे हैं…', + downloading: 'रजिस्ट्री डाउनलोड हो रही है…', + refreshed: 'रिफ़्रेश किया गया — {0} प्रविष्टियाँ।', + already_up_to_date: 'पहले से अद्यतन — {0} प्रविष्टियाँ।', + refresh_cancelled: 'रिफ़्रेश रद्द किया गया।', + refresh_failed_keeping: 'रिफ़्रेश विफल — कैश डेटा रखा गया।', + refresh_failed: 'रिफ़्रेश विफल: {0}', + entries_online: '{0} प्रविष्टियाँ · ऑनलाइन', + entries_offline: '{0} प्रविष्टियाँ · ऑफ़लाइन', + updated_latest: 'नवीनतम पर अपडेट — {0} प्रविष्टियाँ।', + in_memory_only: 'केवल मेमोरी', + failed_startup: 'शुरू होने में विफल।', + failed_load_refresh: 'लोड विफल। पृष्ठ पुनः लोड करें।', + }, + fil: { + _name: 'Filipino / Tagalog', + _dir: 'ltr', + hero_title: 'Offline na Paghahanap ng MAC Address', + tagline_prefix: 'Offline na paghahanap sa IEEE', + tagline_suffix: 'na registry.', + and: 'at', + lookup_heading: 'Paghahanap', + input_label: 'MAC address, prefix, o pangalan ng vendor', + input_placeholder: '00:1A:7D… o cisco', + input_hint: 'Hex: pinakamahabang prefix (MA-S → MA-M → MA-L). Teksto: fuzzy na paghahanap ng vendor.', + noscript: 'Kailangan ng JavaScript ng app na ito para sa offline na paghahanap.', + status_loading: 'Naglo-load ng registry…', + refresh_button: 'I-refresh ang data', + refresh_title: 'Kunin ang pinakabagong IEEE registries (online lang)', + refresh_aria: 'I-refresh ang registry data', + data_version_label: 'Bersyon ng data:', + source_link: 'source', + language_label: 'Wika', + no_matches: 'Walang tugma.', + registry_loading: 'Naglo-load pa ang registry…', + no_prefix: 'Walang entry para sa prefix {0}.', + loaded_from_cache: 'Naglo-load ng {0} entries mula sa cache.', + loaded_latest: 'Naglo-load ng {0} entries (pinakabago).', + up_to_date: '{0} entries · pinakabago', + using_cached: '{0} entries · gumagamit ng cache', + could_not_load: 'Hindi ma-load ang data{0}.', + could_not_load_reload: 'Hindi ma-load ang data{0}. Kumonekta sa network at i-reload.', + offline_no_cache: 'Offline at walang cache.', + offline_no_cache_reload: 'Offline at walang cache. Kumonekta at i-reload.', + offline_cannot_refresh: 'Offline — hindi maaaring mag-refresh.', + checking_updates: 'Tinitingnan ang mga update…', + downloading: 'Nagda-download ng registry…', + refreshed: 'Na-refresh — {0} entries.', + already_up_to_date: 'Pinakabago na — {0} entries.', + refresh_cancelled: 'Kanselado ang refresh.', + refresh_failed_keeping: 'Bigo ang refresh — pinanatili ang cache.', + refresh_failed: 'Bigo ang refresh: {0}', + entries_online: '{0} entries · online', + entries_offline: '{0} entries · offline', + updated_latest: 'Na-update sa pinakabago — {0} entries.', + in_memory_only: 'memory lang', + failed_startup: 'Bigo ang pagsisimula.', + failed_load_refresh: 'Bigo ang pag-load. Subukang i-reload ang pahina.', + }, + ar: { + _name: 'العربية', + _dir: 'rtl', + hero_title: 'بحث عن عناوين MAC دون اتصال', + tagline_prefix: 'بحث دون اتصال في سجلات', + tagline_suffix: 'الخاصة بـ IEEE.', + and: 'و', + lookup_heading: 'بحث', + input_label: 'عنوان MAC أو بادئة أو اسم المُصنِّع', + input_placeholder: '00:1A:7D… أو cisco', + input_hint: 'الإدخال السداسي يجري بحثًا بأطول بادئة (MA-S → MA-M → MA-L). النص يجري بحثًا ضبابيًا.', + noscript: 'يحتاج هذا التطبيق إلى JavaScript للعمل دون اتصال.', + status_loading: 'جاري تحميل السجل…', + refresh_button: 'تحديث البيانات', + refresh_title: 'جلب أحدث سجلات IEEE (يتطلب اتصالًا)', + refresh_aria: 'تحديث بيانات السجل', + data_version_label: 'إصدار البيانات:', + source_link: 'المصدر', + language_label: 'اللغة', + no_matches: 'لا توجد نتائج.', + registry_loading: 'لا يزال السجل قيد التحميل…', + no_prefix: 'لا يوجد سجل للبادئة {0}.', + loaded_from_cache: 'تم تحميل {0} إدخالاً من الذاكرة المؤقتة.', + loaded_latest: 'تم تحميل {0} إدخالاً (الأحدث).', + up_to_date: '{0} إدخال · محدّث', + using_cached: '{0} إدخال · استخدام الذاكرة المؤقتة', + could_not_load: 'تعذّر تحميل البيانات{0}.', + could_not_load_reload: 'تعذّر تحميل البيانات{0}. اتصل بالشبكة وأعد التحميل.', + offline_no_cache: 'دون اتصال ولا توجد بيانات مؤقتة.', + offline_no_cache_reload: 'دون اتصال ولا توجد بيانات مؤقتة. اتصل وأعد التحميل.', + offline_cannot_refresh: 'دون اتصال — لا يمكن التحديث.', + checking_updates: 'البحث عن تحديثات…', + downloading: 'جاري تنزيل السجل…', + refreshed: 'تم التحديث — {0} إدخال.', + already_up_to_date: 'محدّث بالفعل — {0} إدخال.', + refresh_cancelled: 'تم إلغاء التحديث.', + refresh_failed_keeping: 'فشل التحديث — الاحتفاظ بالذاكرة المؤقتة.', + refresh_failed: 'فشل التحديث: {0}', + entries_online: '{0} إدخال · متصل', + entries_offline: '{0} إدخال · دون اتصال', + updated_latest: 'تم التحديث إلى الأحدث — {0} إدخال.', + in_memory_only: 'الذاكرة فقط', + failed_startup: 'فشل البدء.', + failed_load_refresh: 'فشل التحميل. حاول إعادة تحميل الصفحة.', + }, +}; + +const SUPPORTED = Object.keys(LOCALES); +const DEFAULT_LOCALE = 'en'; +const STORAGE_KEY = 'maclookup.lang'; + +// Map browser language tags to our supported keys. +// - "zh-CN", "zh-SG", "zh-Hans-*" -> zh-Hans +// - "zh-HK", "zh-TW", "zh-Hant-*", "yue-*" (Cantonese) -> zh-Hant +// - "fil-*", "tl-*" -> fil +// - "pt-*" -> pt, "es-*" -> es, etc. +function normalizeLocale(tag) { + if (!tag) return null; + const t = String(tag).trim(); + if (!t) return null; + const lower = t.toLowerCase(); + + // Chinese: script tag wins, then region. + if (lower.startsWith('zh')) { + if (/(^|[-_])(hant|tw|hk|mo)([-_]|$)/.test(lower)) return 'zh-Hant'; + if (/(^|[-_])(hans|cn|sg|my)([-_]|$)/.test(lower)) return 'zh-Hans'; + return 'zh-Hans'; + } + // Cantonese + if (lower.startsWith('yue')) return 'zh-Hant'; + // Tagalog / Filipino + if (lower.startsWith('fil') || lower.startsWith('tl')) return 'fil'; + + // Strip region for the rest: "fr-CA" -> "fr". + const base = lower.split(/[-_]/)[0]; + if (SUPPORTED.indexOf(base) >= 0) return base; + return null; +} + +function readPersisted() { + // 1) URL ?lang= or #lang= override (highest priority, lets users share a + // pre-translated link) + try { + const u = new URL(location.href); + const q = u.searchParams.get('lang'); + if (q) { + const n = normalizeLocale(q) || (SUPPORTED.indexOf(q) >= 0 ? q : null); + if (n) return n; + } + const h = (location.hash || '').replace(/^#/, ''); + if (h) { + const params = new URLSearchParams(h); + const v = params.get('lang'); + if (v) { + const n = normalizeLocale(v) || (SUPPORTED.indexOf(v) >= 0 ? v : null); + if (n) return n; + } + } + } catch (_) { /* ignore */ } + + // 2) localStorage (best-effort — private mode can throw on read) + try { + if (typeof localStorage !== 'undefined') { + const v = localStorage.getItem(STORAGE_KEY); + if (v && SUPPORTED.indexOf(v) >= 0) return v; + } + } catch (_) { /* ignore */ } + + return null; +} + +function detectFromNavigator() { + try { + const list = (navigator && navigator.languages && navigator.languages.length) + ? navigator.languages + : [navigator && navigator.language].filter(Boolean); + for (const tag of list) { + const n = normalizeLocale(tag); + if (n) return n; + } + } catch (_) { /* ignore */ } + return null; +} + +function pickInitial() { + return readPersisted() || detectFromNavigator() || DEFAULT_LOCALE; +} + +function persist(locale) { + try { + if (typeof localStorage !== 'undefined') { + localStorage.setItem(STORAGE_KEY, locale); + return; + } + } catch (_) { /* ignore — fall through to URL */ } + // Fallback: encode in URL hash so a reload keeps the choice. + try { + const u = new URL(location.href); + const h = new URLSearchParams((u.hash || '').replace(/^#/, '')); + h.set('lang', locale); + u.hash = h.toString(); + history.replaceState(null, '', u.toString()); + } catch (_) { /* ignore */ } +} + +let currentLocale = DEFAULT_LOCALE; + +function t(key, ...args) { + const dict = LOCALES[currentLocale] || LOCALES[DEFAULT_LOCALE]; + let s = dict[key]; + if (s == null) s = LOCALES[DEFAULT_LOCALE][key]; + if (s == null) return key; + for (let i = 0; i < args.length; i++) { + s = s.split('{' + i + '}').join(String(args[i])); + } + return s; +} + +function applyTranslations() { + const root = document; + // Text content + root.querySelectorAll('[data-i18n]').forEach((el) => { + const key = el.getAttribute('data-i18n'); + if (key) el.textContent = t(key); + }); + // Placeholder + root.querySelectorAll('[data-i18n-placeholder]').forEach((el) => { + const key = el.getAttribute('data-i18n-placeholder'); + if (key) el.setAttribute('placeholder', t(key)); + }); + // Title attribute + root.querySelectorAll('[data-i18n-title]').forEach((el) => { + const key = el.getAttribute('data-i18n-title'); + if (key) el.setAttribute('title', t(key)); + }); + // aria-label + root.querySelectorAll('[data-i18n-aria-label]').forEach((el) => { + const key = el.getAttribute('data-i18n-aria-label'); + if (key) el.setAttribute('aria-label', t(key)); + }); + + // + dir + try { + document.documentElement.lang = currentLocale; + document.documentElement.dir = (LOCALES[currentLocale] && LOCALES[currentLocale]._dir) || 'ltr'; + } catch (_) {} + + // Tell app.js to re-render anything that depends on translations. + try { + window.dispatchEvent(new CustomEvent('i18n:changed', { detail: { locale: currentLocale } })); + } catch (_) {} +} + +function buildSelector() { + const sel = document.getElementById('lang-select'); + if (!sel) return; + sel.innerHTML = ''; + for (const code of SUPPORTED) { + const opt = document.createElement('option'); + opt.value = code; + opt.textContent = LOCALES[code]._name; + if (code === currentLocale) opt.selected = true; + sel.appendChild(opt); + } + sel.addEventListener('change', () => { + setLocale(sel.value); + }); +} + +function setLocale(locale) { + if (SUPPORTED.indexOf(locale) < 0) locale = DEFAULT_LOCALE; + currentLocale = locale; + persist(locale); + applyTranslations(); +} + +// Public API on window so app.js (and tests) can use t() for status messages. +window.i18n = { + t, + setLocale, + getLocale: () => currentLocale, + supported: () => SUPPORTED.slice(), + normalizeLocale, + _LOCALES: LOCALES, // exposed for tests; not load-bearing +}; + +function init() { + currentLocale = pickInitial(); + buildSelector(); + applyTranslations(); +} + +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init, { once: true }); +} else { + init(); +} diff --git a/web/icons/og-image.png b/web/icons/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..505c764b985cd62788df8fe2b5093779aff00d3f GIT binary patch literal 70259 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 + + + + + + + + + + + + + + + Offline MAC Address + Lookup + + + IEEE MA-L · MA-M · MA-S registries · longest-prefix match · fuzzy vendor search + Works offline. No tracking. Installable PWA. + + + 00:1A:7D… + + diff --git a/web/index.html b/web/index.html index 95d9935..9362949 100644 --- a/web/index.html +++ b/web/index.html @@ -1,10 +1,38 @@ - + - MAC Vendor Lookup + + + Offline MAC Address Lookup — IEEE MA-L / MA-M / MA-S + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -12,19 +40,25 @@
-

MAC Vendor Lookup

+
+ + +
+

Offline MAC Address Lookup

- Offline lookup across IEEE + Offline lookup across IEEE MA-L, - MA-M, and - MA-S registries. + MA-M, + and + MA-S + registries.

-

Lookup

- +

Lookup

+ Lookup autocapitalize="off" spellcheck="false" placeholder="00:1A:7D… or cisco" + data-i18n-placeholder="input_placeholder" aria-describedby="query-hint" /> -

+

Hex input does a longest-prefix lookup (MA-S → MA-M → MA-L). Text input does fuzzy vendor search.

@@ -42,25 +77,29 @@

Lookup

- Loading registry… + Loading registry…

- Data version: · - source + Data version: + · + source

+ diff --git a/web/manifest.webmanifest b/web/manifest.webmanifest index c6ab20b..59971a0 100644 --- a/web/manifest.webmanifest +++ b/web/manifest.webmanifest @@ -1,12 +1,15 @@ { - "name": "MAC Vendor Lookup", + "name": "Offline MAC Address Lookup", "short_name": "MAC Lookup", - "description": "Offline IEEE MA-L / MA-M / MA-S vendor lookup with fuzzy search.", + "description": "Offline MAC address lookup across IEEE MA-L, MA-M, and MA-S registries. Longest-prefix match, fuzzy vendor search, installable PWA. Works offline.", "start_url": "./", "scope": "./", "display": "standalone", "background_color": "#0b1020", "theme_color": "#0b1020", + "lang": "en", + "dir": "ltr", + "categories": ["utilities", "productivity", "developer"], "icons": [ { "src": "icons/icon.svg", "type": "image/svg+xml", "sizes": "any", "purpose": "any maskable" }, { "src": "icons/icon-192.png", "type": "image/png", "sizes": "192x192", "purpose": "any" }, diff --git a/web/styles.css b/web/styles.css index 050f1b8..8646cec 100644 --- a/web/styles.css +++ b/web/styles.css @@ -156,3 +156,27 @@ a { color: var(--accent); } overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; } + +/* Language toggle in the header. Right-aligned, unobtrusive. */ +.lang-bar { + display: flex; + justify-content: flex-end; + align-items: center; + gap: .5rem; + margin-bottom: .25rem; +} +.lang-label { + color: var(--muted); + font-size: .8rem; +} +.lang-select { + background: var(--panel); + color: var(--fg); + border: 1px solid var(--panel-2); + border-radius: var(--radius); + padding: .25rem .5rem; + font: inherit; + font-size: .85rem; +} +/* In RTL languages, mirror the bar so the selector sits on the visual right. */ +html[dir="rtl"] .lang-bar { justify-content: flex-start; } diff --git a/web/sw.js b/web/sw.js index d5a674b..465fec9 100644 --- a/web/sw.js +++ b/web/sw.js @@ -18,7 +18,7 @@ * - Bump *_CACHE names when the on-disk format changes so older clients * evict stale entries on activate. */ -const APP_SHELL_CACHE = 'maclookup-shell-v3'; +const APP_SHELL_CACHE = 'maclookup-shell-v4'; const DATA_CACHE = 'maclookup-data-v3'; // Hard ceiling on network-first data requests inside the SW. The browser @@ -30,6 +30,7 @@ const SHELL_URLS = [ './index.html', './styles.css', './app.js', + './i18n.js', './manifest.webmanifest', './icons/icon.svg', ]; From 069b7108634bc9787f48ea2655ce84348c1869e0 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 27 May 2026 20:39:39 +0000 Subject: [PATCH 2/2] Fix PWA smoke tests under Node 22+: defineProperty for read-only globals Root cause: Node 21+ exposes `navigator` as a getter-only accessor on globalThis (web-platform compat). Plain `globalThis.navigator = ...` throws `TypeError: Cannot set property navigator of # which has only a getter`. The CI bump in the previous commit moved setup-node from 20 to 22, which triggered this. Both smoke harnesses now stub globals via Object.defineProperty(..., { value, writable: true, configurable: true }) so the replacement works whether the property was unset (Node 20) or pre-defined as a built-in getter (Node 22+/24+). Applied uniformly to navigator, document, window, location, history, indexedDB, localStorage, URL, URLSearchParams, CustomEvent. Verified locally on both Node v20.20.1 and Node v22.11.0: - tests/web/fresh_load.mjs passes - tests/web/i18n_smoke.mjs passes - python tests 16/16 pass - node --check on app.js / sw.js / i18n.js clean Co-Authored-By: Claude Opus 4.7 --- tests/web/fresh_load.mjs | 21 +++++++++++++++++---- tests/web/i18n_smoke.mjs | 36 ++++++++++++++++++++++++++---------- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/tests/web/fresh_load.mjs b/tests/web/fresh_load.mjs index 3f94e14..b025e46 100644 --- a/tests/web/fresh_load.mjs +++ b/tests/web/fresh_load.mjs @@ -94,12 +94,25 @@ function makeDom() { }; } +// Some web-platform globals (notably `navigator` in Node >= 21) are exposed +// as getter-only accessors on globalThis. Plain assignment throws there. Use +// Object.defineProperty so we can replace them regardless of whether the +// property is unset (Node 20) or a built-in getter (Node 22+/24+). +function setGlobal(name, value) { + Object.defineProperty(globalThis, name, { + value, + writable: true, + configurable: true, + enumerable: false, + }); +} + async function loadApp({ haveIDB = false, baseUrl } = {}) { const dom = makeDom(); - globalThis.document = dom.document; - globalThis.window = { addEventListener() {} }; - globalThis.navigator = { onLine: true }; - globalThis.indexedDB = haveIDB ? globalThis.__realIDB : undefined; + setGlobal('document', dom.document); + setGlobal('window', { addEventListener() {} }); + setGlobal('navigator', { onLine: true }); + setGlobal('indexedDB', haveIDB ? globalThis.__realIDB : undefined); // Redirect data/*.json fetches to the local HTTP server so abort semantics // are real (browser-style streaming). Node 18+ exposes fetch globally. diff --git a/tests/web/i18n_smoke.mjs b/tests/web/i18n_smoke.mjs index 2df6d99..fb9ff03 100644 --- a/tests/web/i18n_smoke.mjs +++ b/tests/web/i18n_smoke.mjs @@ -44,19 +44,35 @@ function makeDom() { }; } +// `navigator` is a getter-only accessor on globalThis in Node >= 21, so a +// direct assignment throws. defineProperty works whether the global is unset +// (Node 20) or a built-in getter (Node 22+/24+). We use the same helper for +// every shim so behavior is uniform regardless of which globals the runtime +// has already populated. +function setGlobal(name, value) { + Object.defineProperty(globalThis, name, { + value, + writable: true, + configurable: true, + enumerable: false, + }); +} + async function loadI18n({ languages = ['en'] } = {}) { - globalThis.document = makeDom(); - globalThis.window = { + setGlobal('document', makeDom()); + setGlobal('window', { addEventListener() {}, dispatchEvent() {}, - }; - globalThis.location = { href: 'http://localhost/', hash: '', search: '' }; - globalThis.history = { replaceState() {} }; - globalThis.navigator = { languages, language: languages[0] }; - globalThis.localStorage = undefined; - globalThis.URL = URL; - globalThis.URLSearchParams = URLSearchParams; - globalThis.CustomEvent = class CustomEvent { constructor(n, o) { this.name = n; this.detail = o && o.detail; } }; + }); + setGlobal('location', { href: 'http://localhost/', hash: '', search: '' }); + setGlobal('history', { replaceState() {} }); + setGlobal('navigator', { languages, language: languages[0] }); + setGlobal('localStorage', undefined); + setGlobal('URL', URL); + setGlobal('URLSearchParams', URLSearchParams); + setGlobal('CustomEvent', class CustomEvent { + constructor(n, o) { this.name = n; this.detail = o && o.detail; } + }); const src = readFileSync(I18N_JS, 'utf8') + `\n//# salt=${Math.random()}\n`; await import('data:text/javascript;base64,' + Buffer.from(src).toString('base64'));