Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ export default function ProxyTab() {
<div
className="flex gap-1 border-b border-border overflow-x-auto"
role="tablist"
aria-label="Proxy sections"
aria-label={t("proxySubTabsAria")}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The variable t is used here (t("proxySubTabsAria")), but it does not appear to be imported or initialized in this file. Please ensure you import useTranslations from next-intl and initialize t at the top of the ProxyTab component:

import { useTranslations } from "next-intl";

// Inside ProxyTab:
const t = useTranslations("settings");

>
{TABS.map((tab) => (
<button
Expand Down
Original file line number Diff line number Diff line change
@@ -1,60 +1,62 @@
"use client";
import { useTranslations } from "next-intl";
import { Card } from "@/shared/components";

export default function DocumentationTab() {
const t = useTranslations("settings");

return (
<Card className="p-6 space-y-6">
<section>
<h3 className="font-semibold mb-2">Proxy scope resolution</h3>
<h3 className="font-semibold mb-2">{t("proxyDocumentationScopeTitle")}</h3>
<p className="text-sm text-text-muted">
OmniRoute resolves the outbound proxy in priority order:
<strong> combo → account → provider → global</strong>. The most specific scope wins.
{t("proxyDocumentationScopeDescBefore")}
<strong>{t("proxyDocumentationScopeOrder")}</strong>
{t("proxyDocumentationScopeDescAfter")}
</p>
</section>

<section>
<h3 className="font-semibold mb-2">Adding a custom proxy</h3>
<h3 className="font-semibold mb-2">{t("proxyDocumentationAddTitle")}</h3>
<p className="text-sm text-text-muted">
Go to the <strong>Proxy Pool</strong> tab → click <em>+ Add proxy</em>. Fill in type
(http/https/socks5), host, and port. Optionally assign it to a scope.
{t("proxyDocumentationAddDescBefore")}
<strong>{t("proxyPoolTab")}</strong>
{t("proxyDocumentationAddDescMiddle")}
<em>{t("proxyDocumentationAddCta")}</em>
{t("proxyDocumentationAddDescAfter")}
</p>
</section>

<section>
<h3 className="font-semibold mb-2">Bulk import format</h3>
<h3 className="font-semibold mb-2">{t("proxyDocumentationBulkTitle")}</h3>
<pre className="text-xs bg-surface-alt/50 p-3 rounded mt-1 overflow-x-auto">
{`http://user:pass@1.2.3.4:8080\nhttps://5.6.7.8:3128\nsocks5://9.0.1.2:1080`}
</pre>
<p className="text-sm text-text-muted mt-1">
Pipe-delimited fields: <code>type|host|port|user|pass|name</code>
{t("proxyDocumentationBulkDesc")} <code>type|host|port|user|pass|name</code>
</p>
</section>

<section>
<h3 className="font-semibold mb-2">SOCKS5</h3>
<p className="text-sm text-text-muted">
SOCKS5 proxies are disabled by default. Set{" "}
{t("proxyDocumentationSocks5DescBefore")}{" "}
<code className="bg-surface-alt px-1 rounded">ENABLE_SOCKS5_PROXY=true</code> to enable.
Comment on lines +43 to 44

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The string " to enable." is hardcoded in English here, which breaks internationalization for other languages (e.g., Portuguese). You should split this into a separate translation key, such as proxyDocumentationSocks5DescAfter, and define it in your translation files.

For example, in en.json:
"proxyDocumentationSocks5DescAfter": " to enable."

In pt-BR.json:
"proxyDocumentationSocks5DescAfter": " para habilitar."

          {t("proxyDocumentationSocks5DescBefore")}{" "}
          <code className="bg-surface-alt px-1 rounded">ENABLE_SOCKS5_PROXY=true</code>{" "}
          {t("proxyDocumentationSocks5DescAfter")}

</p>
Comment on lines +43 to 45
</section>

<section>
<h3 className="font-semibold mb-2">Free Pool</h3>
<h3 className="font-semibold mb-2">{t("freePoolTab")}</h3>
<p className="text-sm text-text-muted">
The Free Pool tab aggregates proxies from 1proxy, Proxifly, and IPLocate. Use ⊕ to test
and promote a proxy to your registry. Only proxies that pass the connectivity test are
added.
{t("proxyDocumentationFreePoolDesc")}
</p>
</section>

<section>
<h3 className="font-semibold mb-2">Vercel Relay</h3>
<p className="text-sm text-text-muted">
The Vercel Relay is an outbound edge relay — not an inbound tunnel. Deploying one sends
LLM API calls through Vercel&apos;s dynamic IPs, bypassing datacenter geo-blocks and rate
limits. The relay is protected by a generated secret header (
<code className="bg-surface-alt px-1 rounded">x-relay-auth</code>). Your Vercel token is
used only during deploy and is never stored.
{t("proxyDocumentationVercelRelayDescBefore")} (
<code className="bg-surface-alt px-1 rounded">x-relay-auth</code>). {t("proxyDocumentationVercelRelayDescAfter")}
</p>
</section>
</Card>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";
import { useState, useEffect, useCallback } from "react";
import { useTranslations } from "next-intl";
import { Button } from "@/shared/components";
import SourceToggleBar, {
type SourceId,
Expand All @@ -17,6 +18,7 @@ type FreePoolStats = {
};

export default function FreePoolTab() {
const t = useTranslations("settings");
const [proxies, setProxies] = useState<FreeProxyRowData[]>([]);
const [stats, setStats] = useState<FreePoolStats | null>(null);
const [disabledSources, setDisabledSources] = useState<Set<SourceId>>(new Set());
Expand Down Expand Up @@ -148,9 +150,9 @@ export default function FreePoolTab() {
value={filterProtocol}
onChange={(e) => setFilterProtocol(e.target.value)}
className="text-xs bg-surface-alt border border-border rounded px-2 py-1"
aria-label="Filter by protocol"
aria-label={t("proxyFreePoolFilterProtocol")}
>
<option value="">Protocol</option>
<option value="">{t("proxyFreePoolProtocol")}</option>
{["http", "https", "socks4", "socks5"].map((p) => (
<option key={p} value={p}>
{p.toUpperCase()}
Expand All @@ -159,44 +161,44 @@ export default function FreePoolTab() {
</select>
<input
type="text"
placeholder="Country (e.g. US)"
placeholder={t("proxyFreePoolCountryPlaceholder")}
value={filterCountry}
onChange={(e) => setFilterCountry(e.target.value.toUpperCase().slice(0, 2))}
className="text-xs bg-surface-alt border border-border rounded px-2 py-1 w-28"
aria-label="Filter by country"
aria-label={t("proxyFreePoolFilterCountry")}
/>
<input
type="number"
placeholder="Min quality"
placeholder={t("proxyFreePoolMinQualityPlaceholder")}
value={minQuality}
onChange={(e) => setMinQuality(e.target.value)}
min={0}
max={100}
className="text-xs bg-surface-alt border border-border rounded px-2 py-1 w-24"
aria-label="Minimum quality score"
aria-label={t("proxyFreePoolMinQualityLabel")}
/>
<Button size="sm" variant="secondary" icon="sync" onClick={handleSync} disabled={syncing}>
{syncing ? "Syncing..." : "Sync All"}
{syncing ? t("syncing") : t("proxyFreePoolSyncAll")}
</Button>
</div>
</div>

{stats && (
<div className="text-xs text-text-muted flex gap-4 flex-wrap">
<span>Total: {stats.total}</span>
<span>In pool: {stats.inPool}</span>
{stats.avgQuality != null && <span>Avg quality: {stats.avgQuality}</span>}
<span>{t("proxyFreePoolTotal")}: {stats.total}</span>
<span>{t("proxyFreePoolInPool")}: {stats.inPool}</span>
{stats.avgQuality != null && <span>{t("proxyFreePoolAvgQuality")}: {stats.avgQuality}</span>}
{stats.lastSyncAt && (
<span>Last sync: {new Date(stats.lastSyncAt).toLocaleTimeString()}</span>
<span>{t("lastSync")}: {new Date(stats.lastSyncAt).toLocaleTimeString()}</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using toLocaleTimeString() directly in the render path can cause React hydration mismatch errors in Next.js because the server's locale/timezone during SSR may differ from the client's browser locale/timezone.

To prevent this, consider adding suppressHydrationWarning to the <span> or rendering the time only after the component has mounted on the client.

Suggested change
<span>{t("lastSync")}: {new Date(stats.lastSyncAt).toLocaleTimeString()}</span>
<span suppressHydrationWarning>{t("lastSync")}: {new Date(stats.lastSyncAt).toLocaleTimeString()}</span>

)}
</div>
)}

{selected.size > 0 && (
<div className="flex items-center gap-2 p-2 bg-primary/10 rounded border border-primary/20">
<span className="text-xs">{selected.size} selected</span>
<span className="text-xs">{t("proxyFreePoolSelected", { count: selected.size })}</span>
<Button size="sm" variant="primary" onClick={() => handleBulkAdd(Array.from(selected))}>
⊕ Add selected to pool
{t("proxyFreePoolAddSelected")}
</Button>
{bulkProgress && <span className="text-xs text-text-muted">{bulkProgress}</span>}
</div>
Expand All @@ -209,7 +211,7 @@ export default function FreePoolTab() {
variant="secondary"
onClick={() => handleBulkAdd(notInPoolProxies.slice(0, 100).map((p) => p.id))}
>
⊕ Add all visible to pool
{t("proxyFreePoolAddVisible")}
</Button>
</div>
)}
Expand All @@ -220,22 +222,22 @@ export default function FreePoolTab() {
<tr>
<th className="px-3 py-2 text-left w-8" scope="col"></th>
<th className="px-3 py-2 text-left" scope="col">
Source
{t("proxyFreePoolSource")}
</th>
<th className="px-3 py-2 text-left" scope="col">
Host:Port
{t("proxyFreePoolHostPort")}
</th>
<th className="px-3 py-2 text-left" scope="col">
Type
{t("proxyFreePoolType")}
</th>
<th className="px-3 py-2 text-left" scope="col">
Country
{t("proxyFreePoolCountry")}
</th>
<th className="px-3 py-2 text-left" scope="col">
Quality
{t("proxyFreePoolQuality")}
</th>
<th className="px-3 py-2 text-left" scope="col">
Latency
{t("proxyFreePoolLatency")}
</th>
<th className="px-3 py-2 text-left" scope="col"></th>
</tr>
Expand All @@ -244,13 +246,13 @@ export default function FreePoolTab() {
{loading ? (
<tr>
<td colSpan={8} className="px-3 py-8 text-center text-text-muted">
Loading...
{t("loading")}
</td>
</tr>
) : proxies.length === 0 ? (
<tr>
<td colSpan={8} className="px-3 py-8 text-center text-text-muted">
No proxies found. Click Sync All to fetch from sources.
{t("proxyFreePoolEmpty")}
</td>
</tr>
) : (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function VercelRelayModal({ isOpen, onClose, onDeployed }: Vercel

const handleDeploy = async () => {
if (!token.trim()) {
setError("Token is required");
setError(t("vercelRelayTokenRequired"));
return;
}
setDeploying(true);
Expand All @@ -33,14 +33,14 @@ export default function VercelRelayModal({ isOpen, onClose, onDeployed }: Vercel
});
const data = await res.json();
if (!res.ok || !data.success) {
setError(data.error?.message || "Deploy failed");
setError(data.error?.message || t("vercelRelayDeployFailed"));
} else {
setToken("");
onDeployed(data.poolProxyId as string, data.relayUrl as string);
onClose();
}
} catch (err) {
setError(err instanceof Error ? err.message : "Unknown error");
setError(err instanceof Error ? err.message : t("unknownError"));
} finally {
setDeploying(false);
}
Expand All @@ -63,7 +63,7 @@ export default function VercelRelayModal({ isOpen, onClose, onDeployed }: Vercel
</span>
{t("vercelRelayModalTitle")}
</h2>
<button onClick={onClose} aria-label="Close" className="text-text-muted hover:text-text">
<button onClick={onClose} aria-label={t("close")} className="text-text-muted hover:text-text">
<span className="material-symbols-outlined" aria-hidden="true">
close
</span>
Expand All @@ -89,7 +89,7 @@ export default function VercelRelayModal({ isOpen, onClose, onDeployed }: Vercel
autoComplete="off"
/>
<p className="text-xs text-text-muted mt-1">
Token is used only during deploy — never stored.
{t("vercelRelayTokenHint")}
</p>
</div>
<div>
Expand Down Expand Up @@ -117,7 +117,7 @@ export default function VercelRelayModal({ isOpen, onClose, onDeployed }: Vercel

<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" size="sm" onClick={onClose} disabled={deploying}>
Cancel
{t("cancel")}
</Button>
<Button variant="primary" size="sm" onClick={handleDeploy} disabled={deploying}>
{deploying ? t("vercelRelayDeploying") : t("vercelRelayDeploy")}
Expand Down
58 changes: 56 additions & 2 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3052,6 +3052,15 @@
"apiEndpointsSearchPlaceholder": "Search endpoints...",
"apiEndpointsRequiresAuth": "Requires auth",
"apiEndpointsNoMatch": "No endpoints match your filter",
"vscodeAliasTitle": "VS Code Token Alias",
"vscodeAliasDescriptionReady": "Ready-to-paste compatibility URLs using the /api/v1/vscode/TOKEN/... endpoint.",
"vscodeAliasDescriptionError": "Showing placeholder URLs because CLI keys could not be loaded in this session.",
"vscodeAliasDescriptionLoading": "Loading CLI keys. Placeholder URLs are shown until a key is available.",
"vscodeAliasDescriptionPlaceholder": "Showing placeholder URLs. Create or activate an API key in CLI Tools to replace TOKEN.",
"vscodeAliasManage": "CLI Tools",
"vscodeAliasBaseLabel": "VS Code base",
"vscodeAliasModelsLabel": "VS Code models",
"vscodeAliasChatLabel": "VS Code chat",
"localServer": "Local Server",
"cloudOmniroute": "Cloud OmniRoute",
"copyUrl": "Copy URL",
Expand Down Expand Up @@ -4919,8 +4928,53 @@
"evictions": "Evictions",
"loadingCacheStats": "Loading cache stats…",
"globalProxy": "Global Proxy",
"globalProxyDesc": "Configure a global outbound proxy for all API calls. Individual providers, combos, and keys can override this.",
"noGlobalProxy": "No global proxy configured",
"proxyPool": "Proxy Pool",
"freePool": "Free Pool",
"proxyDocumentation": "Proxy Documentation",
Comment on lines 4928 to +4933
"proxyGlobalConfigTab": "Global",
"proxyPoolTab": "Proxy Pool",
"freePoolTab": "Free Pool",
"proxyDocumentationTab": "Documentation",
"proxyDocumentationScopeTitle": "Proxy scope resolution",
"proxyDocumentationScopeDescBefore": "OmniRoute resolves the outbound proxy in priority order: ",
"proxyDocumentationScopeOrder": "combo → account → provider → global",
"proxyDocumentationScopeDescAfter": ". The most specific scope wins.",
"proxyDocumentationAddTitle": "Adding a custom proxy",
"proxyDocumentationAddDescBefore": "Go to the ",
"proxyDocumentationAddDescMiddle": " tab → click ",
"proxyDocumentationAddCta": "+ Add proxy",
"proxyDocumentationAddDescAfter": ". Fill in type (http/https/socks5), host, and port. Optionally assign it to a scope.",
"proxyDocumentationBulkTitle": "Bulk import format",
"proxyDocumentationBulkDesc": "Pipe-delimited fields:",
"proxyDocumentationSocks5DescBefore": "SOCKS5 proxies are disabled by default. Set",
"proxyDocumentationFreePoolDesc": "The Free Pool tab aggregates proxies from 1proxy, Proxifly, and IPLocate. Use ⊕ to test and promote a proxy to your registry. Only proxies that pass the connectivity test are added.",
"proxyDocumentationVercelRelayDescBefore": "The Vercel Relay is an outbound edge relay — not an inbound tunnel. Deploying one sends LLM API calls through Vercel's dynamic IPs, bypassing datacenter geo-blocks and rate limits. The relay is protected by a generated secret header",
"proxyDocumentationVercelRelayDescAfter": "Your Vercel token is used only during deploy and is never stored.",
"vercelRelayTokenRequired": "Token is required",
"vercelRelayDeployFailed": "Deploy failed",
"vercelRelayTokenHint": "Token is used only during deploy — never stored.",
"proxyFreePoolFilterProtocol": "Filter by protocol",
"proxyFreePoolProtocol": "Protocol",
"proxyFreePoolCountryPlaceholder": "Country (e.g. US)",
"proxyFreePoolFilterCountry": "Filter by country",
"proxyFreePoolMinQualityPlaceholder": "Min quality",
"proxyFreePoolMinQualityLabel": "Minimum quality score",
"proxyFreePoolSyncAll": "Sync All",
"proxyFreePoolTotal": "Total",
"proxyFreePoolInPool": "In pool",
"proxyFreePoolAvgQuality": "Avg quality",
"proxyFreePoolSelected": "{count} selected",
"proxyFreePoolAddSelected": "Add selected to pool",
"proxyFreePoolAddVisible": "Add all visible to pool",
"proxyFreePoolSource": "Source",
"proxyFreePoolHostPort": "Host:Port",
"proxyFreePoolType": "Type",
"proxyFreePoolCountry": "Country",
"proxyFreePoolQuality": "Quality",
"proxyFreePoolLatency": "Latency",
"proxyFreePoolEmpty": "No proxies found. Click Sync All to fetch from sources.",
"close": "Close",
"cancel": "Cancel",
"globalLabel": "Global",
"configure": "Configure",
"globalSystemPrompt": "Global System Prompt",
Expand Down
Loading