mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
fix(ui): improve dashboard responsiveness and i18n error message
- Add responsive breakpoints (sm/lg) for flex and grid layouts - Prevent text overflow with min-w-0 and break-words utilities - Stack buttons and inputs vertically on small screens - Replace hardcoded HTTP error with translated audit log message
This commit is contained in:
@@ -41,7 +41,7 @@ export default function AuditLogTab() {
|
||||
params.set("offset", String(offset));
|
||||
|
||||
const res = await fetch(`/api/compliance/audit-log?${params.toString()}`);
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
if (!res.ok) throw new Error(t("failedFetchAuditLog"));
|
||||
const data: AuditEntry[] = await res.json();
|
||||
|
||||
setHasMore(data.length > PAGE_SIZE);
|
||||
|
||||
@@ -75,7 +75,7 @@ export default function NewProviderPage() {
|
||||
const selectedProvider = (AI_PROVIDERS as any)[formData.provider];
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="max-w-2xl mx-auto min-w-0">
|
||||
{/* Header */}
|
||||
<div className="mb-8">
|
||||
<Link
|
||||
@@ -85,7 +85,9 @@ export default function NewProviderPage() {
|
||||
<span className="material-symbols-outlined text-lg">arrow_back</span>
|
||||
{t("backToProviders")}
|
||||
</Link>
|
||||
<h1 className="text-3xl font-semibold tracking-tight">{t("addNewProvider")}</h1>
|
||||
<h1 className="text-2xl sm:text-3xl font-semibold tracking-tight break-words">
|
||||
{t("addNewProvider")}
|
||||
</h1>
|
||||
<p className="text-text-muted mt-2">{t("configureNewProvider")}</p>
|
||||
</div>
|
||||
|
||||
@@ -126,13 +128,13 @@ export default function NewProviderPage() {
|
||||
<label className="text-sm font-medium">
|
||||
{t("authMethod")} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex flex-col sm:flex-row gap-3">
|
||||
{authMethodOptions.map((method) => (
|
||||
<button
|
||||
key={method.value}
|
||||
type="button"
|
||||
onClick={() => handleChange("authMethod", method.value)}
|
||||
className={`flex-1 flex items-center justify-center gap-2 p-4 rounded-lg border transition-all ${
|
||||
className={`flex-1 min-w-0 flex items-center justify-center gap-2 p-4 rounded-lg border transition-all ${
|
||||
formData.authMethod === method.value
|
||||
? "border-primary bg-primary/5 text-primary"
|
||||
: "border-border hover:border-primary/50"
|
||||
@@ -141,7 +143,7 @@ export default function NewProviderPage() {
|
||||
<span className="material-symbols-outlined">
|
||||
{method.value === "api_key" ? "key" : "lock"}
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
<span className="font-medium text-center break-words">
|
||||
{method.value === "api_key" ? t("apiKeyLabel") : t("oauth2Label")}
|
||||
</span>
|
||||
</button>
|
||||
@@ -198,7 +200,7 @@ export default function NewProviderPage() {
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-4 border-t border-border">
|
||||
<div className="flex flex-col sm:flex-row gap-3 pt-4 border-t border-border">
|
||||
<Link href="/dashboard/providers" className="flex-1">
|
||||
<Button type="button" variant="ghost" fullWidth>
|
||||
{t("cancel")}
|
||||
|
||||
@@ -88,7 +88,7 @@ export default function IPFilterSection() {
|
||||
</div>
|
||||
|
||||
{/* Mode selector */}
|
||||
<div className="grid grid-cols-4 gap-2 mb-5">
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-2 mb-5">
|
||||
{MODES.map((m) => (
|
||||
<button
|
||||
key={m.value}
|
||||
@@ -119,7 +119,7 @@ export default function IPFilterSection() {
|
||||
{config.enabled && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{/* Add IP */}
|
||||
<div className="flex gap-2 items-end">
|
||||
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-end">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label={t("addIpAddress")}
|
||||
@@ -215,12 +215,12 @@ export default function IPFilterSection() {
|
||||
{config.tempBans.map((ban) => (
|
||||
<div
|
||||
key={ban.ip}
|
||||
className="flex items-center justify-between px-3 py-2 rounded-lg
|
||||
className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg
|
||||
bg-orange-500/5 border border-orange-500/20 text-sm"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-xs text-orange-400">{ban.ip}</span>
|
||||
<span className="text-xs text-text-muted">— {ban.reason}</span>
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
<span className="font-mono text-xs text-orange-400 shrink-0">{ban.ip}</span>
|
||||
<span className="text-xs text-text-muted truncate">— {ban.reason}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-text-muted tabular-nums">
|
||||
|
||||
@@ -4,13 +4,13 @@ import { useState, useEffect, useCallback, useMemo } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const PRICING_FIELDS = ["input", "output", "cached", "reasoning", "cache_creation"];
|
||||
const FIELD_LABELS = {
|
||||
input: "Input",
|
||||
output: "Output",
|
||||
cached: "Cached",
|
||||
reasoning: "Reasoning",
|
||||
cache_creation: "Cache Write",
|
||||
const PRICING_FIELDS = ["input", "output", "cached", "reasoning", "cache_creation"] as const;
|
||||
const FIELD_LABEL_KEYS: Record<(typeof PRICING_FIELDS)[number], string> = {
|
||||
input: "input",
|
||||
output: "output",
|
||||
cached: "cached",
|
||||
reasoning: "reasoning",
|
||||
cache_creation: "cacheCreation",
|
||||
};
|
||||
|
||||
export default function PricingTab() {
|
||||
@@ -121,7 +121,7 @@ export default function PricingTab() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setSaveStatus(`✅ ${providerAlias.toUpperCase()} saved`);
|
||||
setSaveStatus(`✅ ${providerAlias.toUpperCase()} ${t("saved")}`);
|
||||
setEditedProviders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(providerAlias);
|
||||
@@ -130,36 +130,41 @@ export default function PricingTab() {
|
||||
setTimeout(() => setSaveStatus(""), 3000);
|
||||
} else {
|
||||
const err = await response.json();
|
||||
setSaveStatus(`❌ Error: ${err.error}`);
|
||||
setSaveStatus(`❌ ${t("errorOccurred")}: ${err.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setSaveStatus(`❌ Save failed: ${error.message}`);
|
||||
setSaveStatus(`❌ ${t("saveFailed")}: ${error.message}`);
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
},
|
||||
[pricingData]
|
||||
[pricingData, t]
|
||||
);
|
||||
|
||||
const resetProvider = useCallback(async (providerAlias) => {
|
||||
if (!confirm(t("resetPricingConfirm", { provider: providerAlias.toUpperCase() }))) return;
|
||||
try {
|
||||
const response = await fetch(`/api/pricing?provider=${providerAlias}`, { method: "DELETE" });
|
||||
if (response.ok) {
|
||||
const updated = await response.json();
|
||||
setPricingData(updated);
|
||||
setSaveStatus(`🔄 ${providerAlias.toUpperCase()} reset to defaults`);
|
||||
setEditedProviders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(providerAlias);
|
||||
return next;
|
||||
const resetProvider = useCallback(
|
||||
async (providerAlias) => {
|
||||
if (!confirm(t("resetPricingConfirm", { provider: providerAlias.toUpperCase() }))) return;
|
||||
try {
|
||||
const response = await fetch(`/api/pricing?provider=${providerAlias}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
setTimeout(() => setSaveStatus(""), 3000);
|
||||
if (response.ok) {
|
||||
const updated = await response.json();
|
||||
setPricingData(updated);
|
||||
setSaveStatus(`🔄 ${providerAlias.toUpperCase()} ${t("resetDefaults")}`);
|
||||
setEditedProviders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(providerAlias);
|
||||
return next;
|
||||
});
|
||||
setTimeout(() => setSaveStatus(""), 3000);
|
||||
}
|
||||
} catch (error) {
|
||||
setSaveStatus(`❌ ${t("resetFailed")}: ${error.message}`);
|
||||
}
|
||||
} catch (error) {
|
||||
setSaveStatus(`❌ Reset failed: ${error.message}`);
|
||||
}
|
||||
}, []);
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
const selectProviderFilter = useCallback((alias) => {
|
||||
setSelectedProvider((prev) => (prev === alias ? null : alias));
|
||||
@@ -312,12 +317,13 @@ function ProviderSection({
|
||||
saving,
|
||||
}) {
|
||||
const t = useTranslations("settings");
|
||||
const tGlobal = useTranslations();
|
||||
const pricedCount = Object.keys(pricingData).length;
|
||||
const authBadge =
|
||||
provider.authType === "oauth"
|
||||
? "OAuth"
|
||||
? tGlobal("providers.oauthLabel")
|
||||
: provider.authType === "apikey"
|
||||
? "API Key"
|
||||
? tGlobal("providers.apiKeyLabel")
|
||||
: provider.authType;
|
||||
|
||||
return (
|
||||
@@ -355,7 +361,7 @@ function ProviderSection({
|
||||
<div className="flex items-center gap-3">
|
||||
{isEdited && <span className="text-yellow-500 text-xs font-medium">{t("unsaved")}</span>}
|
||||
<span className="text-text-muted text-xs">
|
||||
{pricedCount}/{provider.modelCount} priced
|
||||
{pricedCount}/{provider.modelCount} {t("withPricing")}
|
||||
</span>
|
||||
<div className="w-16 h-1.5 bg-bg-subtle rounded-full overflow-hidden">
|
||||
<div
|
||||
@@ -378,7 +384,7 @@ function ProviderSection({
|
||||
{/* Actions bar */}
|
||||
<div className="flex items-center justify-between px-4 py-2 bg-bg-subtle/50">
|
||||
<span className="text-xs text-text-muted">
|
||||
{provider.modelCount} models • {pricedCount} with pricing configured
|
||||
{provider.modelCount} {t("models")} • {pricedCount} {t("withPricing")}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@@ -411,7 +417,7 @@ function ProviderSection({
|
||||
<th className="px-4 py-2 text-left font-semibold">{t("model")}</th>
|
||||
{PRICING_FIELDS.map((field) => (
|
||||
<th key={field} className="px-2 py-2 text-right font-semibold w-24">
|
||||
{FIELD_LABELS[field]}
|
||||
{t(FIELD_LABEL_KEYS[field])}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
@@ -437,6 +443,7 @@ function ProviderSection({
|
||||
// ── Model Row ────────────────────────────────────────────────────────────
|
||||
|
||||
function ModelRow({ model, pricing, onPricingChange }) {
|
||||
const t = useTranslations("settings");
|
||||
const hasPricing = pricing && Object.values(pricing).some((v: any) => v > 0);
|
||||
|
||||
return (
|
||||
@@ -449,7 +456,7 @@ function ModelRow({ model, pricing, onPricingChange }) {
|
||||
<span className="font-medium text-xs">{model.name}</span>
|
||||
{model.custom && (
|
||||
<span className="px-1 py-0.5 text-[8px] font-bold bg-blue-500/15 text-blue-400 border border-blue-500/20 rounded uppercase">
|
||||
custom
|
||||
{t("custom")}
|
||||
</span>
|
||||
)}
|
||||
<span className="text-text-muted text-[10px] opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
|
||||
@@ -93,7 +93,7 @@ function ProviderProfilesCard({ profiles, onSave, saving }) {
|
||||
<h2 className="text-lg font-bold">{t("providerProfiles")}</h2>
|
||||
</div>
|
||||
{editMode ? (
|
||||
<div className="flex gap-2">
|
||||
<div className="flex flex-wrap justify-end gap-2">
|
||||
<Button size="sm" variant="secondary" onClick={() => setEditMode(false)}>
|
||||
{tc("cancel")}
|
||||
</Button>
|
||||
@@ -217,7 +217,7 @@ function RateLimitCard({ rateLimitStatus, defaults, onSaveDefaults, saving }) {
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider mb-3 text-text-muted">
|
||||
{t("defaultSafetyNet")}
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
||||
{[
|
||||
{ key: "requestsPerMinute", label: t("rpm") },
|
||||
{ key: "minTimeBetweenRequests", label: t("minGap"), format: formatMs },
|
||||
|
||||
@@ -99,7 +99,10 @@ export default function RoutingTab() {
|
||||
<h3 className="text-lg font-semibold">{t("routingStrategy")}</h3>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-2 mb-4" style={{ gridAutoRows: "1fr" }}>
|
||||
<div
|
||||
className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-2 mb-4"
|
||||
style={{ gridAutoRows: "1fr" }}
|
||||
>
|
||||
{STRATEGIES.map((s) => (
|
||||
<button
|
||||
key={s.value}
|
||||
@@ -172,18 +175,18 @@ export default function RoutingTab() {
|
||||
{aliases.map((a, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="flex items-center justify-between px-3 py-2 rounded-lg bg-surface/30 border border-border/20"
|
||||
className="flex items-center justify-between gap-2 px-3 py-2 rounded-lg bg-surface/30 border border-border/20"
|
||||
>
|
||||
<div className="flex items-center gap-3 text-sm">
|
||||
<span className="font-mono text-purple-400">{a.pattern}</span>
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||
<span className="font-mono text-purple-400 break-all">{a.pattern}</span>
|
||||
<span className="material-symbols-outlined text-[14px] text-text-muted">
|
||||
arrow_forward
|
||||
</span>
|
||||
<span className="font-mono text-text-main">{a.target}</span>
|
||||
<span className="font-mono text-text-main break-all">{a.target}</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeAlias(i)}
|
||||
className="text-text-muted hover:text-red-400 transition-colors"
|
||||
className="shrink-0 text-text-muted hover:text-red-400 transition-colors"
|
||||
>
|
||||
<span className="material-symbols-outlined text-[16px]">close</span>
|
||||
</button>
|
||||
@@ -192,7 +195,7 @@ export default function RoutingTab() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 items-end">
|
||||
<div className="flex flex-col sm:flex-row gap-2 items-stretch sm:items-end">
|
||||
<div className="flex-1">
|
||||
<Input
|
||||
label={t("pattern")}
|
||||
@@ -209,7 +212,12 @@ export default function RoutingTab() {
|
||||
onChange={(e) => setNewTarget(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button size="sm" variant="primary" onClick={addAlias} className="mb-[2px]">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
onClick={addAlias}
|
||||
className="mb-[2px] sm:w-auto w-full"
|
||||
>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -92,14 +92,12 @@ export default function SystemPromptTab() {
|
||||
disabled={loading}
|
||||
/>
|
||||
<div className="absolute bottom-2 right-3 text-xs text-text-muted/60 tabular-nums">
|
||||
{config.prompt.length} chars
|
||||
{t("chars", { count: config.prompt.length })}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted/70 flex items-center gap-1.5">
|
||||
<span className="material-symbols-outlined text-[14px]">info</span>
|
||||
{t("systemPromptHint")} Send{" "}
|
||||
<code className="px-1 py-0.5 rounded bg-surface/50">_skipSystemPrompt: true</code> in a
|
||||
request to bypass.
|
||||
{t("systemPromptHint")}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -259,7 +259,7 @@ export default function SystemStorageTab() {
|
||||
</div>
|
||||
|
||||
{/* Storage info grid */}
|
||||
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
|
||||
<div className="p-3 rounded-lg bg-bg border border-border">
|
||||
<p className="text-[11px] text-text-muted uppercase tracking-wide mb-1">
|
||||
{t("databasePath")}
|
||||
@@ -277,7 +277,7 @@ export default function SystemStorageTab() {
|
||||
</div>
|
||||
|
||||
{/* Export / Import */}
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-4">
|
||||
<Button variant="outline" size="sm" onClick={handleExport} loading={exportLoading}>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1" aria-hidden="true">
|
||||
download
|
||||
|
||||
@@ -167,7 +167,7 @@ export default function ThinkingBudgetTab() {
|
||||
<div className="p-4 rounded-lg bg-surface/30 border border-border/30">
|
||||
<p className="text-sm font-medium mb-3">{t("baseEffortLevel")}</p>
|
||||
<p className="text-xs text-text-muted mb-3">{t("adaptiveHint")}</p>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
{EFFORTS.map((e) => (
|
||||
<button
|
||||
key={e.value}
|
||||
|
||||
@@ -34,34 +34,36 @@ export default function SettingsPage() {
|
||||
const activeTab = userSelectedTab || tabs.find((t) => t.id === tabParam)?.id || "general";
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto">
|
||||
<div className="max-w-2xl mx-auto min-w-0">
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Tab navigation */}
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={t("settingsSectionsAria")}
|
||||
className="inline-flex items-center p-1 rounded-lg bg-black/5 dark:bg-white/5 self-start"
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
role="tab"
|
||||
aria-selected={activeTab === tab.id}
|
||||
tabIndex={activeTab === tab.id ? 0 : -1}
|
||||
onClick={() => setUserSelectedTab(tab.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all text-sm",
|
||||
activeTab === tab.id
|
||||
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
||||
: "text-text-muted hover:text-text-main"
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]" aria-hidden="true">
|
||||
{tab.icon}
|
||||
</span>
|
||||
<span className="hidden sm:inline">{t(tab.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
<div className="w-full overflow-x-auto pb-1">
|
||||
<div
|
||||
role="tablist"
|
||||
aria-label={t("settingsSectionsAria")}
|
||||
className="inline-flex items-center p-1 rounded-lg bg-black/5 dark:bg-white/5 min-w-max"
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
role="tab"
|
||||
aria-selected={activeTab === tab.id}
|
||||
tabIndex={activeTab === tab.id ? 0 : -1}
|
||||
onClick={() => setUserSelectedTab(tab.id)}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-md font-medium transition-all text-sm",
|
||||
activeTab === tab.id
|
||||
? "bg-white dark:bg-white/10 text-text-main shadow-sm"
|
||||
: "text-text-muted hover:text-text-main"
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]" aria-hidden="true">
|
||||
{tab.icon}
|
||||
</span>
|
||||
<span className="hidden sm:inline whitespace-nowrap">{t(tab.labelKey)}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tab contents */}
|
||||
|
||||
@@ -26,9 +26,9 @@ export default function TranslatorPageClient() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="p-8 space-y-6">
|
||||
<div className="p-4 sm:p-8 space-y-6 min-w-0">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 min-w-0">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-text-main flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-primary text-[28px]">translate</span>
|
||||
@@ -38,7 +38,15 @@ export default function TranslatorPageClient() {
|
||||
{modeDescriptions[mode] || t("modeDescriptionFallback")}
|
||||
</p>
|
||||
</div>
|
||||
<SegmentedControl options={modes} value={mode} onChange={setMode} size="md" />
|
||||
<div className="w-full sm:w-auto overflow-x-auto">
|
||||
<SegmentedControl
|
||||
options={modes}
|
||||
value={mode}
|
||||
onChange={setMode}
|
||||
size="md"
|
||||
className="min-w-max"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode Content */}
|
||||
|
||||
@@ -248,7 +248,7 @@ export default function ChatTesterMode() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-4 min-w-0">
|
||||
{/* Info Banner */}
|
||||
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
|
||||
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
|
||||
@@ -264,7 +264,7 @@ export default function ChatTesterMode() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 min-w-0">
|
||||
{/* Left: Chat Interface */}
|
||||
<div className="space-y-4">
|
||||
{/* Controls */}
|
||||
@@ -349,7 +349,7 @@ export default function ChatTesterMode() {
|
||||
? t("youWithFormat", { format: FORMAT_META[clientFormat]?.label })
|
||||
: t("assistant")}
|
||||
</p>
|
||||
<p className="whitespace-pre-wrap">{msg.content}</p>
|
||||
<p className="whitespace-pre-wrap break-words">{msg.content}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -54,7 +54,7 @@ export default function LiveMonitorMode() {
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-5 min-w-0">
|
||||
{/* Info Banner */}
|
||||
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
|
||||
<span
|
||||
@@ -212,7 +212,7 @@ export default function LiveMonitorMode() {
|
||||
{tgtMeta.label}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="py-2 pr-4 text-xs font-mono text-text-muted">
|
||||
<td className="py-2 pr-4 text-xs font-mono text-text-muted break-all">
|
||||
{event.model || notAvailable}
|
||||
</td>
|
||||
<td className="py-2 pr-4">
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function PlaygroundMode() {
|
||||
onClick={handleTranslate}
|
||||
loading={translating}
|
||||
disabled={!inputContent.trim() || translating}
|
||||
className="whitespace-nowrap"
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{t("translateAction")}
|
||||
</Button>
|
||||
|
||||
@@ -107,7 +107,7 @@ export default function TestBenchMode() {
|
||||
...prev,
|
||||
[scenario.id]: {
|
||||
status: "error",
|
||||
error: errData.error || `HTTP ${sendRes.status}`,
|
||||
error: errData.error || t("errorMessage", { message: `HTTP ${sendRes.status}` }),
|
||||
latency,
|
||||
httpStatus: sendRes.status,
|
||||
},
|
||||
@@ -152,7 +152,7 @@ export default function TestBenchMode() {
|
||||
const srcMeta = FORMAT_META[sourceFormat] || FORMAT_META.openai;
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
<div className="space-y-5 min-w-0">
|
||||
{/* Info Banner */}
|
||||
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
|
||||
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
|
||||
@@ -167,7 +167,7 @@ export default function TestBenchMode() {
|
||||
{/* Controls */}
|
||||
<Card>
|
||||
<div className="p-4 flex flex-col gap-4">
|
||||
<div className="flex flex-col sm:flex-row items-end gap-4">
|
||||
<div className="flex flex-col sm:flex-row items-end gap-4 min-w-0">
|
||||
<div className="flex-1 w-full">
|
||||
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
|
||||
{t("source")}
|
||||
@@ -206,6 +206,7 @@ export default function TestBenchMode() {
|
||||
onClick={handleRunAll}
|
||||
loading={runningAll}
|
||||
disabled={runningAll}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{t("runAllTests")}
|
||||
</Button>
|
||||
|
||||
@@ -125,11 +125,13 @@ export default function Features() {
|
||||
</span>
|
||||
</div>
|
||||
<h3
|
||||
className={`text-lg font-bold mb-2 ${feature.colors.titleHover} transition-colors`}
|
||||
className={`text-lg font-bold mb-2 break-words ${feature.colors.titleHover} transition-colors`}
|
||||
>
|
||||
{t(feature.titleKey)}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-400 leading-relaxed">{t(feature.descKey)}</p>
|
||||
<p className="text-sm text-gray-400 leading-relaxed break-words">
|
||||
{t(feature.descKey)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -49,8 +49,8 @@ export default function FlowAnimation() {
|
||||
}, [providers.length]);
|
||||
|
||||
return (
|
||||
<div className="mt-16 w-full max-w-4xl">
|
||||
<div className="relative h-[360px] hidden md:flex items-center justify-center animate-[float_6s_ease-in-out_infinite]">
|
||||
<div className="mt-16 w-full max-w-4xl overflow-hidden">
|
||||
<div className="relative h-[360px] hidden md:flex items-center justify-center animate-[float_6s_ease-in-out_infinite] overflow-hidden">
|
||||
{/* OmniRoute Hub - Center */}
|
||||
<div className="relative z-20 w-32 h-32 rounded-full bg-[#111520] border-2 border-[#E54D5E] shadow-[0_0_40px_rgba(229,77,94,0.3)] flex flex-col items-center justify-center gap-1 group cursor-pointer hover:scale-105 transition-transform duration-500">
|
||||
<span className="material-symbols-outlined text-4xl text-[#E54D5E]" aria-hidden="true">
|
||||
@@ -158,7 +158,7 @@ export default function FlowAnimation() {
|
||||
{providers.map((provider, idx) => (
|
||||
<div
|
||||
key={provider.id}
|
||||
className={`px-4 py-2 rounded-lg ${provider.color} ${provider.textColor} flex items-center justify-center font-bold text-xs shadow-lg hover:scale-110 transition-all cursor-help min-w-[140px] ${
|
||||
className={`px-3 py-2 rounded-lg ${provider.color} ${provider.textColor} flex items-center justify-center font-bold text-[11px] leading-tight text-center shadow-lg hover:scale-110 transition-all cursor-help min-w-[110px] max-w-[140px] ${
|
||||
activeFlow === idx ? "ring-4 ring-[#E54D5E]/50 scale-110" : ""
|
||||
}`}
|
||||
title={provider.name}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function Footer() {
|
||||
</div>
|
||||
<h3 className="text-white text-lg font-bold">{t("brandName")}</h3>
|
||||
</div>
|
||||
<p className="text-gray-500 text-sm max-w-xs mb-6">{t("footerTagline")}</p>
|
||||
<p className="text-gray-500 text-sm max-w-xs mb-6 break-words">{t("footerTagline")}</p>
|
||||
<div className="flex gap-4">
|
||||
<a
|
||||
className="text-gray-400 hover:text-white transition-colors"
|
||||
@@ -101,7 +101,9 @@ export default function Footer() {
|
||||
|
||||
{/* Bottom */}
|
||||
<div className="border-t border-[#2D333B] pt-8 flex flex-col md:flex-row justify-between items-center gap-4">
|
||||
<p className="text-gray-600 text-sm">{t("copyright", { year })}</p>
|
||||
<p className="text-gray-600 text-sm break-words text-center md:text-left">
|
||||
{t("copyright", { year })}
|
||||
</p>
|
||||
<div className="flex gap-6">
|
||||
<a
|
||||
className="text-gray-600 hover:text-white text-sm transition-colors"
|
||||
|
||||
@@ -26,7 +26,7 @@ export default function GetStarted() {
|
||||
<p className="text-gray-400 text-lg mb-8">{t("getStartedDescription")}</p>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-4 min-w-0">
|
||||
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
|
||||
1
|
||||
</div>
|
||||
@@ -36,7 +36,7 @@ export default function GetStarted() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-4 min-w-0">
|
||||
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
|
||||
2
|
||||
</div>
|
||||
@@ -46,7 +46,7 @@ export default function GetStarted() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<div className="flex gap-4 min-w-0">
|
||||
<div className="flex-none w-8 h-8 rounded-full bg-[#E54D5E]/20 text-[#E54D5E] flex items-center justify-center font-bold">
|
||||
3
|
||||
</div>
|
||||
@@ -72,13 +72,13 @@ export default function GetStarted() {
|
||||
</div>
|
||||
|
||||
{/* Terminal content */}
|
||||
<div className="p-6 font-mono text-sm leading-relaxed overflow-x-auto">
|
||||
<div className="p-6 font-mono text-sm leading-relaxed overflow-x-hidden">
|
||||
<div
|
||||
className="flex items-center gap-2 mb-4 group cursor-pointer"
|
||||
className="flex items-center gap-2 mb-4 group cursor-pointer min-w-0"
|
||||
onClick={() => handleCopy(command)}
|
||||
>
|
||||
<span className="text-green-400">$</span>
|
||||
<span className="text-white">{command}</span>
|
||||
<span className="text-white break-all">{command}</span>
|
||||
<span className="ml-auto text-gray-500 text-xs opacity-0 group-hover:opacity-100">
|
||||
{copied ? t("copied") : t("copy")}
|
||||
</span>
|
||||
@@ -88,10 +88,10 @@ export default function GetStarted() {
|
||||
<span className="text-[#E54D5E]">></span> {t("startingOmniRoute")}
|
||||
<br />
|
||||
<span className="text-[#E54D5E]">></span> {t("serverRunningOnLabel")}{" "}
|
||||
<span className="text-blue-400">{endpoint}</span>
|
||||
<span className="text-blue-400 break-all">{endpoint}</span>
|
||||
<br />
|
||||
<span className="text-[#E54D5E]">></span> {t("dashboardLabel")}:{" "}
|
||||
<span className="text-blue-400">{dashboardUrl}</span>
|
||||
<span className="text-blue-400 break-all">{dashboardUrl}</span>
|
||||
<br />
|
||||
<span className="text-green-400">></span> {t("readyToRoute")}
|
||||
</div>
|
||||
@@ -104,10 +104,10 @@ export default function GetStarted() {
|
||||
<span className="text-purple-400">{t("dataLocation")}</span>
|
||||
<br />
|
||||
<span className="text-gray-500">{t("dataLocationMacLinux")}</span>{" "}
|
||||
~/.omniroute/db.json
|
||||
<span className="break-all">~/.omniroute/db.json</span>
|
||||
<br />
|
||||
<span className="text-gray-500">{t("dataLocationWindows")}</span>{" "}
|
||||
%APPDATA%/omniroute/db.json
|
||||
<span className="break-all">%APPDATA%/omniroute/db.json</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,7 +7,7 @@ export default function HeroSection() {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<section className="relative pt-32 pb-20 px-6 min-h-[90vh] flex flex-col items-center justify-center overflow-hidden">
|
||||
<section className="relative pt-32 pb-20 px-4 sm:px-6 min-h-[90vh] flex flex-col items-center justify-center overflow-hidden">
|
||||
{/* Glow effect */}
|
||||
<div className="absolute top-0 left-1/2 -translate-x-1/2 w-[1000px] h-[500px] bg-[#E54D5E]/10 rounded-full blur-[120px] pointer-events-none"></div>
|
||||
|
||||
@@ -19,13 +19,13 @@ export default function HeroSection() {
|
||||
</div>
|
||||
|
||||
{/* Main heading */}
|
||||
<h1 className="text-5xl md:text-7xl font-black leading-[1.1] tracking-tight">
|
||||
<h1 className="text-4xl sm:text-5xl md:text-7xl font-black leading-[1.1] tracking-tight break-words">
|
||||
{t("oneEndpoint")} <br />
|
||||
<span className="text-[#E54D5E]">{t("allProviders")}</span>
|
||||
</h1>
|
||||
|
||||
{/* Description */}
|
||||
<p className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto font-light">
|
||||
<p className="text-lg md:text-xl text-gray-400 max-w-2xl mx-auto font-light break-words">
|
||||
{t("heroDescription")}
|
||||
</p>
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function HeroSection() {
|
||||
<div className="flex flex-wrap items-center justify-center gap-4 w-full">
|
||||
<button
|
||||
onClick={() => router.push("/dashboard")}
|
||||
className="h-12 px-8 rounded-lg bg-[#E54D5E] hover:bg-[#C93D4E] text-white text-base font-bold transition-all shadow-[0_0_15px_rgba(229,77,94,0.4)] flex items-center gap-2"
|
||||
className="w-full sm:w-auto h-12 px-8 rounded-lg bg-[#E54D5E] hover:bg-[#C93D4E] text-white text-base font-bold transition-all shadow-[0_0_15px_rgba(229,77,94,0.4)] flex items-center justify-center gap-2"
|
||||
>
|
||||
<span className="material-symbols-outlined" aria-hidden="true">
|
||||
rocket_launch
|
||||
@@ -44,7 +44,7 @@ export default function HeroSection() {
|
||||
href="https://github.com/diegosouzapw/OmniRoute"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="h-12 px-8 rounded-lg border border-[#2D333B] bg-[#111520] hover:bg-[#2D333B] text-white text-base font-bold transition-all flex items-center gap-2"
|
||||
className="w-full sm:w-auto h-12 px-8 rounded-lg border border-[#2D333B] bg-[#111520] hover:bg-[#2D333B] text-white text-base font-bold transition-all flex items-center justify-center gap-2"
|
||||
>
|
||||
<span className="material-symbols-outlined" aria-hidden="true">
|
||||
code
|
||||
|
||||
@@ -24,8 +24,8 @@ export default function HowItWorks() {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-2">{t("howItWorksStep1Title")}</h3>
|
||||
<p className="text-sm text-gray-400">{t("howItWorksStep1Description")}</p>
|
||||
<h3 className="text-xl font-bold mb-2 break-words">{t("howItWorksStep1Title")}</h3>
|
||||
<p className="text-sm text-gray-400 break-words">{t("howItWorksStep1Description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -40,8 +40,10 @@ export default function HowItWorks() {
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-2 text-[#E54D5E]">{t("howItWorksStep2Title")}</h3>
|
||||
<p className="text-sm text-gray-400">{t("howItWorksStep2Description")}</p>
|
||||
<h3 className="text-xl font-bold mb-2 text-[#E54D5E] break-words">
|
||||
{t("howItWorksStep2Title")}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-400 break-words">{t("howItWorksStep2Description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -56,8 +58,8 @@ export default function HowItWorks() {
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-xl font-bold mb-2">{t("howItWorksStep3Title")}</h3>
|
||||
<p className="text-sm text-gray-400">{t("howItWorksStep3Description")}</p>
|
||||
<h3 className="text-xl font-bold mb-2 break-words">{t("howItWorksStep3Title")}</h3>
|
||||
<p className="text-sm text-gray-400 break-words">{t("howItWorksStep3Description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -11,18 +11,20 @@ export default function Navigation() {
|
||||
|
||||
return (
|
||||
<nav className="fixed top-0 z-50 w-full bg-[#0B0E14]/80 backdrop-blur-md border-b border-[#2D333B]">
|
||||
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 h-16 flex items-center justify-between gap-3">
|
||||
{/* Logo */}
|
||||
<button
|
||||
type="button"
|
||||
className="flex items-center gap-3 cursor-pointer bg-transparent border-none p-0"
|
||||
className="flex items-center gap-3 cursor-pointer bg-transparent border-none p-0 min-w-0"
|
||||
onClick={() => router.push("/")}
|
||||
aria-label={t("navigateHome")}
|
||||
>
|
||||
<div className="size-8 rounded bg-linear-to-br from-[#E54D5E] to-[#C93D4E] flex items-center justify-center text-white">
|
||||
<OmniRouteLogo size={20} className="text-white" />
|
||||
</div>
|
||||
<h2 className="text-white text-xl font-bold tracking-tight">{t("brandName")}</h2>
|
||||
<h2 className="text-white text-lg sm:text-xl font-bold tracking-tight truncate sm:max-w-none max-w-[160px]">
|
||||
{t("brandName")}
|
||||
</h2>
|
||||
</button>
|
||||
|
||||
{/* Desktop menu */}
|
||||
@@ -59,7 +61,7 @@ export default function Navigation() {
|
||||
</div>
|
||||
|
||||
{/* CTA + Mobile menu */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2 sm:gap-4 shrink-0">
|
||||
<button
|
||||
onClick={() => router.push("/dashboard")}
|
||||
className="hidden sm:flex h-9 items-center justify-center rounded-lg px-4 bg-[#E54D5E] hover:bg-[#C93D4E] transition-all text-white text-sm font-bold shadow-[0_0_15px_rgba(229,77,94,0.4)] hover:shadow-[0_0_20px_rgba(229,77,94,0.6)]"
|
||||
|
||||
@@ -63,11 +63,15 @@ export default function LandingPage() {
|
||||
<Features />
|
||||
|
||||
{/* CTA Section */}
|
||||
<section className="py-32 px-6 relative overflow-hidden">
|
||||
<section className="py-24 sm:py-32 px-4 sm:px-6 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-linear-to-t from-[#E54D5E]/5 to-transparent pointer-events-none"></div>
|
||||
<div className="max-w-4xl mx-auto text-center relative z-10">
|
||||
<h2 className="text-4xl md:text-5xl font-black mb-6">{t("ctaTitle")}</h2>
|
||||
<p className="text-xl text-gray-400 mb-10 max-w-2xl mx-auto">{t("ctaDescription")}</p>
|
||||
<h2 className="text-3xl sm:text-4xl md:text-5xl font-black mb-6 break-words">
|
||||
{t("ctaTitle")}
|
||||
</h2>
|
||||
<p className="text-lg sm:text-xl text-gray-400 mb-10 max-w-2xl mx-auto break-words">
|
||||
{t("ctaDescription")}
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
|
||||
<button
|
||||
onClick={() => router.push("/dashboard")}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { ThemeProvider } from "@/shared/components/ThemeProvider";
|
||||
import "@/lib/initCloudSync"; // Auto-initialize cloud sync
|
||||
import { NextIntlClientProvider } from "next-intl";
|
||||
import { getMessages, getLocale } from "next-intl/server";
|
||||
import { RTL_LOCALES } from "@/i18n/config";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -23,9 +24,10 @@ export const metadata = {
|
||||
export default async function RootLayout({ children }) {
|
||||
const locale = await getLocale();
|
||||
const messages = await getMessages();
|
||||
const isRtl = RTL_LOCALES.includes(locale as (typeof RTL_LOCALES)[number]);
|
||||
|
||||
return (
|
||||
<html lang={locale} suppressHydrationWarning>
|
||||
<html lang={locale} dir={isRtl ? "rtl" : "ltr"} suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
|
||||
@@ -1,4 +1,35 @@
|
||||
export const LOCALES = ["en", "pt-BR"] as const;
|
||||
export const LOCALES = [
|
||||
"en",
|
||||
"pt-BR",
|
||||
"es",
|
||||
"fr",
|
||||
"it",
|
||||
"ru",
|
||||
"zh-CN",
|
||||
"de",
|
||||
"in",
|
||||
"th",
|
||||
"uk-UA",
|
||||
"ar",
|
||||
"ja",
|
||||
"vi",
|
||||
"bg",
|
||||
"da",
|
||||
"fi",
|
||||
"he",
|
||||
"hu",
|
||||
"id",
|
||||
"ko",
|
||||
"ms",
|
||||
"nl",
|
||||
"no",
|
||||
"pt",
|
||||
"ro",
|
||||
"pl",
|
||||
"sk",
|
||||
"sv",
|
||||
"phi",
|
||||
] as const;
|
||||
export type Locale = (typeof LOCALES)[number];
|
||||
export const DEFAULT_LOCALE: Locale = "en";
|
||||
|
||||
@@ -10,6 +41,36 @@ export const LANGUAGES: readonly {
|
||||
}[] = [
|
||||
{ code: "en", label: "EN", name: "English", flag: "🇺🇸" },
|
||||
{ code: "pt-BR", label: "PT-BR", name: "Português (Brasil)", flag: "🇧🇷" },
|
||||
{ code: "es", label: "ES", name: "Español", flag: "🇪🇸" },
|
||||
{ code: "fr", label: "FR", name: "Français", flag: "🇫🇷" },
|
||||
{ code: "it", label: "IT", name: "Italiano", flag: "🇮🇹" },
|
||||
{ code: "ru", label: "RU", name: "Русский", flag: "🇷🇺" },
|
||||
{ code: "zh-CN", label: "ZH-CN", name: "中文 (简体)", flag: "🇨🇳" },
|
||||
{ code: "de", label: "DE", name: "Deutsch", flag: "🇩🇪" },
|
||||
{ code: "in", label: "IN", name: "हिन्दी", flag: "🇮🇳" },
|
||||
{ code: "th", label: "TH", name: "ไทย", flag: "🇹🇭" },
|
||||
{ code: "uk-UA", label: "UK-UA", name: "Українська", flag: "🇺🇦" },
|
||||
{ code: "ar", label: "AR", name: "العربية", flag: "🇸🇦" },
|
||||
{ code: "ja", label: "JA", name: "日本語", flag: "🇯🇵" },
|
||||
{ code: "vi", label: "VI", name: "Tiếng Việt", flag: "🇻🇳" },
|
||||
{ code: "bg", label: "BG", name: "Български", flag: "🇧🇬" },
|
||||
{ code: "da", label: "DA", name: "Dansk", flag: "🇩🇰" },
|
||||
{ code: "fi", label: "FI", name: "Suomi", flag: "🇫🇮" },
|
||||
{ code: "he", label: "HE", name: "עברית", flag: "🇮🇱" },
|
||||
{ code: "hu", label: "HU", name: "Magyar", flag: "🇭🇺" },
|
||||
{ code: "id", label: "ID", name: "Bahasa Indonesia", flag: "🇮🇩" },
|
||||
{ code: "ko", label: "KO", name: "한국어", flag: "🇰🇷" },
|
||||
{ code: "ms", label: "MS", name: "Bahasa Melayu", flag: "🇲🇾" },
|
||||
{ code: "nl", label: "NL", name: "Nederlands", flag: "🇳🇱" },
|
||||
{ code: "no", label: "NO", name: "Norsk", flag: "🇳🇴" },
|
||||
{ code: "pt", label: "PT", name: "Português (Portugal)", flag: "🇵🇹" },
|
||||
{ code: "ro", label: "RO", name: "Română", flag: "🇷🇴" },
|
||||
{ code: "pl", label: "PL", name: "Polski", flag: "🇵🇱" },
|
||||
{ code: "sk", label: "SK", name: "Slovenčina", flag: "🇸🇰" },
|
||||
{ code: "sv", label: "SV", name: "Svenska", flag: "🇸🇪" },
|
||||
{ code: "phi", label: "PHI", name: "Filipino", flag: "🇵🇭" },
|
||||
] as const;
|
||||
|
||||
export const RTL_LOCALES = ["ar", "he"] as const;
|
||||
|
||||
export const LOCALE_COOKIE = "NEXT_LOCALE";
|
||||
|
||||
Reference in New Issue
Block a user