diff --git a/.env.example b/.env.example
index e0131b2f69..2cddc9d147 100644
--- a/.env.example
+++ b/.env.example
@@ -2470,10 +2470,10 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# ═══════════════════════════════════════════════════════════════════════════════
# Optional add-on (feature flag RADAR_ENABLED, default off — see feature flag
# settings, not an env var) that overlays a signed, freshly-curated free-model
-# catalog on top of the release baseline. Both variables below are optional and
-# only needed to point the client at a self-hosted/forked feed instead of the
-# default OmniRoute Radar feed. Used by: src/lib/radar/sync.ts,
-# src/lib/radar/pinnedKeys.ts.
+# catalog on top of the release baseline. All four variables below are optional
+# and only needed to point the client at a self-hosted/forked feed or
+# supporter-key flow instead of the default OmniRoute Radar service. Used by:
+# src/lib/radar/sync.ts, src/lib/radar/pinnedKeys.ts, src/lib/radar/links.ts.
# Base URL of the Radar feed service. Overrides the built-in default so forks
# and self-hosters can point at their own signed feed.
@@ -2483,3 +2483,12 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# signature, replacing the pinned default key. Required when self-hosting a
# feed signed with a different key pair.
# RADAR_FEED_PUBKEY=
+
+# URL the dashboard's "I'm a contributor" button opens (GitHub OAuth
+# supporter-key claim flow). No pricing/value lives in this repo — only the
+# link.
+# RADAR_CONTRIBUTOR_CLAIM_URL=https://radar.omniroute.online/auth/github
+
+# URL the dashboard's "Support the project" button opens (payment/plans
+# page). No pricing/value lives in this repo — only the link.
+# RADAR_SUPPORTER_PLANS_URL=https://radar.omniroute.online/planos
diff --git a/docs/frameworks/RADAR.md b/docs/frameworks/RADAR.md
index e7618cd6c2..5f4023903c 100644
--- a/docs/frameworks/RADAR.md
+++ b/docs/frameworks/RADAR.md
@@ -82,6 +82,43 @@ that lets the feed service decide which tier to serve (see
---
+## Getting a supporter key
+
+The activation screen (`/dashboard/radar`) links out to two flows for **obtaining** a
+supporter key. The OSS repo itself never issues one, never runs payment code, and
+**never states a price** — pricing is decided and displayed entirely on the
+destination pages, not in this repo (spec decision D14).
+
+- **"I'm a contributor"** — opens `RADAR_CONTRIBUTOR_CLAIM_URL` (default
+ `https://radar.omniroute.online/auth/github`), a GitHub OAuth claim flow hosted on
+ the private radar server. It verifies the visitor's GitHub account and grants a
+ supporter key to anyone with 5+ merged pull requests or a top-100 contributor spot
+ on the repo.
+- **"Support the project"** — opens `RADAR_SUPPORTER_PLANS_URL` (default
+ `https://radar.omniroute.online/planos`), the payment/plans page.
+
+Both URLs are resolved server-side (`src/lib/radar/links.ts`, same env-override
+pattern as `RADAR_FEED_URL`) and relayed to the dashboard through the existing
+`GET /api/radar/settings` response (`contributorClaimUrl`, `supporterPlansUrl`) — the
+client component never reads `process.env` itself.
+
+| Var | Purpose |
+| -------------------------------- | ---------------------------------------------------------------------------------------------- |
+| `RADAR_CONTRIBUTOR_CLAIM_URL` | Overrides the contributor-claim URL (default `https://radar.omniroute.online/auth/github`). |
+| `RADAR_SUPPORTER_PLANS_URL` | Overrides the supporter-plans URL (default `https://radar.omniroute.online/planos`). |
+
+Once a visitor has a key (`omr_` + 40 hex chars), it is set with `POST
+/api/radar/settings` (`{ supporterKey }`) — the same endpoint documented under
+[Data sync](#data-sync-is-a-separate-opt-in--the-privacy-promise) above.
+
+**Known gap:** the dashboard activation screen does not yet have a dedicated
+key-paste input — pasting a key today requires calling `POST /api/radar/settings`
+directly (curl, a script, or a future UI). This release only adds the two claim/plans
+buttons; the API already accepts and masks the key, but no `` for it exists in
+`src/app/(dashboard)/dashboard/radar/page.tsx` yet.
+
+---
+
## Security model
### Ed25519 signature over exact bytes
diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md
index 7e93c508fe..90c8fb7590 100644
--- a/docs/reference/ENVIRONMENT.md
+++ b/docs/reference/ENVIRONMENT.md
@@ -1275,14 +1275,17 @@ that should be able to run the docs translator.
Optional add-on gated by the RADAR_ENABLED feature flag (default off — a feature
flag toggled via Settings/DB, not an env var; see
[docs/frameworks/RADAR.md](../frameworks/RADAR.md#flag-radar_enabled-default-off)).
-Both variables below are optional overrides used only to point the client at a
-self-hosted or forked feed instead of the default OmniRoute Radar feed. See
-[docs/frameworks/RADAR.md](../frameworks/RADAR.md) for the full module doc.
+The four variables below are optional overrides used only to point the client at a
+self-hosted or forked feed / supporter-key flow instead of the default OmniRoute
+Radar service. See [docs/frameworks/RADAR.md](../frameworks/RADAR.md) for the full
+module doc.
-| Variable | Default | Source File | Description |
-| -------------------- | ------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------ |
-| `RADAR_FEED_URL` | `https://radar.omniroute.online` | `src/lib/radar/sync.ts` | Base URL of the Radar feed service. Override to point at a self-hosted or forked feed. |
-| `RADAR_FEED_PUBKEY` | _(pinned default key)_ | `src/lib/radar/pinnedKeys.ts` | Ed25519 public key (base64-DER SPKI or PEM) used to verify feed signatures from a custom feed. |
+| Variable | Default | Source File | Description |
+| -------------------------------- | --------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------ |
+| `RADAR_FEED_URL` | `https://radar.omniroute.online` | `src/lib/radar/sync.ts` | Base URL of the Radar feed service. Override to point at a self-hosted or forked feed. |
+| `RADAR_FEED_PUBKEY` | _(pinned default key)_ | `src/lib/radar/pinnedKeys.ts` | Ed25519 public key (base64-DER SPKI or PEM) used to verify feed signatures from a custom feed. |
+| `RADAR_CONTRIBUTOR_CLAIM_URL` | `https://radar.omniroute.online/auth/github` | `src/lib/radar/links.ts` | URL the "I'm a contributor" dashboard button opens (GitHub OAuth supporter-key claim flow). |
+| `RADAR_SUPPORTER_PLANS_URL` | `https://radar.omniroute.online/planos` | `src/lib/radar/links.ts` | URL the "Support the project" dashboard button opens (payment/plans page). |
---
diff --git a/src/app/(dashboard)/dashboard/radar/page.tsx b/src/app/(dashboard)/dashboard/radar/page.tsx
index bbf01666f3..c30ed0b31b 100644
--- a/src/app/(dashboard)/dashboard/radar/page.tsx
+++ b/src/app/(dashboard)/dashboard/radar/page.tsx
@@ -125,6 +125,12 @@ export default function RadarPage() {
campaigns: [],
tier: null,
});
+ // F4/T7 — "get a supporter key" outbound links, relayed by
+ // GET /api/radar/settings (server-resolved, see src/lib/radar/links.ts).
+ // Never hardcoded here: this component must never embed an external URL
+ // literal (see tests/unit/radar-referrals-page-tab.test.ts).
+ const [contributorClaimUrl, setContributorClaimUrl] = useState(null);
+ const [supporterPlansUrl, setSupporterPlansUrl] = useState(null);
// Fetch catalog
const fetchCatalog = useCallback(async () => {
@@ -183,6 +189,14 @@ export default function RadarPage() {
if (!settingsRes.ok) throw new Error(`HTTP ${settingsRes.status}`);
const settingsData = await settingsRes.json();
setOptIn(settingsData.optIn === true);
+ // F4/T7 — best-effort: keep whatever we already had if the field is
+ // absent (older cached response shape), never fall back to a literal.
+ if (typeof settingsData.contributorClaimUrl === "string") {
+ setContributorClaimUrl(settingsData.contributorClaimUrl);
+ }
+ if (typeof settingsData.supporterPlansUrl === "string") {
+ setSupporterPlansUrl(settingsData.supporterPlansUrl);
+ }
if (settingsData.optIn === true) {
// Already opted in — load the catalog now so the populated/empty
@@ -352,6 +366,35 @@ export default function RadarPage() {
>
{activating ? t("activating") : t("activateButton")}
+
+ {/* F4/T7 — "get a supporter key" outbound links. Both open in a
+ new tab; neither one carries a price/value (D14 — the
+ only place pricing lives is the destination page). */}
+ {contributorClaimUrl && supporterPlansUrl && (
+
+ )}
)}
diff --git a/src/app/api/radar/settings/route.ts b/src/app/api/radar/settings/route.ts
index 9cd4604f23..2b92e08bdb 100644
--- a/src/app/api/radar/settings/route.ts
+++ b/src/app/api/radar/settings/route.ts
@@ -3,6 +3,13 @@
* snapshot. Powers the dashboard page's "am I already opted in?" check so
* a reload doesn't re-show the activation screen (see FIX 3).
*
+ * Also relays the two F4/T7 "get a supporter key" outbound links
+ * (`contributorClaimUrl`, `supporterPlansUrl` — see `@/lib/radar/links`) so
+ * the client component never reads `process.env` itself. Smallest surface
+ * per spec: no dedicated route, reuses this one. Both are plain public
+ * URLs (no secret, no pricing) — safe to expose alongside the settings
+ * snapshot, gated by the same flag/auth checks below.
+ *
* POST /api/radar/settings — set Radar opt-in and/or supporter key.
*
* Zod-validated body: { optIn?: boolean, supporterKey?: string|null }
@@ -22,6 +29,7 @@ import { CORS_HEADERS, handleCorsOptions } from "@/shared/utils/cors";
import { isFeatureFlagEnabled } from "@/shared/utils/featureFlags";
import { isAuthenticated } from "@/shared/utils/apiAuth";
import { setRadarOptIn, setRadarKey, getRadarSettings } from "@/lib/db/radar";
+import { getContributorClaimUrl, getSupporterPlansUrl } from "@/lib/radar/links";
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
export const dynamic = "force-dynamic";
@@ -75,6 +83,8 @@ export async function GET(request: Request) {
optIn: settings.optIn,
hasSupporterKey: settings.supporterKey !== null,
supporterKeyMasked: maskKey(settings.supporterKey),
+ contributorClaimUrl: getContributorClaimUrl(),
+ supporterPlansUrl: getSupporterPlansUrl(),
},
{ headers: { ...CORS_HEADERS, "Cache-Control": "no-store" } },
);
diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json
index f29e15e90b..3b5e057b25 100644
--- a/src/i18n/messages/ar.json
+++ b/src/i18n/messages/ar.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "تتم جميع المعالجة على مثيل OmniRoute الخاص بك",
"activateButton": "تفعيل",
"activating": "جارٍ التفعيل...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "المزود",
"colModel": "النموذج",
"colQuota": "الحصة",
diff --git a/src/i18n/messages/az.json b/src/i18n/messages/az.json
index 642ee7f34a..88df76e148 100644
--- a/src/i18n/messages/az.json
+++ b/src/i18n/messages/az.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Bütün emal sizin OmniRoute instansiyanızda baş verir",
"activateButton": "Aktivləşdir",
"activating": "Aktivləşdirilir...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Təchizatçı",
"colModel": "Model",
"colQuota": "Kvota",
diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json
index b4c3504cb3..39f618f59a 100644
--- a/src/i18n/messages/bg.json
+++ b/src/i18n/messages/bg.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Всички обработки се извършват на вашия OmniRoute инстанс",
"activateButton": "Активирайте",
"activating": "Активиране...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Доставчик",
"colModel": "Модел",
"colQuota": "Квота",
diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json
index 4f76394a5d..530aeb48bd 100644
--- a/src/i18n/messages/bn.json
+++ b/src/i18n/messages/bn.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "সমস্ত প্রক্রিয়াকরণ আপনার OmniRoute ইনস্ট্যান্সে ঘটে",
"activateButton": "সক্রিয় করুন",
"activating": "সক্রিয় হচ্ছে...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "প্রদানকারী",
"colModel": "মডেল",
"colQuota": "কোটা",
diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json
index 16dc33b22d..2d3a48084d 100644
--- a/src/i18n/messages/cs.json
+++ b/src/i18n/messages/cs.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Veškeré zpracování probíhá na vaší instanci OmniRoute",
"activateButton": "Aktivovat",
"activating": "Aktivace...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Poskytovatel",
"colModel": "Model",
"colQuota": "Kvóta",
diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json
index 54bad0890f..1916bd4fbf 100644
--- a/src/i18n/messages/da.json
+++ b/src/i18n/messages/da.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Al behandling sker på din OmniRoute instans",
"activateButton": "Aktiver",
"activating": "Aktiverer...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Udbyder",
"colModel": "Model",
"colQuota": "Kvote",
diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json
index 22c30f8b72..8a50bbfa89 100644
--- a/src/i18n/messages/de.json
+++ b/src/i18n/messages/de.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Alle Verarbeitungen erfolgen auf Ihrer OmniRoute-Instanz",
"activateButton": "Aktivieren",
"activating": "Aktivierung...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Anbieter",
"colModel": "Modell",
"colQuota": "Quote",
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 576fe16843..d1bc2c3c5f 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -12261,6 +12261,11 @@
"privacyLocalOnly": "All processing happens on your OmniRoute instance",
"activateButton": "Activate",
"activating": "Activating...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Provider",
"colModel": "Model",
"colQuota": "Quota",
diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json
index 882a3cf4c5..1d3e1829d3 100644
--- a/src/i18n/messages/es.json
+++ b/src/i18n/messages/es.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Todo el procesamiento ocurre en tu instancia de OmniRoute",
"activateButton": "Activar",
"activating": "Activando...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Proveedor",
"colModel": "Modelo",
"colQuota": "Cuota",
diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json
index 47771dc15f..541b4e8201 100644
--- a/src/i18n/messages/fa.json
+++ b/src/i18n/messages/fa.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "تمام پردازشها در نمونه OmniRoute شما انجام میشود",
"activateButton": "فعالسازی",
"activating": "در حال فعالسازی...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "تأمینکننده",
"colModel": "مدل",
"colQuota": "سهمیه",
diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json
index 7703a50a03..681edce5c2 100644
--- a/src/i18n/messages/fi.json
+++ b/src/i18n/messages/fi.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Kaikki käsittely tapahtuu OmniRoute-instanssissasi",
"activateButton": "Aktivoi",
"activating": "Aktivointi...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Palveluntarjoaja",
"colModel": "Malli",
"colQuota": "Kiintiö",
diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json
index a58e9539c7..6febef7cbd 100644
--- a/src/i18n/messages/fr.json
+++ b/src/i18n/messages/fr.json
@@ -12237,6 +12237,11 @@
"privacyLocalOnly": "Tout le traitement se fait sur votre instance OmniRoute",
"activateButton": "Activer",
"activating": "Activation en cours...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Fournisseur",
"colModel": "Modèle",
"colQuota": "Quota",
diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json
index ec5f103ece..bfd5e86155 100644
--- a/src/i18n/messages/gu.json
+++ b/src/i18n/messages/gu.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "તમામ પ્રક્રિયા તમારા ઓમ્નીરૂટ ઇન્સ્ટન્સ પર થાય છે",
"activateButton": "સક્રિય કરો",
"activating": "સક્રિય થઈ રહ્યું છે...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "પ્રદાતા",
"colModel": "મોડલ",
"colQuota": "ક્વોટા",
diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json
index 0a1acf9c61..138560d266 100644
--- a/src/i18n/messages/he.json
+++ b/src/i18n/messages/he.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "כל העיבוד מתבצע על מופע OmniRoute שלך",
"activateButton": "הפעל",
"activating": "מפעיל...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "ספק",
"colModel": "מודל",
"colQuota": "מכסה",
diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json
index 5d04366372..c1edee4b31 100644
--- a/src/i18n/messages/hi.json
+++ b/src/i18n/messages/hi.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "सभी प्रोसेसिंग आपके OmniRoute उदाहरण पर होती है",
"activateButton": "सक्रिय करें",
"activating": "सक्रिय किया जा रहा है...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "प्रदाता",
"colModel": "मॉडल",
"colQuota": "कोटा",
diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json
index b44bd46aa2..93da62a51e 100644
--- a/src/i18n/messages/hu.json
+++ b/src/i18n/messages/hu.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Minden feldolgozás a te OmniRoute példányodon történik",
"activateButton": "Aktiválás",
"activating": "Aktiválás...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Szolgáltató",
"colModel": "Modell",
"colQuota": "Kvóta",
diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json
index 8c75baac46..cdd2643680 100644
--- a/src/i18n/messages/id.json
+++ b/src/i18n/messages/id.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Semua pemrosesan terjadi di instance OmniRoute Anda",
"activateButton": "Aktifkan",
"activating": "Mengaktifkan...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Penyedia",
"colModel": "Model",
"colQuota": "Kuota",
diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json
index 37148788eb..682cfbe3c8 100644
--- a/src/i18n/messages/in.json
+++ b/src/i18n/messages/in.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "सभी प्रोसेसिंग आपके OmniRoute इंस्टेंस पर होती है",
"activateButton": "सक्रिय करें",
"activating": "सक्रियण हो रहा है...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "प्रदाता",
"colModel": "मॉडल",
"colQuota": "कोटा",
diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json
index 82049c41da..db40130bfb 100644
--- a/src/i18n/messages/it.json
+++ b/src/i18n/messages/it.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Tutto l'elaborazione avviene sulla tua istanza OmniRoute",
"activateButton": "Attiva",
"activating": "Attivazione in corso...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Fornitore",
"colModel": "Modello",
"colQuota": "Quota",
diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json
index b8dbb913a3..a9e11214a4 100644
--- a/src/i18n/messages/ja.json
+++ b/src/i18n/messages/ja.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "すべての処理はあなたのOmniRouteインスタンスで行われます",
"activateButton": "アクティブにする",
"activating": "アクティブにしています...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "プロバイダー",
"colModel": "モデル",
"colQuota": "クォータ",
diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json
index 604cb2525f..5629f8fe31 100644
--- a/src/i18n/messages/ko.json
+++ b/src/i18n/messages/ko.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "모든 처리는 귀하의 OmniRoute 인스턴스에서 발생합니다",
"activateButton": "활성화",
"activating": "활성화 중...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "제공자",
"colModel": "모델",
"colQuota": "할당량",
diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json
index f6a923ae9b..8a566c0949 100644
--- a/src/i18n/messages/mr.json
+++ b/src/i18n/messages/mr.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "सर्व प्रक्रिया तुमच्या OmniRoute उदाहरणावर होते",
"activateButton": "सक्रिय करा",
"activating": "सक्रिय करत आहे...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "प्रदाता",
"colModel": "मॉडेल",
"colQuota": "कोटा",
diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json
index d59a41387b..5bc9cefc53 100644
--- a/src/i18n/messages/ms.json
+++ b/src/i18n/messages/ms.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Semua pemprosesan berlaku pada instance OmniRoute anda",
"activateButton": "Aktifkan",
"activating": "Mengaktifkan...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Penyedia",
"colModel": "Model",
"colQuota": "Kuota",
diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json
index b30b2e2f59..74be8313b2 100644
--- a/src/i18n/messages/nl.json
+++ b/src/i18n/messages/nl.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Alle verwerking gebeurt op uw OmniRoute-instantie",
"activateButton": "Activeren",
"activating": "Activeren...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Leverancier",
"colModel": "Model",
"colQuota": "Quota",
diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json
index 8ed665b57d..99d6f2b39c 100644
--- a/src/i18n/messages/no.json
+++ b/src/i18n/messages/no.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "All behandling skjer på din OmniRoute-instans",
"activateButton": "Aktiver",
"activating": "Aktiverer...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Leverandør",
"colModel": "Modell",
"colQuota": "Kvote",
diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json
index b82c979cf3..52e88e96b8 100644
--- a/src/i18n/messages/phi.json
+++ b/src/i18n/messages/phi.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "All processing happens on your OmniRoute instance",
"activateButton": "Activate",
"activating": "Activating...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Provider",
"colModel": "Model",
"colQuota": "Quota",
diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json
index b38c51280b..919f1ea176 100644
--- a/src/i18n/messages/pl.json
+++ b/src/i18n/messages/pl.json
@@ -12234,6 +12234,11 @@
"privacyLocalOnly": "Wszystkie przetwarzanie odbywa się na twojej instancji OmniRoute",
"activateButton": "Aktywuj",
"activating": "Aktywacja...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Dostawca",
"colModel": "Model",
"colQuota": "Kwota",
diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json
index bff818e63b..8ab11f7702 100644
--- a/src/i18n/messages/pt-BR.json
+++ b/src/i18n/messages/pt-BR.json
@@ -12261,6 +12261,11 @@
"privacyLocalOnly": "Todo processamento acontece na sua instância OmniRoute",
"activateButton": "Ativar",
"activating": "Ativando...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Provedor",
"colModel": "Modelo",
"colQuota": "Cota",
diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json
index ba06cc71f6..a8433114e9 100644
--- a/src/i18n/messages/pt.json
+++ b/src/i18n/messages/pt.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Todo o processamento ocorre na sua instância OmniRoute",
"activateButton": "Ativar",
"activating": "A ativar...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Fornecedor",
"colModel": "Modelo",
"colQuota": "Quota",
diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json
index 646b9e12af..8ffc2b5faa 100644
--- a/src/i18n/messages/ro.json
+++ b/src/i18n/messages/ro.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Toate procesările au loc pe instanța ta OmniRoute",
"activateButton": "Activează",
"activating": "Activare...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Furnizor",
"colModel": "Model",
"colQuota": "Cotă",
diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json
index a44acd37c7..d2d37004f1 100644
--- a/src/i18n/messages/ru.json
+++ b/src/i18n/messages/ru.json
@@ -12306,6 +12306,11 @@
"privacyLocalOnly": "Все обработки происходят на вашем экземпляре OmniRoute",
"activateButton": "Активировать",
"activating": "Активация...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Провайдер",
"colModel": "Модель",
"colQuota": "Квота",
diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json
index 269c5a5972..f27b22a0c6 100644
--- a/src/i18n/messages/sk.json
+++ b/src/i18n/messages/sk.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Všetko spracovanie prebieha na vašej inštancii OmniRoute",
"activateButton": "Aktivovať",
"activating": "Aktivujem...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Poskytovateľ",
"colModel": "Model",
"colQuota": "Kvóta",
diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json
index 325a77b4b9..1bebb2b029 100644
--- a/src/i18n/messages/sv.json
+++ b/src/i18n/messages/sv.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "All bearbetning sker på din OmniRoute-instans",
"activateButton": "Aktivera",
"activating": "Aktiverar...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Leverantör",
"colModel": "Modell",
"colQuota": "Kvot",
diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json
index 216476a557..134aaea826 100644
--- a/src/i18n/messages/sw.json
+++ b/src/i18n/messages/sw.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "All bearbetning sker på din OmniRoute-instans",
"activateButton": "Aktivera",
"activating": "Aktiverar...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Leverantör",
"colModel": "Modell",
"colQuota": "Kvot",
diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json
index 36be7747a8..101961d4f7 100644
--- a/src/i18n/messages/ta.json
+++ b/src/i18n/messages/ta.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "அனைத்து செயலாக்கமும் உங்கள் OmniRoute instance இல் நடைபெறும்",
"activateButton": "செயல்படுத்தவும்",
"activating": "செயல்படுத்துகிறது...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "வழங்குநர்",
"colModel": "மாதிரி",
"colQuota": "கோட்டை",
diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json
index 174cccb1a4..d6fb26ec7b 100644
--- a/src/i18n/messages/te.json
+++ b/src/i18n/messages/te.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "అన్ని ప్రాసెసింగ్ మీ OmniRoute ఉదాహరణపై జరుగుతుంది",
"activateButton": "యాక్టివేట్ చేయండి",
"activating": "యాక్టివేట్ అవుతోంది...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "ప్రొవైడర్",
"colModel": "మోడల్",
"colQuota": "క్వోటా",
diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json
index 00b0d6ad6b..1c2da07af9 100644
--- a/src/i18n/messages/th.json
+++ b/src/i18n/messages/th.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "การประมวลผลทั้งหมดเกิดขึ้นบนอินสแตนซ์ OmniRoute ของคุณ",
"activateButton": "เปิดใช้งาน",
"activating": "กำลังเปิดใช้งาน...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "ผู้ให้บริการ",
"colModel": "โมเดล",
"colQuota": "โควต้า",
diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json
index 8371e20be9..c8f85cf4b1 100644
--- a/src/i18n/messages/tr.json
+++ b/src/i18n/messages/tr.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Tüm işleme, OmniRoute örneğinizde gerçekleşir",
"activateButton": "Etkinleştir",
"activating": "Etkinleştiriliyor...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Sağlayıcı",
"colModel": "Model",
"colQuota": "Kota",
diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json
index fb3e7b80df..edf9a00c13 100644
--- a/src/i18n/messages/uk-UA.json
+++ b/src/i18n/messages/uk-UA.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "Усе оброблення відбувається на вашій інстанції OmniRoute",
"activateButton": "Активувати",
"activating": "Активація...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Постачальник",
"colModel": "Модель",
"colQuota": "Квота",
diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json
index 6cd1caf669..bfe3fce3bf 100644
--- a/src/i18n/messages/ur.json
+++ b/src/i18n/messages/ur.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "تمام پروسیسنگ آپ کے OmniRoute انسٹنس پر ہوتی ہے",
"activateButton": "چالو کریں",
"activating": "چالو ہو رہا ہے...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "فراہم کنندہ",
"colModel": "ماڈل",
"colQuota": "کوٹہ",
diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json
index 3672a1733f..3951d02b2e 100644
--- a/src/i18n/messages/vi.json
+++ b/src/i18n/messages/vi.json
@@ -12261,6 +12261,11 @@
"privacyLocalOnly": "Tất cả xử lý diễn ra trên phiên bản OmniRoute của bạn",
"activateButton": "Kích hoạt",
"activating": "Đang kích hoạt...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "Nhà cung cấp",
"colModel": "Mô hình",
"colQuota": "Hạn ngạch",
diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json
index 58d53bdacd..d62d9a0a57 100644
--- a/src/i18n/messages/zh-CN.json
+++ b/src/i18n/messages/zh-CN.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "所有处理都在您的OmniRoute实例上进行",
"activateButton": "激活",
"activating": "正在激活...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "提供者",
"colModel": "模型",
"colQuota": "配额",
diff --git a/src/i18n/messages/zh-TW.json b/src/i18n/messages/zh-TW.json
index 9c08c95552..0e130048f7 100644
--- a/src/i18n/messages/zh-TW.json
+++ b/src/i18n/messages/zh-TW.json
@@ -12212,6 +12212,11 @@
"privacyLocalOnly": "所有處理都在您的 OmniRoute 實例上進行",
"activateButton": "啟用",
"activating": "正在啟用...",
+ "claimSectionTitle": "Don't have a supporter key yet?",
+ "contributorButton": "I'm a contributor",
+ "contributorHint": "Verify your GitHub account — 5+ merged pull requests or a top-100 contributor spot unlocks a free supporter key.",
+ "supporterButton": "Support the project",
+ "supporterHint": "Support OmniRoute to get a supporter key with the live catalog and extra perks.",
"colProvider": "提供者",
"colModel": "模型",
"colQuota": "配額",
diff --git a/src/lib/radar/links.ts b/src/lib/radar/links.ts
new file mode 100644
index 0000000000..3641efa763
--- /dev/null
+++ b/src/lib/radar/links.ts
@@ -0,0 +1,42 @@
+/**
+ * links.ts — pure config for the two Radar "get a supporter key" outbound
+ * links (F4/T7): the contributor-claim (GitHub OAuth) flow and the
+ * supporter-plans (payment) page on the private radar.omniroute.online
+ * server.
+ *
+ * DELIBERATELY DB-FREE and side-effect-free — same shape as the
+ * `RADAR_FEED_URL` override already used by `./sync.ts`, so forks/self-hosters
+ * point both links at their own deployment via env vars (see
+ * docs/frameworks/RADAR.md).
+ *
+ * These functions are read server-side only (inside a route handler) and the
+ * resolved URLs are relayed to the client via GET /api/radar/settings — the
+ * dashboard page never reads `process.env` itself, matching the pattern the
+ * D28 referral links already established for the private feed.
+ *
+ * No price or monetary value is ever resolved, stored, or exposed here — the
+ * URLs point at pages that are themselves the ONLY place pricing lives
+ * (spec D14: no pricing in the OSS repo).
+ */
+
+/** Default contributor-claim entry point — starts the GitHub OAuth flow. */
+const DEFAULT_CONTRIBUTOR_CLAIM_URL = "https://radar.omniroute.online/auth/github";
+
+/** Default supporter plans/payment page. */
+const DEFAULT_SUPPORTER_PLANS_URL = "https://radar.omniroute.online/planos";
+
+/**
+ * URL that starts the "I'm a contributor" GitHub OAuth claim flow.
+ * Override with `RADAR_CONTRIBUTOR_CLAIM_URL` for forks/self-hosters.
+ */
+export function getContributorClaimUrl(): string {
+ return process.env.RADAR_CONTRIBUTOR_CLAIM_URL || DEFAULT_CONTRIBUTOR_CLAIM_URL;
+}
+
+/**
+ * URL for the "Support the project" plans/payment page.
+ * Override with `RADAR_SUPPORTER_PLANS_URL` for forks/self-hosters.
+ */
+export function getSupporterPlansUrl(): string {
+ return process.env.RADAR_SUPPORTER_PLANS_URL || DEFAULT_SUPPORTER_PLANS_URL;
+}
diff --git a/tests/unit/radar-api-routes.test.ts b/tests/unit/radar-api-routes.test.ts
index 395bc17118..7d10e4351b 100644
--- a/tests/unit/radar-api-routes.test.ts
+++ b/tests/unit/radar-api-routes.test.ts
@@ -355,6 +355,7 @@ test("POST /api/radar/sync: authenticated, invalid body => 400", async () => {
// ---------------------------------------------------------------------------
// FIX 3 — GET /api/radar/settings: { optIn, hasSupporterKey, supporterKeyMasked }
+// F4/T7 — same response also relays contributorClaimUrl/supporterPlansUrl.
// ---------------------------------------------------------------------------
test("GET /api/radar/settings: flag on, authenticated, default state => optIn false, no key", async () => {
@@ -371,6 +372,9 @@ test("GET /api/radar/settings: flag on, authenticated, default state => optIn fa
assert.equal(body.optIn, false);
assert.equal(body.hasSupporterKey, false);
assert.equal(body.supporterKeyMasked, null);
+ // F4/T7: default claim/plans links are always present, opt-in or not.
+ assert.equal(body.contributorClaimUrl, "https://radar.omniroute.online/auth/github");
+ assert.equal(body.supporterPlansUrl, "https://radar.omniroute.online/planos");
});
test("GET /api/radar/settings: flag on, authenticated, after opt-in + key => reflects persisted state, never raw key", async () => {
@@ -402,6 +406,28 @@ test("GET /api/radar/settings: flag on, authenticated, after opt-in + key => ref
assert.ok(!text.includes(RAW_KEY), "raw key must NEVER appear in the serialized response body");
});
+test("GET /api/radar/settings: F4/T7 claim/plans links honor env overrides (fork-friendly)", async () => {
+ resetStorage();
+ process.env.RADAR_ENABLED = "true";
+ process.env.RADAR_CONTRIBUTOR_CLAIM_URL = "https://fork.example.com/auth/github";
+ process.env.RADAR_SUPPORTER_PLANS_URL = "https://fork.example.com/plans";
+
+ try {
+ const { GET } = await import("../../src/app/api/radar/settings/route.ts");
+ const response = await GET(
+ mockGetRequest("http://localhost:20128/api/radar/settings", await authHeaders()),
+ );
+ const body = await response.json();
+
+ assert.equal(response.status, 200);
+ assert.equal(body.contributorClaimUrl, "https://fork.example.com/auth/github");
+ assert.equal(body.supporterPlansUrl, "https://fork.example.com/plans");
+ } finally {
+ delete process.env.RADAR_CONTRIBUTOR_CLAIM_URL;
+ delete process.env.RADAR_SUPPORTER_PLANS_URL;
+ }
+});
+
// ---------------------------------------------------------------------------
// Tests: error sanitization (Hard Rule #12)
// ---------------------------------------------------------------------------
diff --git a/tests/unit/radar-claim-buttons.test.ts b/tests/unit/radar-claim-buttons.test.ts
new file mode 100644
index 0000000000..240e0670b5
--- /dev/null
+++ b/tests/unit/radar-claim-buttons.test.ts
@@ -0,0 +1,122 @@
+/**
+ * tests/unit/radar-claim-buttons.test.ts
+ *
+ * TDD guard for the F4/T7 "get a supporter key" buttons on the Radar
+ * activation screen (src/app/(dashboard)/dashboard/radar/page.tsx):
+ *
+ * - "I'm a contributor" and "Support the project" open in a new tab
+ * (target="_blank" rel="noopener noreferrer") and never hardcode an
+ * external URL — both links come from GET /api/radar/settings
+ * (server-resolved via src/lib/radar/links.ts), never process.env
+ * read client-side.
+ * - No price/monetary value appears anywhere in the page source (D14).
+ * - Every new t("...") key referenced exists (non-empty) in en.json and
+ * all locale files.
+ *
+ * Structural, source-based — same style as
+ * tests/unit/radar-referrals-page-tab.test.ts — deliberately avoids a full
+ * component render (no jsdom harness in this repo's unit runner).
+ */
+
+import test from "node:test";
+import assert from "node:assert/strict";
+import fs from "node:fs";
+import path from "node:path";
+
+const PAGE_PATH = path.resolve(
+ process.cwd(),
+ "src/app/(dashboard)/dashboard/radar/page.tsx"
+);
+const PAGE_SRC = fs.readFileSync(PAGE_PATH, "utf-8");
+
+const NEW_KEYS = [
+ "claimSectionTitle",
+ "contributorButton",
+ "contributorHint",
+ "supporterButton",
+ "supporterHint",
+];
+
+test("radar page: claim/plans links are state, never a hardcoded external URL literal", () => {
+ assert.ok(
+ PAGE_SRC.includes("contributorClaimUrl") && PAGE_SRC.includes("supporterPlansUrl"),
+ "page must reference contributorClaimUrl/supporterPlansUrl state"
+ );
+ // Same guard as the D28 referrals test: no literal https:// (except in
+ // comments) anywhere in this client component — links are always
+ // server-resolved and relayed through the settings fetch.
+ assert.ok(
+ !/https?:\/\/(?!localhost)/.test(PAGE_SRC.replace(/\/\*[\s\S]*?\*\//g, "")),
+ "page must never hardcode an external URL directly"
+ );
+ // Never read process.env directly in this client component.
+ assert.ok(
+ !PAGE_SRC.includes("process.env"),
+ "page must never read process.env client-side — URLs come from the settings fetch"
+ );
+});
+
+test("radar page: both buttons open in a new tab safely", () => {
+ const contributorAnchor = PAGE_SRC.match(
+ /href=\{contributorClaimUrl\}[\s\S]{0,120}/
+ )?.[0];
+ const supporterAnchor = PAGE_SRC.match(/href=\{supporterPlansUrl\}[\s\S]{0,120}/)?.[0];
+ assert.ok(contributorAnchor, "contributorClaimUrl anchor must exist");
+ assert.ok(supporterAnchor, "supporterPlansUrl anchor must exist");
+ for (const anchor of [contributorAnchor, supporterAnchor]) {
+ assert.ok(anchor!.includes('target="_blank"'), "must open in a new tab");
+ assert.ok(
+ anchor!.includes('rel="noopener noreferrer"'),
+ "must set rel=noopener noreferrer"
+ );
+ }
+});
+
+test("radar page: references the 5 new claim-section t(...) keys", () => {
+ for (const key of NEW_KEYS) {
+ assert.ok(
+ PAGE_SRC.includes(`t("${key}")`),
+ `page.tsx must reference t("${key}")`
+ );
+ }
+});
+
+test("radar page + all 43 locale files: no price/monetary value in the claim section copy (D14)", () => {
+ // D14: no pricing anywhere in the OSS repo, only a link to the plans page.
+ const PRICE_PATTERN = /\$\s?\d|R\$\s?\d|\d+[.,]\d{2}\s?(USD|BRL|EUR)|\b(lifetime|life-time)\b.{0,20}\$/i;
+ assert.ok(!PRICE_PATTERN.test(PAGE_SRC), "page.tsx must not contain a price/monetary value");
+
+ const messagesDir = path.resolve(process.cwd(), "src/i18n/messages");
+ const files = fs.readdirSync(messagesDir).filter((f) => f.endsWith(".json"));
+ assert.ok(files.length >= 40, `expected ~43 locale files, found ${files.length}`);
+
+ for (const file of files) {
+ const data = JSON.parse(fs.readFileSync(path.join(messagesDir, file), "utf-8"));
+ const radarPage = data.radarPage as Record | undefined;
+ assert.ok(radarPage, `${file}: missing radarPage namespace`);
+ for (const key of NEW_KEYS) {
+ const value = radarPage![key];
+ assert.equal(typeof value, "string", `${file}: radarPage.${key} must be a string`);
+ assert.ok((value as string).length > 0, `${file}: radarPage.${key} is empty`);
+ assert.ok(
+ !PRICE_PATTERN.test(value as string),
+ `${file}: radarPage.${key} must not contain a price/monetary value`
+ );
+ }
+ }
+});
+
+test("no OSS file mentions the word 'freellmapi'", () => {
+ // Repo-wide guard scoped to the files this task touches — the full
+ // repo-wide ban is enforced elsewhere; this is a local regression check
+ // for the files this feature added/edited.
+ const filesToCheck = [
+ PAGE_PATH,
+ path.resolve(process.cwd(), "src/lib/radar/links.ts"),
+ path.resolve(process.cwd(), "src/app/api/radar/settings/route.ts"),
+ ];
+ for (const file of filesToCheck) {
+ const src = fs.readFileSync(file, "utf-8");
+ assert.ok(!/freellmapi/i.test(src), `${file} must not mention freellmapi`);
+ }
+});
diff --git a/tests/unit/radar-links.test.ts b/tests/unit/radar-links.test.ts
new file mode 100644
index 0000000000..9a173ec89e
--- /dev/null
+++ b/tests/unit/radar-links.test.ts
@@ -0,0 +1,56 @@
+/**
+ * tests/unit/radar-links.test.ts
+ *
+ * TDD guard for src/lib/radar/links.ts — the two outbound "get a supporter
+ * key" links (F4/T7): contributor-claim (GitHub OAuth) and supporter-plans
+ * (payment page). Pure, DB-free module: defaults + env override only.
+ *
+ * No price/monetary value assertion lives here on purpose — this module
+ * never resolves one (D14: pricing only lives on the private plans page the
+ * URL points at, never in the OSS repo).
+ */
+
+import test from "node:test";
+import assert from "node:assert/strict";
+
+test.beforeEach(() => {
+ delete process.env.RADAR_CONTRIBUTOR_CLAIM_URL;
+ delete process.env.RADAR_SUPPORTER_PLANS_URL;
+});
+
+test.after(() => {
+ delete process.env.RADAR_CONTRIBUTOR_CLAIM_URL;
+ delete process.env.RADAR_SUPPORTER_PLANS_URL;
+});
+
+test("getContributorClaimUrl: defaults to the radar.omniroute.online GitHub OAuth entry point", async () => {
+ const { getContributorClaimUrl } = await import("../../src/lib/radar/links.ts");
+ assert.equal(getContributorClaimUrl(), "https://radar.omniroute.online/auth/github");
+});
+
+test("getContributorClaimUrl: honors RADAR_CONTRIBUTOR_CLAIM_URL override", async () => {
+ process.env.RADAR_CONTRIBUTOR_CLAIM_URL = "https://fork.example.com/auth/github";
+ const { getContributorClaimUrl } = await import("../../src/lib/radar/links.ts");
+ assert.equal(getContributorClaimUrl(), "https://fork.example.com/auth/github");
+});
+
+test("getSupporterPlansUrl: defaults to the radar.omniroute.online plans page", async () => {
+ const { getSupporterPlansUrl } = await import("../../src/lib/radar/links.ts");
+ assert.equal(getSupporterPlansUrl(), "https://radar.omniroute.online/planos");
+});
+
+test("getSupporterPlansUrl: honors RADAR_SUPPORTER_PLANS_URL override", async () => {
+ process.env.RADAR_SUPPORTER_PLANS_URL = "https://fork.example.com/plans";
+ const { getSupporterPlansUrl } = await import("../../src/lib/radar/links.ts");
+ assert.equal(getSupporterPlansUrl(), "https://fork.example.com/plans");
+});
+
+test("getContributorClaimUrl / getSupporterPlansUrl: empty-string env falls back to default (not a blank link)", async () => {
+ process.env.RADAR_CONTRIBUTOR_CLAIM_URL = "";
+ process.env.RADAR_SUPPORTER_PLANS_URL = "";
+ const { getContributorClaimUrl, getSupporterPlansUrl } = await import(
+ "../../src/lib/radar/links.ts"
+ );
+ assert.equal(getContributorClaimUrl(), "https://radar.omniroute.online/auth/github");
+ assert.equal(getSupporterPlansUrl(), "https://radar.omniroute.online/planos");
+});