diff --git a/i18n/locales/ar-EG.json b/i18n/locales/ar-EG.json
index 18682843b5..b072fd4842 100644
--- a/i18n/locales/ar-EG.json
+++ b/i18n/locales/ar-EG.json
@@ -82,7 +82,6 @@
"cancel": "إلغاء",
"save": "حفظ",
"edit": "تعديل",
- "error": "خطأ",
"view_on": {
"gitlab": "عرض على GitLab",
"bitbucket": "عرض على Bitbucket",
@@ -113,7 +112,9 @@
"message": "انضم إليّ على npmx",
"share_button": "دعوة صديق",
"compose_text": "جرّب npmx، المتصفح السريع لحزم npm!"
- }
+ },
+ "likes_error": "تعذّر تحميل الحزم المُعجَب بها.",
+ "likes_empty": "لا توجد حزم مُعجَب بها بعد."
},
"package": {
"size_increase": {
diff --git a/i18n/locales/ar.json b/i18n/locales/ar.json
index 179ed6251f..31bc7749cf 100644
--- a/i18n/locales/ar.json
+++ b/i18n/locales/ar.json
@@ -155,7 +155,9 @@
}
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "تعذر تحميل الحزم المفضلة.",
+ "likes_empty": "لا توجد حزم مفضلة بعد."
},
"package": {
"not_found": "لم يتم العثور على الحزمة",
diff --git a/i18n/locales/az-AZ.json b/i18n/locales/az-AZ.json
index 93c9a8117a..da00b06ba9 100644
--- a/i18n/locales/az-AZ.json
+++ b/i18n/locales/az-AZ.json
@@ -193,7 +193,6 @@
"cancel": "Ləğv et",
"save": "Saxla",
"edit": "Redaktə et",
- "error": "Xəta",
"view_on": {
"npm": "npm-də bax",
"github": "GitHub-da bax"
@@ -214,7 +213,9 @@
"message": "Deyəsən onlar hələ npmx istifadə etmirlər. Onlara bu barədə demək istəyirsiniz?",
"share_button": "Bluesky-da paylaş",
"compose_text": "Salam {'@'}{handle}! npmx.dev-i yoxlamısan? Bu, npm reyestri üçün sürətli, müasir və açıq mənbəli brauzerdir.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Bəyənilən paketlər yüklənə bilmədi.",
+ "likes_empty": "Hələ bəyənilən paket yoxdur."
},
"package": {
"not_found": "Paket Tapılmadı",
diff --git a/i18n/locales/bg-BG.json b/i18n/locales/bg-BG.json
index f937b02e1e..467fced6b9 100644
--- a/i18n/locales/bg-BG.json
+++ b/i18n/locales/bg-BG.json
@@ -156,7 +156,6 @@
"cancel": "Отказ",
"save": "Запазване",
"edit": "Редактиране",
- "error": "Грешка",
"view_on": {
"npm": "преглед в npm",
"github": "Преглед в GitHub"
@@ -177,7 +176,9 @@
"message": "Изглежда, че все още не използват npmx. Искате ли да им кажете за него?",
"share_button": "Споделете в Bluesky",
"compose_text": "Здравей {'@'}{handle}! Проверил ли си npmx.dev? Това е браузър за npm регистъра - бърз, модерен и с отворен код.\\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Неуспешно зареждане на харесани пакети.",
+ "likes_empty": "Все още няма харесани пакети."
},
"package": {
"not_found": "Пакетът не е намерен",
diff --git a/i18n/locales/bn-IN.json b/i18n/locales/bn-IN.json
index 3712252621..33729ed174 100644
--- a/i18n/locales/bn-IN.json
+++ b/i18n/locales/bn-IN.json
@@ -121,7 +121,9 @@
}
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "পছন্দের প্যাকেজগুলি লোড করা যায়নি।",
+ "likes_empty": "এখনও কোনো পছন্দের প্যাকেজ নেই।"
},
"package": {
"not_found": "প্যাকেজ পাওয়া যায়নি",
diff --git a/i18n/locales/cs-CZ.json b/i18n/locales/cs-CZ.json
index b5ffca3a02..91840b95d9 100644
--- a/i18n/locales/cs-CZ.json
+++ b/i18n/locales/cs-CZ.json
@@ -323,7 +323,6 @@
"cancel": "Zrušit",
"save": "Uložit",
"edit": "Upravit",
- "error": "Chyba",
"view_on": {
"npm": "Zobrazit na npm",
"github": "Zobrazit na GitHubu",
@@ -359,7 +358,9 @@
"message": "Zdá se, že ještě nepoužívají npmx. Chcete jim o tom říct?",
"share_button": "Sdílet na Bluesky",
"compose_text": "Ahoj {'@'}{handle}! Viděl jsi už npmx.dev? Je to prohlížeč pro npm registr, který je rychlý, moderní a open-source.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Nepodařilo se načíst oblíbené balíčky.",
+ "likes_empty": "Zatím žádné oblíbené balíčky."
},
"package": {
"not_found": "Balíček nenalezen",
diff --git a/i18n/locales/de-AT.json b/i18n/locales/de-AT.json
index 458cf38f23..93a3bd30ce 100644
--- a/i18n/locales/de-AT.json
+++ b/i18n/locales/de-AT.json
@@ -21,5 +21,23 @@
"instant_search_turn_on": "Schnellsuche aktivieren",
"instant_search_turn_off": "Schnellsuche deaktivieren",
"instant_search_advisory": "Die Schnellsuche sendet bei jedem Tastendruck eine Anfrage."
+ },
+ "profile": {
+ "likes_error": "Gelikte Pakete konnten nicht geladen werden.",
+ "likes_empty": "Noch keine gelikten Pakete.",
+ "avatar_alt": "Profilavatar",
+ "unknown_profile": "Unbekanntes Profil",
+ "linked_accounts": {
+ "status": {
+ "verified": "Verifiziert",
+ "unverified": "Nicht verifiziert",
+ "stale": "Veraltet",
+ "failed": "Fehlgeschlagen"
+ },
+ "title": "Verknüpfte Konten",
+ "verified_summary": "Verifizierte Konten",
+ "legend_aria_label": "Legende zum Verifizierungsstatus von Konten",
+ "empty": "Keine verknüpften Konten"
+ }
}
}
diff --git a/i18n/locales/de-DE.json b/i18n/locales/de-DE.json
index 0618235d76..cc07f7170e 100644
--- a/i18n/locales/de-DE.json
+++ b/i18n/locales/de-DE.json
@@ -1,3 +1,21 @@
{
- "$schema": "../schema.json"
+ "$schema": "../schema.json",
+ "profile": {
+ "likes_error": "Gelikte Pakete konnten nicht geladen werden.",
+ "likes_empty": "Noch keine gelikten Pakete.",
+ "avatar_alt": "Profilavatar",
+ "unknown_profile": "Unbekanntes Profil",
+ "linked_accounts": {
+ "status": {
+ "verified": "Verifiziert",
+ "unverified": "Nicht verifiziert",
+ "stale": "Veraltet",
+ "failed": "Fehlgeschlagen"
+ },
+ "title": "Verknüpfte Konten",
+ "verified_summary": "Verifizierte Konten",
+ "legend_aria_label": "Legende zum Verifizierungsstatus von Konten",
+ "empty": "Keine verknüpften Konten"
+ }
+ }
}
diff --git a/i18n/locales/de.json b/i18n/locales/de.json
index e805f32888..087ad3abbf 100644
--- a/i18n/locales/de.json
+++ b/i18n/locales/de.json
@@ -322,7 +322,6 @@
"cancel": "Abbrechen",
"save": "Speichern",
"edit": "Bearbeiten",
- "error": "Fehler",
"view_on": {
"npm": "Auf npm ansehen",
"github": "Auf GitHub ansehen",
@@ -350,6 +349,8 @@
"website": "Website",
"website_placeholder": "https://beispiel.de",
"likes": "Likes",
+ "likes_error": "Gelikte Pakete konnten nicht geladen werden.",
+ "likes_empty": "Noch keine gelikten Pakete.",
"seo_title": "{handle} - npmx",
"seo_description": "npmx-Profil von {handle}",
"not_found": "Profil nicht gefunden",
@@ -358,6 +359,18 @@
"message": "Es sieht nicht so aus, als ob sie npmx schon benutzen. Möchtest du ihnen davon erzählen?",
"share_button": "Auf Bluesky teilen",
"compose_text": "Hey {'@'}{handle}! Hast du schon npmx.dev ausprobiert? Es ist ein Browser für die npm Registry, der schnell, modern und Open-Source ist.\nhttps://npmx.dev"
+ },
+ "linked_accounts": {
+ "status": {
+ "verified": "Verifiziert",
+ "unverified": "Nicht verifiziert",
+ "stale": "Veraltet",
+ "failed": "Fehlgeschlagen"
+ },
+ "title": "Verknüpfte Konten",
+ "verified_summary": "Verifizierte Konten",
+ "legend_aria_label": "Legende zum Verifizierungsstatus von Konten",
+ "empty": "Keine verknüpften Konten"
}
},
"package": {
@@ -379,6 +392,7 @@
"size": "Installationsgröße um {percent} gestiegen ({size} größer)",
"deps": "{count} zusätzliche Abhängigkeiten"
},
+ "size_decrease": {},
"replacement": {
"title": "Du brauchst diese Abhängigkeit vielleicht nicht.",
"native": "Dies kann durch {replacement} ersetzt werden, verfügbar seit Node {nodeVersion}.",
@@ -556,6 +570,7 @@
"current_tags": "Aktuelle Tags",
"no_match_filter": "Keine Versionen entsprechen {filter}"
},
+ "timeline": {},
"dependencies": {
"title": "Abhängigkeit ({count}) | Abhängigkeiten ({count})",
"list_label": "Paketabhängigkeiten",
@@ -1316,7 +1331,10 @@
"vulnerabilities": {
"label": "Sicherheitslücken",
"description": "Bekannte Sicherheitsrisiken"
- }
+ },
+ "githubStars": {},
+ "githubIssues": {},
+ "createdAt": {}
},
"values": {
"any": "Beliebig",
diff --git a/i18n/locales/en-GB.json b/i18n/locales/en-GB.json
index cd250a7f15..09c69c0e03 100644
--- a/i18n/locales/en-GB.json
+++ b/i18n/locales/en-GB.json
@@ -45,5 +45,9 @@
"error": "Failed to load organisations",
"empty": "No organisations found"
}
+ },
+ "profile": {
+ "likes_error": "Could not load liked packages.",
+ "likes_empty": "No liked packages yet."
}
}
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 9c132582da..bad836caf6 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -327,7 +327,6 @@
"cancel": "Cancel",
"save": "Save",
"edit": "Edit",
- "error": "Error",
"view_on": {
"npm": "View on npm",
"github": "View on GitHub",
@@ -355,6 +354,8 @@
"website": "Website",
"website_placeholder": "https://example.com",
"likes": "Likes",
+ "likes_error": "Could not load liked packages.",
+ "likes_empty": "No liked packages yet.",
"seo_title": "{handle} - npmx",
"seo_description": "npmx profile by {handle}",
"not_found": "Profile Not Found",
@@ -363,6 +364,18 @@
"message": "It doesn't look like they're using npmx yet. Want to tell them about it?",
"share_button": "Share on Bluesky",
"compose_text": "Hey {'@'}{handle}! Have you checked out npmx.dev yet? It's a browser for the npm registry that's fast, modern, and open-source.\nhttps://npmx.dev"
+ },
+ "linked_accounts": {
+ "status": {
+ "verified": "Verified",
+ "unverified": "Unverified",
+ "stale": "Stale",
+ "failed": "Failed"
+ },
+ "title": "Linked Accounts",
+ "verified_summary": "{verified}/{total} verified",
+ "legend_aria_label": "Account verification status legend",
+ "empty": "No linked accounts"
}
},
"package": {
diff --git a/i18n/locales/es-419.json b/i18n/locales/es-419.json
index aff49a7df8..fce9151ed6 100644
--- a/i18n/locales/es-419.json
+++ b/i18n/locales/es-419.json
@@ -43,7 +43,9 @@
"invite": {
"message": "Parece que aún no usa npmx. ¿Quieres contarle?",
"compose_text": "¡Hola {'@'}{handle}! ¿Has probado ya npmx.dev? Es un navegador para el registro de npm rápido, moderno y de código abierto.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "No se pudieron cargar los paquetes favoritos.",
+ "likes_empty": "Aún no hay paquetes favoritos."
},
"package": {
"readme": {
diff --git a/i18n/locales/es.json b/i18n/locales/es.json
index 9bcb7263fc..1586180d9c 100644
--- a/i18n/locales/es.json
+++ b/i18n/locales/es.json
@@ -211,7 +211,6 @@
"cancel": "Cancelar",
"save": "Guardar",
"edit": "Editar",
- "error": "Error",
"view_on": {
"npm": "ver en npm",
"github": "Ver en GitHub",
@@ -244,7 +243,9 @@
"message": "Parece que aún no usa npmx. ¿Quieres contárselo?",
"share_button": "Compartir en Bluesky",
"compose_text": "¡Hola {'@'}{handle}! ¿Has probado ya npmx.dev? Es un navegador para el registro de npm rápido, moderno y de código abierto.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "No se pudieron cargar los paquetes que te gustan.",
+ "likes_empty": "Aún no hay paquetes que te gusten."
},
"package": {
"not_found": "Paquete no encontrado",
diff --git a/i18n/locales/fr-FR.json b/i18n/locales/fr-FR.json
index 0e40fa415b..448df305b1 100644
--- a/i18n/locales/fr-FR.json
+++ b/i18n/locales/fr-FR.json
@@ -324,7 +324,6 @@
"cancel": "Annuler",
"save": "Enregistrer",
"edit": "Modifier",
- "error": "Erreur",
"view_on": {
"npm": "voir sur npm",
"github": "Voir sur GitHub",
@@ -360,7 +359,9 @@
"message": "Il semblerait qu'ils n'utilisent pas encore npmx. Vous voulez leur en parler ?",
"share_button": "Partager sur Bluesky",
"compose_text": "Salut {'@'}{handle} ! As-tu déjà testé npmx.dev ? C'est un navigateur pour le registre npm : rapide, moderne et open source.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Impossible de charger les paquets aimés.",
+ "likes_empty": "Aucun paquet aimé pour le moment."
},
"package": {
"not_found": "Paquet introuvable",
diff --git a/i18n/locales/hi-IN.json b/i18n/locales/hi-IN.json
index ea702a7728..ba58005b1e 100644
--- a/i18n/locales/hi-IN.json
+++ b/i18n/locales/hi-IN.json
@@ -210,7 +210,6 @@
"cancel": "रद्द करें",
"save": "सहेजें",
"edit": "संपादित करें",
- "error": "त्रुटि",
"view_on": {
"npm": "npm पर देखें",
"github": "GitHub पर देखें",
@@ -229,7 +228,9 @@
"expand": "विस्तृत करें"
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "पसंद किए गए पैकेज लोड नहीं हो सके।",
+ "likes_empty": "अभी तक कोई पसंद किए गए पैकेज नहीं।"
},
"package": {
"not_found": "पैकेज नहीं मिला",
diff --git a/i18n/locales/hu-HU.json b/i18n/locales/hu-HU.json
index c14948a6e1..f2a2bb74ab 100644
--- a/i18n/locales/hu-HU.json
+++ b/i18n/locales/hu-HU.json
@@ -156,7 +156,6 @@
"cancel": "Mégse",
"save": "Mentés",
"edit": "Szerkesztés",
- "error": "Hiba",
"view_on": {
"npm": "megtekintés npm-en",
"github": "Megtekintés GitHubon"
@@ -177,7 +176,9 @@
"message": "Úgy tűnik, hogy még nem használja az npmx-et. Szeretnéd megtudatni vele?",
"share_button": "Megosztás a Bluesky-on",
"compose_text": "Halló {'@'}{handle}! Már próbáltad az npmx.dev-et? Egy gyors, modern és nyílt forráskódú böngésző az npm regiszterhez.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Nem sikerült betölteni a kedvelt csomagokat.",
+ "likes_empty": "Még nincsenek kedvelt csomagok."
},
"package": {
"not_found": "Csomag Nem Található",
diff --git a/i18n/locales/id-ID.json b/i18n/locales/id-ID.json
index 9646ca17b1..b96137c96c 100644
--- a/i18n/locales/id-ID.json
+++ b/i18n/locales/id-ID.json
@@ -211,7 +211,6 @@
"cancel": "Batal",
"save": "Simpan",
"edit": "Edit",
- "error": "Kesalahan",
"view_on": {
"npm": "lihat di npm",
"github": "Lihat di GitHub",
@@ -244,7 +243,9 @@
"message": "Sepertinya mereka belum menggunakan npmx. Ingin memberi tahu mereka?",
"share_button": "Bagikan di Bluesky",
"compose_text": "Hai {'@'}{handle}! Sudah pernah mencoba npmx.dev? Ini adalah browser untuk npm registry yang cepat, modern, dan open-source.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Gagal memuat paket yang disukai.",
+ "likes_empty": "Belum ada paket yang disukai."
},
"package": {
"not_found": "Paket Tidak Ditemukan",
diff --git a/i18n/locales/it-IT.json b/i18n/locales/it-IT.json
index b19efca8db..da169b7504 100644
--- a/i18n/locales/it-IT.json
+++ b/i18n/locales/it-IT.json
@@ -155,7 +155,9 @@
}
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "Impossibile caricare i pacchetti preferiti.",
+ "likes_empty": "Nessun pacchetto preferito ancora."
},
"package": {
"not_found": "Pacchetto Non Trovato",
diff --git a/i18n/locales/ja-JP.json b/i18n/locales/ja-JP.json
index 72397e4907..79f498916c 100644
--- a/i18n/locales/ja-JP.json
+++ b/i18n/locales/ja-JP.json
@@ -193,7 +193,6 @@
"cancel": "キャンセル",
"save": "保存",
"edit": "編集",
- "error": "エラー",
"view_on": {
"npm": "npmで表示",
"github": "GitHubで表示",
@@ -224,7 +223,9 @@
"message": "まだnpmxを利用していないようです。npmxを紹介しますか?",
"share_button": "Blueskyで共有",
"compose_text": "{'@'}{handle} さん、npmx.devはもうチェックしましたか? 高速でモダンなオープンソースのnpmレジストリブラウザです。\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "いいねしたパッケージを読み込めませんでした。",
+ "likes_empty": "いいねしたパッケージはまだありません。"
},
"package": {
"not_found": "パッケージが見つかりません",
diff --git a/i18n/locales/kn-IN.json b/i18n/locales/kn-IN.json
index 2111c4dd23..084628b057 100644
--- a/i18n/locales/kn-IN.json
+++ b/i18n/locales/kn-IN.json
@@ -122,7 +122,9 @@
}
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "ಇಷ್ಟಪಟ್ಟ ಪ್ಯಾಕೇಜುಗಳನ್ನು ಲೋಡ್ ಮಾಡಲು ಸಾಧ್ಯವಾಗಲಿಲ್ಲ.",
+ "likes_empty": "ಇನ್ನೂ ಇಷ್ಟಪಟ್ಟ ಪ್ಯಾಕೇಜುಗಳಿಲ್ಲ."
},
"package": {
"not_found": "ಪ್ಯಾಕೇಜ್ ಕಂಡುಬಂದಿಲ್ಲ",
diff --git a/i18n/locales/mr-IN.json b/i18n/locales/mr-IN.json
index 57c36430e0..3f7a802b7d 100644
--- a/i18n/locales/mr-IN.json
+++ b/i18n/locales/mr-IN.json
@@ -194,7 +194,6 @@
"cancel": "रद्द करा",
"save": "जतन करा",
"edit": "संपादित करा",
- "error": "त्रुटी",
"view_on": {
"npm": "npm वर पहा",
"github": "GitHub वर पहा",
@@ -213,7 +212,9 @@
"expand": "विस्तृत करा"
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "आवडलेल्या पॅकेजेस लोड करता आल्या नाहीत.",
+ "likes_empty": "अद्याप आवडलेली कोणतीही पॅकेजेस नाहीत."
},
"package": {
"not_found": "पॅकेज सापडले नाही",
diff --git a/i18n/locales/nb-NO.json b/i18n/locales/nb-NO.json
index 1d6351607a..79f07386ba 100644
--- a/i18n/locales/nb-NO.json
+++ b/i18n/locales/nb-NO.json
@@ -322,7 +322,6 @@
"cancel": "Avbryt",
"save": "Lagre",
"edit": "Rediger",
- "error": "Feil",
"view_on": {
"npm": "vis på npm",
"github": "Vis på GitHub",
@@ -358,7 +357,9 @@
"message": "Det ser ikke ut som de bruker npmx ennå. Vil du fortelle dem om det?",
"share_button": "Del på Bluesky",
"compose_text": "Hei {'@'}{handle}! Har du sjekket ut npmx.dev ennå? Det er en leser for npm-registeret som er rask, moderne og åpen kildekode.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Kunne ikke laste likte pakker.",
+ "likes_empty": "Ingen likte pakker ennå."
},
"package": {
"not_found": "Pakke ikke funnet",
diff --git a/i18n/locales/ne-NP.json b/i18n/locales/ne-NP.json
index 60b7dec1f1..d76a7e7235 100644
--- a/i18n/locales/ne-NP.json
+++ b/i18n/locales/ne-NP.json
@@ -122,7 +122,9 @@
}
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "मनपरेका प्याकेजहरू लोड गर्न सकिएन।",
+ "likes_empty": "अहिले सम्म मनपरेका प्याकेजहरू छैनन्।"
},
"package": {
"not_found": "प्याकेज फेला परेन",
diff --git a/i18n/locales/nl.json b/i18n/locales/nl.json
index 85e5f13c1a..dc88b57c84 100644
--- a/i18n/locales/nl.json
+++ b/i18n/locales/nl.json
@@ -322,7 +322,6 @@
"cancel": "Annuleer",
"save": "Opslaan",
"edit": "Wijzigen",
- "error": "Fout",
"view_on": {
"npm": "Bekijk op npm",
"github": "Bekijk op GitHub",
@@ -358,7 +357,9 @@
"message": "Het lijkt erop dat ze npmx nog niet gebruiken. Wilt u ze hierop wijzen",
"share_button": "Deel op Bluesky",
"compose_text": "Hallo {'@'}{handle}! Hebt u npmx.dev al bekeken? Het is een browser voor het npm register die snel, modern en open source is.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Kon gelikete pakketten niet laden.",
+ "likes_empty": "Nog geen gelikete pakketten."
},
"package": {
"not_found": "Pakket niet gevonden",
diff --git a/i18n/locales/pl-PL.json b/i18n/locales/pl-PL.json
index f4acd249ed..f031246b0e 100644
--- a/i18n/locales/pl-PL.json
+++ b/i18n/locales/pl-PL.json
@@ -193,7 +193,6 @@
"cancel": "Anuluj",
"save": "Zapisz",
"edit": "Edytuj",
- "error": "Błąd",
"view_on": {
"npm": "zobacz na npm",
"github": "Zobacz w GitHub",
@@ -224,7 +223,9 @@
"message": "Wygląda na to, że jeszcze nie korzystają z npmx. Chcesz ich powiadomić?",
"share_button": "Podziel się na Bluesky",
"compose_text": "Hej {'@'}{handle}! Czy znasz już npmx.dev? To szybka, nowoczesna przeglądarka rejestru npm z otwartym kodem źródłowym.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Nie udało się wczytać polubionych pakietów.",
+ "likes_empty": "Brak polubionych pakietów."
},
"package": {
"not_found": "Nie znaleziono pakietu",
diff --git a/i18n/locales/pt-BR.json b/i18n/locales/pt-BR.json
index 935b61c5ec..419d408722 100644
--- a/i18n/locales/pt-BR.json
+++ b/i18n/locales/pt-BR.json
@@ -319,7 +319,6 @@
"cancel": "Cancelar",
"save": "Salvar",
"edit": "Editar",
- "error": "Erro",
"view_on": {
"npm": "Ver no npm",
"github": "Ver no GitHub",
@@ -355,7 +354,9 @@
"message": "Parece que eles ainda não estão usando o npmx. Quer contar a eles sobre isso?",
"share_button": "Compartilhar no Bluesky",
"compose_text": "Olá, {'@'}{handle}! Você já conferiu npmx.dev? É um navegador para o registro npm rápido, moderno e de código aberto.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Não foi possível carregar os pacotes curtidos.",
+ "likes_empty": "Nenhum pacote curtido ainda."
},
"package": {
"not_found": "Pacote não encontrado",
diff --git a/i18n/locales/ru-RU.json b/i18n/locales/ru-RU.json
index 66dc858a5a..4a9776d06c 100644
--- a/i18n/locales/ru-RU.json
+++ b/i18n/locales/ru-RU.json
@@ -324,7 +324,6 @@
"cancel": "Отменить",
"save": "Сохранить",
"edit": "Изменить",
- "error": "Ошибка",
"view_on": {
"npm": "Открыть на npm",
"github": "Открыть на GitHub",
@@ -360,7 +359,9 @@
"message": "Похоже, этот пользователь ещё не пользуется npmx. Хотите рассказать ему о проекте?",
"share_button": "Поделиться в Bluesky",
"compose_text": "Привет, {'@'}{handle}! Уже смотрел npmx.dev? Это быстрый, современный и open-source браузер для реестра npm.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Не удалось загрузить понравившиеся пакеты.",
+ "likes_empty": "Понравившихся пакетов пока нет."
},
"package": {
"not_found": "Пакет не найден",
diff --git a/i18n/locales/sr-Latn-RS.json b/i18n/locales/sr-Latn-RS.json
index c015089b8c..1460ee31af 100644
--- a/i18n/locales/sr-Latn-RS.json
+++ b/i18n/locales/sr-Latn-RS.json
@@ -211,7 +211,6 @@
"cancel": "Otkažite",
"save": "Sačuvajte",
"edit": "Uredite",
- "error": "Greška",
"view_on": {
"npm": "pogledajte na npm-u",
"github": "Pogledajte na GitHub-u",
@@ -244,7 +243,9 @@
"message": "Izgleda da još uvek ne koriste npmx. Želite li da im kažete nešto više o tome?",
"share_button": "Podelite na Bluesky-u",
"compose_text": "Hej {'@'}{handle}! Da li ste već pogledali npmx.dev? To je pretraživač za npm registar koji je brz, moderan i otvorenog koda.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Nije moguće učitati omiljene pakete.",
+ "likes_empty": "Još nema omiljenih paketa."
},
"package": {
"not_found": "Paket nije pronađen",
diff --git a/i18n/locales/ta-IN.json b/i18n/locales/ta-IN.json
index e8b657110c..fd5231ece7 100644
--- a/i18n/locales/ta-IN.json
+++ b/i18n/locales/ta-IN.json
@@ -154,7 +154,9 @@
}
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "விரும்பிய தொகுப்புகளை ஏற்ற முடியவில்லை.",
+ "likes_empty": "இன்னும் விரும்பிய தொகுப்புகள் இல்லை."
},
"package": {
"not_found": "தொகுப்பு கிடைக்கவில்லை",
diff --git a/i18n/locales/te-IN.json b/i18n/locales/te-IN.json
index 7e92347d2e..5686a70a35 100644
--- a/i18n/locales/te-IN.json
+++ b/i18n/locales/te-IN.json
@@ -122,7 +122,9 @@
}
},
"profile": {
- "invite": {}
+ "invite": {},
+ "likes_error": "Could not load liked packages.",
+ "likes_empty": "No liked packages yet."
},
"package": {
"not_found": "ప్యాకేజ్ కనుగొనబడలేదు",
diff --git a/i18n/locales/tr-TR.json b/i18n/locales/tr-TR.json
index 963fed0027..630f8b6e31 100644
--- a/i18n/locales/tr-TR.json
+++ b/i18n/locales/tr-TR.json
@@ -193,7 +193,6 @@
"cancel": "İptal",
"save": "Kaydet",
"edit": "Düzenle",
- "error": "Hata",
"view_on": {
"npm": "npm'de görüntüle",
"github": "GitHub'da görüntüle"
@@ -214,7 +213,9 @@
"message": "npmx'i deneyin - npm için daha iyi bir paket tarayıcısı",
"share_button": "Paylaş",
"compose_text": "npmx'i deneyin - npm için daha iyi bir paket tarayıcısı: {url}"
- }
+ },
+ "likes_error": "Beğenilen paketler yüklenemedi.",
+ "likes_empty": "Henüz beğenilen paket yok."
},
"package": {
"not_found": "Paket Bulunamadı",
diff --git a/i18n/locales/uk-UA.json b/i18n/locales/uk-UA.json
index 336c409e1b..1cc61049bb 100644
--- a/i18n/locales/uk-UA.json
+++ b/i18n/locales/uk-UA.json
@@ -211,7 +211,6 @@
"cancel": "Скасувати",
"save": "Зберегти",
"edit": "Редагувати",
- "error": "Помилка",
"view_on": {
"npm": "Переглянути на npm",
"github": "Переглянути на GitHub",
@@ -244,7 +243,9 @@
"message": "Схоже, вони ще не користуються npmx. Хочете розповісти їм про нього?",
"share_button": "Поділитися в Bluesky",
"compose_text": "Привіт, {'@'}{handle}! Ти вже перевірив npmx.dev? Це швидкий сучасний браузер для реєстру npm з відкритим кодом.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Не вдалося завантажити вподобані пакети.",
+ "likes_empty": "Вподобаних пакетів поки немає."
},
"package": {
"not_found": "Пакет не знайдено",
diff --git a/i18n/locales/vi-VN.json b/i18n/locales/vi-VN.json
index d9a5698fa2..461d32fb43 100644
--- a/i18n/locales/vi-VN.json
+++ b/i18n/locales/vi-VN.json
@@ -211,7 +211,6 @@
"cancel": "Hủy",
"save": "Lưu",
"edit": "Chỉnh sửa",
- "error": "Lỗi",
"view_on": {
"npm": "xem trên npm",
"github": "Xem trên GitHub",
@@ -244,7 +243,9 @@
"message": "Có vẻ họ chưa dùng npmx. Bạn có muốn giới thiệu cho họ không?",
"share_button": "Chia sẻ trên Bluesky",
"compose_text": "Chào {'@'}{handle}! Bạn đã thử npmx.dev chưa? Đây là trình duyệt cho npm registry, nhanh, hiện đại và mã nguồn mở.\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "Không thể tải các package đã thích.",
+ "likes_empty": "Chưa có package nào được thích."
},
"package": {
"not_found": "Không tìm thấy gói",
diff --git a/i18n/locales/zh-CN.json b/i18n/locales/zh-CN.json
index 559283c623..83394d1486 100644
--- a/i18n/locales/zh-CN.json
+++ b/i18n/locales/zh-CN.json
@@ -323,7 +323,6 @@
"cancel": "取消",
"save": "保存",
"edit": "编辑",
- "error": "加载出错",
"view_on": {
"npm": "在 npm 上查看",
"github": "在 GitHub 上查看",
@@ -359,7 +358,9 @@
"message": "看起来他们还没有使用 npmx,去邀请一下?",
"share_button": "分享到 Bluesky",
"compose_text": "嗨 {'@'}{handle}!您用过 npmx.dev 吗?它是一个快速、现代且开源的 npm registry 浏览器。\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "无法加载喜欢的包。",
+ "likes_empty": "还没有喜欢的包。"
},
"package": {
"not_found": "未找到包",
diff --git a/i18n/locales/zh-TW.json b/i18n/locales/zh-TW.json
index 623e3955fe..9ed82a60d4 100644
--- a/i18n/locales/zh-TW.json
+++ b/i18n/locales/zh-TW.json
@@ -322,7 +322,6 @@
"cancel": "取消",
"save": "儲存",
"edit": "編輯",
- "error": "錯誤",
"view_on": {
"npm": "在 npm 上檢視",
"github": "在 GitHub 上檢視",
@@ -357,7 +356,9 @@
"message": "看起來對方還沒在用 npmx。要不要跟他們分享一下?",
"share_button": "分享到 Bluesky",
"compose_text": "Hey {'@'}{handle}!你用過 npmx.dev 了嗎?它是 npm Registry 的瀏覽器,快速、現代,而且是開源的。\nhttps://npmx.dev"
- }
+ },
+ "likes_error": "無法載入喜歡的套件。",
+ "likes_empty": "目前尚無喜歡的套件。"
},
"package": {
"not_found": "找不到套件",
diff --git a/i18n/schema.json b/i18n/schema.json
index f06f2f53a8..2bcf4101d2 100644
--- a/i18n/schema.json
+++ b/i18n/schema.json
@@ -985,9 +985,6 @@
"edit": {
"type": "string"
},
- "error": {
- "type": "string"
- },
"view_on": {
"type": "object",
"properties": {
@@ -1069,6 +1066,12 @@
"likes": {
"type": "string"
},
+ "likes_error": {
+ "type": "string"
+ },
+ "likes_empty": {
+ "type": "string"
+ },
"seo_title": {
"type": "string"
},
@@ -1095,6 +1098,42 @@
}
},
"additionalProperties": false
+ },
+ "linked_accounts": {
+ "type": "object",
+ "properties": {
+ "status": {
+ "type": "object",
+ "properties": {
+ "verified": {
+ "type": "string"
+ },
+ "unverified": {
+ "type": "string"
+ },
+ "stale": {
+ "type": "string"
+ },
+ "failed": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
+ },
+ "title": {
+ "type": "string"
+ },
+ "verified_summary": {
+ "type": "string"
+ },
+ "legend_aria_label": {
+ "type": "string"
+ },
+ "empty": {
+ "type": "string"
+ }
+ },
+ "additionalProperties": false
}
},
"additionalProperties": false
diff --git a/package.json b/package.json
index a69d2bd921..019d5f6c3f 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"@iconify-json/svg-spinners": "1.2.4",
"@iconify-json/vscode-icons": "1.2.45",
"@intlify/shared": "11.3.0",
+ "@keytrace/claims": "1.4.0",
"@lunariajs/core": "https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935",
"@napi-rs/canvas": "0.1.97",
"@nuxt/a11y": "1.0.0-alpha.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 673b6bcedb..17c743729e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -92,6 +92,9 @@ importers:
'@intlify/shared':
specifier: 11.3.0
version: 11.3.0
+ '@keytrace/claims':
+ specifier: 1.4.0
+ version: 1.4.0
'@lunariajs/core':
specifier: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935
version: https://pkg.pr.new/lunariajs/lunaria/@lunariajs/core@904b935
@@ -2091,6 +2094,10 @@ packages:
'@jsr/std__path@1.1.4':
resolution: {integrity: sha512-SK4u9H6NVTfolhPdlvdYXfNFefy1W04AEHWJydryYbk+xqzNiVmr5o7TLJLJFqwHXuwMRhwrn+mcYeUfS0YFaA==, tarball: https://npm.jsr.io/~/11/@jsr/std__path/1.1.4.tgz}
+ '@keytrace/claims@1.4.0':
+ resolution: {integrity: sha512-XeojwXFFnvhPsDI7f99e2KCvk8gDC5ztKxRBplB+Zx5OmfcaBovX+DZJODZsxz3b1r4ZITBDOjZD8gftgpAZyw==}
+ engines: {node: '>=18'}
+
'@kwsites/file-exists@1.1.1':
resolution: {integrity: sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==}
@@ -13325,6 +13332,8 @@ snapshots:
dependencies:
'@jsr/std__internal': 1.0.12
+ '@keytrace/claims@1.4.0': {}
+
'@kwsites/file-exists@1.1.1':
dependencies:
debug: 4.4.3
diff --git a/server/api/keytrace/[domain].ts b/server/api/keytrace/[domain].ts
new file mode 100644
index 0000000000..3fe34ded03
--- /dev/null
+++ b/server/api/keytrace/[domain].ts
@@ -0,0 +1,177 @@
+import { getClaimsForHandle, type ClaimVerificationResult } from '@keytrace/claims'
+import type { KeytraceProofMethod, KeytraceResponse } from '#shared/types/keytrace'
+import {
+ getOptionalClaimField,
+ mapKeytraceVerificationStatus,
+ mapClaimTypeToPlatform,
+} from '#server/utils/keytrace'
+import { toProxiedImageUrl } from '#server/utils/image-proxy'
+import { getSafeHttpUrl } from '#shared/utils/url'
+
+function domainToDisplayName(domain: string): string {
+ const firstSegment = domain.split('.')[0] || domain
+ return firstSegment.charAt(0).toUpperCase() + firstSegment.slice(1)
+}
+
+function buildDefaultAvatarUrl(domain: string, imageProxySecret: string): string {
+ return buildSeededAvatarUrl(domain, imageProxySecret)
+}
+
+function buildSeededAvatarUrl(seed: string, imageProxySecret: string): string {
+ const rawFallbackAvatar = `https://api.dicebear.com/9.x/initials/svg?seed=${encodeURIComponent(seed)}`
+ return toProxiedImageUrl(rawFallbackAvatar, imageProxySecret)
+}
+
+function toProfileAvatarUrl(
+ rawAvatarUrl: string | undefined,
+ domain: string,
+ imageProxySecret: string,
+): string {
+ const safeAvatarUrl = getSafeHttpUrl(rawAvatarUrl)
+ if (!safeAvatarUrl) {
+ return buildDefaultAvatarUrl(domain, imageProxySecret)
+ }
+
+ return toProxiedImageUrl(safeAvatarUrl, imageProxySecret)
+}
+
+function toAccountAvatarUrl(
+ rawAvatarUrl: string | undefined,
+ imageProxySecret: string,
+ fallbackSeed: string,
+): string {
+ const safeAvatarUrl = getSafeHttpUrl(rawAvatarUrl)
+ if (!safeAvatarUrl) {
+ return buildSeededAvatarUrl(fallbackSeed, imageProxySecret)
+ }
+
+ return toProxiedImageUrl(safeAvatarUrl, imageProxySecret)
+}
+
+function buildFallbackProfile(domain: string, imageProxySecret: string): KeytraceResponse {
+ return {
+ profile: {
+ name: `${domainToDisplayName(domain)} Developer`,
+ avatar: buildDefaultAvatarUrl(domain, imageProxySecret),
+ description: `No Keytrace claims found for ${domain}.`,
+ },
+ accounts: [],
+ }
+}
+
+function buildServiceUnavailableProfile(
+ domain: string,
+ imageProxySecret: string,
+): KeytraceResponse {
+ return {
+ profile: {
+ name: `${domainToDisplayName(domain)} Developer`,
+ avatar: buildDefaultAvatarUrl(domain, imageProxySecret),
+ description: 'Keytrace is temporarily unavailable. Please try again shortly.',
+ },
+ accounts: [],
+ }
+}
+
+const allowedProofMethods = new Set
([
+ 'dns',
+ 'github',
+ 'npm',
+ 'mastodon',
+ 'pgp',
+ 'other',
+])
+
+function mapProofMethod(type: string): KeytraceProofMethod {
+ const mappedType = mapClaimTypeToPlatform(type)
+ return allowedProofMethods.has(mappedType as KeytraceProofMethod)
+ ? (mappedType as KeytraceProofMethod)
+ : 'other'
+}
+
+// Convert Keytrace claims to our account format
+function mapClaimsToAccounts(
+ claims: ClaimVerificationResult[],
+ imageProxySecret: string,
+): KeytraceResponse['accounts'] {
+ return claims.map(claim => ({
+ platform: mapClaimTypeToPlatform(claim.type),
+ username: claim.identity.subject,
+ displayName: claim.identity.displayName || claim.identity.subject,
+ avatar: toAccountAvatarUrl(claim.identity.avatarUrl, imageProxySecret, claim.identity.subject),
+ url: getSafeHttpUrl(claim.identity.profileUrl) || undefined,
+ status: mapKeytraceVerificationStatus(claim),
+ proofMethod: mapProofMethod(claim.type),
+ addedAt: claim.claim.createdAt,
+ lastCheckedAt:
+ getOptionalClaimField(claim, 'lastVerifiedAt') ||
+ claim.claim.retractedAt ||
+ claim.claim.createdAt,
+ failureReason: claim.error || undefined,
+ }))
+}
+
+type KeytraceFetchResult =
+ | { kind: 'success'; data: KeytraceResponse }
+ | { kind: 'no-claims' }
+ | { kind: 'error'; error: unknown }
+
+// Fetch real Keytrace profile data
+async function fetchKeytraceProfile(
+ domain: string,
+ imageProxySecret: string,
+): Promise {
+ try {
+ const result = await getClaimsForHandle(domain)
+
+ if (!result.claims || result.claims.length === 0) {
+ return { kind: 'no-claims' }
+ }
+
+ // Build profile from first claim's identity (they all belong to the same DID)
+ const firstClaim = result.claims[0]
+ if (!firstClaim) {
+ return { kind: 'no-claims' }
+ }
+
+ return {
+ kind: 'success',
+ data: {
+ profile: {
+ name: firstClaim.identity.displayName || domainToDisplayName(domain),
+ avatar: toProfileAvatarUrl(firstClaim.identity.avatarUrl, domain, imageProxySecret),
+ description: `Identity profile for ${domain}`,
+ },
+ accounts: mapClaimsToAccounts(result.claims, imageProxySecret),
+ },
+ }
+ } catch (error) {
+ console.error('Failed to fetch Keytrace profile:', error)
+ return { kind: 'error', error }
+ }
+}
+
+export default defineEventHandler(async event => {
+ const { imageProxySecret } = useRuntimeConfig(event)
+ const domain = getRouterParam(event, 'domain')?.trim().toLowerCase()
+ if (!domain) {
+ throw createError({
+ statusCode: 400,
+ message: 'Domain is required',
+ })
+ }
+
+ // Try to fetch real Keytrace data
+ const keytraceData = await fetchKeytraceProfile(domain, imageProxySecret)
+
+ if (keytraceData.kind === 'success') {
+ return keytraceData.data
+ }
+
+ if (keytraceData.kind === 'no-claims') {
+ return buildFallbackProfile(domain, imageProxySecret)
+ }
+
+ // If Keytrace is unavailable and mock mode isn't allowed, return a neutral profile.
+ return buildServiceUnavailableProfile(domain, imageProxySecret)
+})
diff --git a/server/api/keytrace/reverify.post.ts b/server/api/keytrace/reverify.post.ts
new file mode 100644
index 0000000000..b46a4fca08
--- /dev/null
+++ b/server/api/keytrace/reverify.post.ts
@@ -0,0 +1,93 @@
+import { getClaimsForHandle, type ClaimVerificationResult } from '@keytrace/claims'
+import type { KeytraceReverifyRequest, KeytraceReverifyResponse } from '#shared/types/keytrace'
+import {
+ getOptionalClaimField,
+ mapPlatformToClaimType,
+ mapKeytraceVerificationStatus,
+} from '#server/utils/keytrace'
+
+function mapReverifyStatus(claim: ClaimVerificationResult): KeytraceReverifyResponse['status'] {
+ return mapKeytraceVerificationStatus(claim)
+}
+
+function getReverifyLastCheckedAt(claim: ClaimVerificationResult): string {
+ return getOptionalClaimField(claim, 'lastVerifiedAt') || claim.claim.createdAt
+}
+
+function matchesAccount(
+ claim: ClaimVerificationResult,
+ claimType: string,
+ username: string,
+ url?: string,
+): boolean {
+ if (claim.type !== claimType) {
+ return false
+ }
+
+ const normalizedSubject = claim.identity.subject.toLowerCase()
+ const normalizedUsername = username.toLowerCase()
+ if (normalizedSubject === normalizedUsername) {
+ return true
+ }
+
+ const profileUrl = (claim.identity.profileUrl || '').toLowerCase()
+ const normalizedUrl = (url || '').toLowerCase()
+ if (normalizedUrl && profileUrl && profileUrl === normalizedUrl) {
+ return true
+ }
+
+ return false
+}
+
+export default defineEventHandler(async event => {
+ const body = await readBody(event)
+
+ const identity = body?.identity?.trim().toLowerCase()
+ const platform = body?.platform?.trim().toLowerCase()
+ const username = body?.username?.trim()
+
+ if (!identity || !platform || !username) {
+ throw createError({
+ statusCode: 400,
+ message: 'identity, platform and username are required',
+ })
+ }
+
+ try {
+ const result = await getClaimsForHandle(identity)
+ const claimType = mapPlatformToClaimType(platform)
+ const matchedClaim = result.claims.find(claim =>
+ matchesAccount(claim, claimType, username, body?.url),
+ )
+
+ if (!matchedClaim) {
+ return {
+ status: 'unverified',
+ lastCheckedAt: new Date().toISOString(),
+ failureReason: 'No matching Keytrace claim found for this account.',
+ }
+ }
+
+ const response: KeytraceReverifyResponse = {
+ status: mapReverifyStatus(matchedClaim),
+ lastCheckedAt: getReverifyLastCheckedAt(matchedClaim),
+ failureReason: matchedClaim.error || undefined,
+ }
+
+ if (matchedClaim.claim.retractedAt) {
+ response.status = 'unverified'
+ response.retractedAt = matchedClaim.claim.retractedAt
+ response.failureReason = response.failureReason || 'Keytrace claim was retracted.'
+ }
+
+ return response
+ } catch (error) {
+ console.error('[keytrace] reverify failed', error)
+
+ return {
+ status: 'unverified',
+ lastCheckedAt: new Date().toISOString(),
+ failureReason: 'Keytrace is temporarily unavailable. Please try again shortly.',
+ }
+ }
+})
diff --git a/server/utils/keytrace.ts b/server/utils/keytrace.ts
new file mode 100644
index 0000000000..4448994538
--- /dev/null
+++ b/server/utils/keytrace.ts
@@ -0,0 +1,93 @@
+import type { ClaimVerificationResult } from '@keytrace/claims'
+import type { KeytraceVerificationStatus } from '#shared/types/keytrace'
+
+const STALE_THRESHOLD_DAYS = 30
+
+const platformToClaimTypeMap: Record = {
+ github: 'github',
+ dns: 'dns',
+ mastodon: 'activitypub',
+ bluesky: 'bsky',
+ npm: 'npm',
+ tangled: 'tangled',
+ pgp: 'pgp',
+ twitter: 'twitter',
+ linkedin: 'linkedin',
+ instagram: 'instagram',
+ reddit: 'reddit',
+ hackernews: 'hackernews',
+ orcid: 'orcid',
+ itchio: 'itchio',
+ discord: 'discord',
+ steam: 'steam',
+}
+
+const claimTypeToPlatformMap: Record = {
+ github: 'github',
+ dns: 'dns',
+ activitypub: 'mastodon',
+ bsky: 'bluesky',
+ npm: 'npm',
+ tangled: 'tangled',
+ pgp: 'pgp',
+ twitter: 'twitter',
+ linkedin: 'linkedin',
+ instagram: 'instagram',
+ reddit: 'reddit',
+ hackernews: 'hackernews',
+ orcid: 'orcid',
+ itchio: 'itchio',
+ discord: 'discord',
+ steam: 'steam',
+}
+
+export function mapPlatformToClaimType(platform: string): string {
+ return platformToClaimTypeMap[platform] || platform
+}
+
+export function mapClaimTypeToPlatform(type: string): string {
+ return claimTypeToPlatformMap[type] || type
+}
+
+export function getOptionalClaimField(
+ claim: ClaimVerificationResult,
+ key: string,
+): string | undefined {
+ const rawClaim = claim.claim as unknown as Record
+ const value = rawClaim[key]
+ return typeof value === 'string' ? value : undefined
+}
+
+export function isStaleIsoDate(isoDate: string | undefined): boolean {
+ if (!isoDate) {
+ return false
+ }
+
+ const timestamp = Date.parse(isoDate)
+ if (Number.isNaN(timestamp)) {
+ return false
+ }
+
+ return Date.now() - timestamp > STALE_THRESHOLD_DAYS * 24 * 60 * 60 * 1000
+}
+
+export function mapKeytraceVerificationStatus(
+ claim: ClaimVerificationResult,
+): KeytraceVerificationStatus {
+ const rawStatus = getOptionalClaimField(claim, 'status')
+ const lastVerifiedAt = getOptionalClaimField(claim, 'lastVerifiedAt')
+
+ if (rawStatus === 'failed' || rawStatus === 'retracted') {
+ return 'failed'
+ }
+
+ if (rawStatus === 'verified' || claim.verified) {
+ return isStaleIsoDate(lastVerifiedAt) ? 'stale' : 'verified'
+ }
+
+ if (claim.error) {
+ return 'failed'
+ }
+
+ return 'unverified'
+}
diff --git a/shared/types/keytrace.ts b/shared/types/keytrace.ts
new file mode 100644
index 0000000000..dbc04d2fdf
--- /dev/null
+++ b/shared/types/keytrace.ts
@@ -0,0 +1,42 @@
+export type KeytraceProfile = {
+ name: string
+ avatar: string
+ banner?: string
+ description: string
+}
+
+export type KeytraceVerificationStatus = 'verified' | 'unverified' | 'stale' | 'failed'
+
+export type KeytraceProofMethod = 'dns' | 'github' | 'npm' | 'mastodon' | 'pgp' | 'other'
+
+export type KeytraceAccount = {
+ platform: string
+ username: string
+ displayName?: string
+ avatar?: string
+ url?: string
+ status: KeytraceVerificationStatus
+ proofMethod: KeytraceProofMethod
+ addedAt: string
+ lastCheckedAt: string
+ failureReason?: string
+}
+
+export type KeytraceResponse = {
+ profile: KeytraceProfile
+ accounts: KeytraceAccount[]
+}
+
+export type KeytraceReverifyRequest = {
+ identity: string
+ platform: string
+ username: string
+ url?: string
+}
+
+export type KeytraceReverifyResponse = {
+ status: KeytraceVerificationStatus
+ lastCheckedAt: string
+ failureReason?: string
+ retractedAt?: string
+}
diff --git a/test/nuxt/a11y.spec.ts b/test/nuxt/a11y.spec.ts
index aa16620760..d483dc8996 100644
--- a/test/nuxt/a11y.spec.ts
+++ b/test/nuxt/a11y.spec.ts
@@ -271,6 +271,8 @@ import FacetScatterChart from '~/components/Compare/FacetScatterChart.vue'
import PackageLikeCard from '~/components/Package/LikeCard.vue'
import SizeIncrease from '~/components/Package/SizeIncrease.vue'
import Likes from '~/components/Package/Likes.vue'
+import AccountItem from '~/components/AccountItem.vue'
+import LinkedAccounts from '~/components/LinkedAccounts.vue'
import type { VueUiXyDatasetItem } from 'vue-data-ui'
describe('component accessibility audits', () => {
@@ -643,7 +645,11 @@ describe('component accessibility audits', () => {
it('should have no accessibility violations as secondary button', async () => {
const component = await mountSuspended(LinkBase, {
- props: { to: 'http://example.com', disabled: true, variant: 'button-secondary' },
+ props: {
+ to: 'http://example.com',
+ disabled: true,
+ variant: 'button-secondary',
+ },
slots: { default: 'Button link content' },
})
const results = await runAxe(component)
@@ -652,7 +658,11 @@ describe('component accessibility audits', () => {
it('should have no accessibility violations as primary button', async () => {
const component = await mountSuspended(LinkBase, {
- props: { to: 'http://example.com', disabled: true, variant: 'button-primary' },
+ props: {
+ to: 'http://example.com',
+ disabled: true,
+ variant: 'button-primary',
+ },
slots: { default: 'Button link content' },
})
const results = await runAxe(component)
@@ -904,6 +914,52 @@ describe('component accessibility audits', () => {
})
})
+ describe('LinkedAccounts', () => {
+ it('should have no accessibility violations with account list', async () => {
+ const component = await mountSuspended(LinkedAccounts, {
+ props: {
+ identity: 'npmx.dev',
+ accounts: [
+ {
+ platform: 'github',
+ username: 'npmx-dev',
+ displayName: 'npmx-dev',
+ status: 'verified',
+ proofMethod: 'github',
+ addedAt: '2026-04-01T10:00:00.000Z',
+ lastCheckedAt: '2026-04-21T10:00:00.000Z',
+ url: 'https://github.com/npmx-dev',
+ },
+ ],
+ },
+ })
+ const results = await runAxe(component)
+ expect(results.violations).toEqual([])
+ })
+ })
+
+ describe('AccountItem', () => {
+ it('should have no accessibility violations', async () => {
+ const component = await mountSuspended(AccountItem, {
+ props: {
+ identity: 'npmx.dev',
+ account: {
+ platform: 'github',
+ username: 'npmx-dev',
+ displayName: 'npmx-dev',
+ status: 'verified',
+ proofMethod: 'github',
+ addedAt: '2026-04-01T10:00:00.000Z',
+ lastCheckedAt: '2026-04-21T10:00:00.000Z',
+ url: 'https://github.com/npmx-dev',
+ },
+ },
+ })
+ const results = await runAxe(component)
+ expect(results.violations).toEqual([])
+ })
+ })
+
describe('PackageHeader', () => {
it('should have no accessibility violations', async () => {
const component = await mountSuspended(PackageHeader, {
@@ -3725,7 +3781,14 @@ describe('component accessibility audits', () => {
files: {
added: [{ path: 'new.ts', type: 'added' as const, newSize: 100 }],
removed: [{ path: 'old.ts', type: 'removed' as const, oldSize: 50 }],
- modified: [{ path: 'changed.ts', type: 'modified' as const, oldSize: 200, newSize: 250 }],
+ modified: [
+ {
+ path: 'changed.ts',
+ type: 'modified' as const,
+ oldSize: 200,
+ newSize: 250,
+ },
+ ],
},
dependencyChanges: [
{
@@ -3750,7 +3813,12 @@ describe('component accessibility audits', () => {
const mockAllChanges = [
{ path: 'new.ts', type: 'added' as const, newSize: 100 },
{ path: 'old.ts', type: 'removed' as const, oldSize: 50 },
- { path: 'changed.ts', type: 'modified' as const, oldSize: 200, newSize: 250 },
+ {
+ path: 'changed.ts',
+ type: 'modified' as const,
+ oldSize: 200,
+ newSize: 250,
+ },
]
const mockGroupedDeps = new Map([
@@ -3849,7 +3917,14 @@ describe('component accessibility audits', () => {
files: {
added: [{ path: 'new.ts', type: 'added' as const, newSize: 100 }],
removed: [],
- modified: [{ path: 'changed.ts', type: 'modified' as const, oldSize: 200, newSize: 250 }],
+ modified: [
+ {
+ path: 'changed.ts',
+ type: 'modified' as const,
+ oldSize: 200,
+ newSize: 250,
+ },
+ ],
},
dependencyChanges: [],
stats: {
@@ -3864,7 +3939,12 @@ describe('component accessibility audits', () => {
const mockAllChanges = [
{ path: 'new.ts', type: 'added' as const, newSize: 100 },
- { path: 'changed.ts', type: 'modified' as const, oldSize: 200, newSize: 250 },
+ {
+ path: 'changed.ts',
+ type: 'modified' as const,
+ oldSize: 200,
+ newSize: 250,
+ },
]
it('should have no accessibility violations when closed', async () => {