mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 18:52:18 +03:00
fix(i18n): preserve remaining Vietnamese localization (#7935)
* fix(i18n): preserve remaining Vietnamese localization * chore(quality): rebaseline file-size cap for 9 dashboard components (i18n wiring) Restoring the Vietnamese localization on 9 dashboard components (useTranslations wiring + t()/tc() call-site swaps for previously hardcoded strings) grows each file by a small, irreducible amount. Bumps the frozen file-size-baseline.json caps to match, with a justification entry per the project's own ratchet policy. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * fix(i18n): wire weekday localization + add missing qwen CLI description Two gaps left by this PR's own new contract tests, caught while reconciling the branch against the release tip: - CostOverviewTab.tsx added formatWeekdayLabel() but never called it; the Weekly Usage Pattern chart still showed raw English day abbreviations regardless of locale. Now maps weeklyPattern rows through it before handing them to WeeklyPatternCard. - cliTools.toolDescriptions was missing an entry for "qwen" (a baseUrlSupport:"full" tool) in both en.json and vi.json, failing the PR's own cli-catalog-display-contract.test.ts. Covered by the PR's existing tests/unit/dashboard-localization-contract.test.ts and tests/unit/cli-catalog-display-contract.test.ts (both now pass). Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> * i18n(vi): backfill the 4 proxySubscription keys #7299 added to en.json #7299 (proxy subscriptions) merged while this branch was rebasing, adding settings.proxySubscriptionsTab and settings.proxySubscription.error.{LOCAL_CORE_ENDPOINT_INVALID, NEEDS_CORE_NOT_CONFIGURED,NO_USABLE_NODES} to en.json. This PR's own i18n-vi-completeness contract asserts full en↔vi key parity, so the merge of the current release tip surfaced them as missing. Adds the Vietnamese translations, keeping the SS/VMess/Trojan/VLESS/SOCKS5 technical terms verbatim. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: nguyenha935 <208228297+nguyenha935@users.noreply.github.com> Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Co-authored-by: nguyenha935 <nguyenha935@users.noreply.github.com>
This commit is contained in:
@@ -6,10 +6,6 @@
|
||||
// (parent owns the state + persistence). All mutations go through the pure
|
||||
// `compressionPipelineModel` so invariants (valid intensity, non-empty pipeline) hold.
|
||||
//
|
||||
// Hydration note: this lives under the combos screen, which deliberately uses NO
|
||||
// `useTranslations` (an earlier redesign failed to hydrate on the production build with a
|
||||
// page-level `useTranslations`). Strings are literal English to match `CompressionHub`.
|
||||
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
@@ -26,6 +22,7 @@ import {
|
||||
sortableKeyboardCoordinates,
|
||||
} from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { useTranslations } from "next-intl";
|
||||
import {
|
||||
allowedIntensities,
|
||||
addLayer,
|
||||
@@ -57,6 +54,7 @@ function SortableRow(props: {
|
||||
onPatch: (patch: Partial<PipelineStep>) => void;
|
||||
onRemove: () => void;
|
||||
}) {
|
||||
const t = useTranslations("contextCombos");
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: props.id,
|
||||
});
|
||||
@@ -74,7 +72,7 @@ function SortableRow(props: {
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Drag to reorder step"
|
||||
aria-label={t("dragToReorder")}
|
||||
data-testid={`pipeline-drag-${props.index}`}
|
||||
className="cursor-grab rounded-lg border border-border px-2 py-2 text-sm text-text-muted"
|
||||
{...attributes}
|
||||
@@ -83,7 +81,7 @@ function SortableRow(props: {
|
||||
⠿
|
||||
</button>
|
||||
<select
|
||||
aria-label="Engine"
|
||||
aria-label={t("engine")}
|
||||
value={props.step.engine}
|
||||
onChange={(event) => props.onPatch({ engine: event.target.value })}
|
||||
className={SELECT_CLASS}
|
||||
@@ -95,7 +93,7 @@ function SortableRow(props: {
|
||||
))}
|
||||
</select>
|
||||
<select
|
||||
aria-label="Intensity"
|
||||
aria-label={t("intensity")}
|
||||
value={props.step.intensity ?? ""}
|
||||
onChange={(event) => props.onPatch({ intensity: event.target.value })}
|
||||
className={SELECT_CLASS}
|
||||
@@ -113,13 +111,14 @@ function SortableRow(props: {
|
||||
data-testid={`pipeline-remove-${props.index}`}
|
||||
className="rounded-lg border border-border px-3 py-2 text-sm text-text-main disabled:opacity-50"
|
||||
>
|
||||
Remove
|
||||
{t("removeStep")}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CompressionPipelineEditor({ steps, onChange, engineIntensities }: Props) {
|
||||
const t = useTranslations("contextCombos");
|
||||
const sensors = useSensors(
|
||||
useSensor(PointerSensor),
|
||||
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
|
||||
@@ -141,14 +140,14 @@ export function CompressionPipelineEditor({ steps, onChange, engineIntensities }
|
||||
return (
|
||||
<div className="space-y-3" data-testid="compression-pipeline-editor">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-sm font-semibold text-text-main">Pipeline</h3>
|
||||
<h3 className="text-sm font-semibold text-text-main">{t("pipeline")}</h3>
|
||||
<button
|
||||
type="button"
|
||||
data-testid="pipeline-add-step"
|
||||
onClick={() => onChange(addLayer(steps, { engine: firstEngine }, engineIntensities))}
|
||||
className="rounded-lg border border-border px-3 py-1.5 text-xs text-text-main"
|
||||
>
|
||||
Add step
|
||||
{t("addStep")}
|
||||
</button>
|
||||
</div>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import type { EngineConfigField } from "@omniroute/open-sse/services/compression/engines/types";
|
||||
import { EngineConfigForm } from "@/shared/components/compression/EngineConfigForm";
|
||||
|
||||
@@ -63,10 +64,9 @@ interface PreviewResult {
|
||||
|
||||
// ── Default preview sample ────────────────────────────────────────────────
|
||||
|
||||
const PREVIEW_SAMPLE =
|
||||
"The quick brown fox jumps over the lazy dog. " +
|
||||
"This is a sample message used to preview compression. " +
|
||||
"It contains enough text to show meaningful token savings.";
|
||||
const ENGINE_ICON_ALIASES: Record<string, string> = {
|
||||
brain: "psychology",
|
||||
};
|
||||
|
||||
// ── Sub-components ────────────────────────────────────────────────────────
|
||||
|
||||
@@ -79,7 +79,11 @@ function StatCard({ label, value }: { label: string; value: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function renderDiffSegment(segment: PreviewDiffSegment, index: number) {
|
||||
function renderDiffSegment(
|
||||
segment: PreviewDiffSegment,
|
||||
index: number,
|
||||
translateLabel: (label: string) => string
|
||||
) {
|
||||
const label = segment.type ?? "change";
|
||||
const text =
|
||||
segment.value ??
|
||||
@@ -93,7 +97,7 @@ function renderDiffSegment(segment: PreviewDiffSegment, index: number) {
|
||||
return (
|
||||
<div key={`${label}-${index}`} className="rounded border border-border bg-background p-2">
|
||||
<span className="mr-2 rounded bg-muted px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-text-muted">
|
||||
{label}
|
||||
{translateLabel(label)}
|
||||
</span>
|
||||
<span className="whitespace-pre-wrap break-words text-text">{text}</span>
|
||||
</div>
|
||||
@@ -103,6 +107,8 @@ function renderDiffSegment(segment: PreviewDiffSegment, index: number) {
|
||||
// ── Main component ────────────────────────────────────────────────────────
|
||||
|
||||
export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
const locale = useLocale();
|
||||
const t = useTranslations("compressionEngineConfig");
|
||||
// ── Data state ──────────────────────────────────────────────────────────
|
||||
const [engine, setEngine] = useState<EngineEntry | null>(null);
|
||||
const [configState, setConfigState] = useState<Record<string, unknown>>({});
|
||||
@@ -111,7 +117,7 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
// ── Preview state ───────────────────────────────────────────────────────
|
||||
const [previewText, setPreviewText] = useState(PREVIEW_SAMPLE);
|
||||
const [previewText, setPreviewText] = useState(() => t("previewSample"));
|
||||
const [preview, setPreview] = useState<PreviewResult | null>(null);
|
||||
const [previewError, setPreviewError] = useState<string | null>(null);
|
||||
const [previewLoading, setPreviewLoading] = useState(false);
|
||||
@@ -147,7 +153,7 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
if (enginesData) {
|
||||
foundEngine = enginesData.engines?.find((e) => e.id === engineId) ?? null;
|
||||
} else {
|
||||
setLoadError("Failed to load engine information.");
|
||||
setLoadError(t("loadFailed"));
|
||||
}
|
||||
|
||||
// Detailed config lives in the engine's settings sub-object (when it has one);
|
||||
@@ -174,7 +180,7 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [engineId]);
|
||||
}, [engineId, t]);
|
||||
|
||||
// ── Handlers ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -200,10 +206,10 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
body: JSON.stringify({ [subKey]: detail }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setSaveError("Failed to save configuration.");
|
||||
setSaveError(t("saveFailed"));
|
||||
}
|
||||
} catch {
|
||||
setSaveError("Failed to save configuration.");
|
||||
setSaveError(t("saveFailed"));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
@@ -226,10 +232,10 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
const data = (await res.json()) as PreviewResult;
|
||||
setPreview(data);
|
||||
} else {
|
||||
setPreviewError("Preview failed.");
|
||||
setPreviewError(t("previewFailed"));
|
||||
}
|
||||
} catch {
|
||||
setPreviewError("Preview failed.");
|
||||
setPreviewError(t("previewFailed"));
|
||||
} finally {
|
||||
setPreviewLoading(false);
|
||||
}
|
||||
@@ -239,20 +245,48 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center p-12 text-text-muted text-sm">Loading…</div>
|
||||
<div className="flex items-center justify-center p-12 text-text-muted text-sm">
|
||||
{t("loading")}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!engine) {
|
||||
return (
|
||||
<div className="p-6 text-sm text-text-muted">
|
||||
{loadError ?? `Engine "${engineId}" not found.`}
|
||||
{loadError ?? t("engineNotFound", { engine: engineId })}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const subtitle = engine.metadata?.description ?? engine.description;
|
||||
const visibleConfigSchema = engine.configSchema.filter((field) => field.key !== "enabled");
|
||||
const engineNameKey = `engines.${engineId}.name`;
|
||||
const engineDescriptionKey = `engines.${engineId}.description`;
|
||||
const engineName = t.has(engineNameKey) ? t(engineNameKey) : engine.name;
|
||||
const rawSubtitle = engine.metadata?.description ?? engine.description;
|
||||
const subtitle = t.has(engineDescriptionKey) ? t(engineDescriptionKey) : rawSubtitle;
|
||||
const visibleConfigSchema = engine.configSchema
|
||||
.filter((field) => field.key !== "enabled")
|
||||
.map((field) => {
|
||||
const engineFieldPrefix = `engineFields.${engineId}.${field.key}`;
|
||||
const fieldPrefix = `fields.${field.key}`;
|
||||
const labelKey = t.has(`${engineFieldPrefix}.label`)
|
||||
? `${engineFieldPrefix}.label`
|
||||
: `${fieldPrefix}.label`;
|
||||
const descriptionKey = t.has(`${engineFieldPrefix}.description`)
|
||||
? `${engineFieldPrefix}.description`
|
||||
: `${fieldPrefix}.description`;
|
||||
|
||||
return {
|
||||
...field,
|
||||
label: t.has(labelKey) ? t(labelKey) : field.label,
|
||||
description:
|
||||
field.description && t.has(descriptionKey) ? t(descriptionKey) : field.description,
|
||||
options: field.options?.map((option) => {
|
||||
const optionKey = `options.${field.key}.${option.value}`;
|
||||
return { ...option, label: t.has(optionKey) ? t(optionKey) : option.label };
|
||||
}),
|
||||
};
|
||||
});
|
||||
// Only engines with a dedicated settings sub-object can persist their detail here.
|
||||
const persistable = Boolean(SETTINGS_SUBOBJECT[engineId]);
|
||||
|
||||
@@ -266,10 +300,10 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
className="material-symbols-outlined text-[28px] leading-none text-text-muted"
|
||||
aria-hidden="true"
|
||||
>
|
||||
{engine.icon}
|
||||
{ENGINE_ICON_ALIASES[engine.icon] || engine.icon}
|
||||
</span>
|
||||
)}
|
||||
<h1 className="text-2xl font-bold text-text">{engine.name}</h1>
|
||||
<h1 className="text-2xl font-bold text-text">{engineName}</h1>
|
||||
</div>
|
||||
{subtitle && <p className="text-sm text-text-muted">{subtitle}</p>}
|
||||
</div>
|
||||
@@ -283,17 +317,17 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
{/* ── Panel pointer (on/off + level live there now) ── */}
|
||||
<div className="flex flex-col gap-1 rounded-lg border border-border bg-surface p-4">
|
||||
<p className="text-xs text-text-muted" data-testid="panel-pointer-notice">
|
||||
Turn this layer on/off and set its level in{" "}
|
||||
{t("panelPointerPrefix")}{" "}
|
||||
<a href="/dashboard/context/settings" className="underline hover:text-text">
|
||||
Compression Settings
|
||||
{t("compressionSettings")}
|
||||
</a>
|
||||
. This page edits its detailed configuration only.
|
||||
{t("panelPointerSuffix")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* ── Config form ── */}
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="text-sm font-semibold text-text">Configuration</h2>
|
||||
<h2 className="text-sm font-semibold text-text">{t("configuration")}</h2>
|
||||
{visibleConfigSchema.length > 0 ? (
|
||||
<EngineConfigForm
|
||||
schema={visibleConfigSchema}
|
||||
@@ -301,7 +335,7 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
onChange={setConfigState}
|
||||
/>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">No additional configuration.</p>
|
||||
<p className="text-sm text-text-muted">{t("noAdditionalConfiguration")}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
{persistable ? (
|
||||
@@ -310,12 +344,11 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
disabled={saving}
|
||||
className="px-4 py-1.5 rounded bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
{saving ? t("saving") : t("save")}
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-xs text-text-muted" data-testid="no-detail-store-notice">
|
||||
This layer is configured by the global settings; there is no per-engine override to
|
||||
save here yet.
|
||||
{t("globalSettingsOnly")}
|
||||
</p>
|
||||
)}
|
||||
{saveError && <p className="text-xs text-destructive">{saveError}</p>}
|
||||
@@ -324,12 +357,12 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
|
||||
{/* ── Live preview ── */}
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="text-sm font-semibold text-text">Preview</h2>
|
||||
<h2 className="text-sm font-semibold text-text">{t("preview")}</h2>
|
||||
<textarea
|
||||
className="border border-border rounded px-3 py-2 text-sm text-text bg-background resize-y min-h-[80px]"
|
||||
value={previewText}
|
||||
onChange={(e) => setPreviewText(e.target.value)}
|
||||
aria-label="Preview input"
|
||||
aria-label={t("previewInput")}
|
||||
/>
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
@@ -337,7 +370,7 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
disabled={previewLoading}
|
||||
className="px-4 py-1.5 rounded bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
{previewLoading ? "Processing..." : "Preview"}
|
||||
{previewLoading ? t("processing") : t("preview")}
|
||||
</button>
|
||||
</div>
|
||||
{previewError && <p className="text-xs text-destructive">{previewError}</p>}
|
||||
@@ -345,19 +378,22 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
<div className="flex flex-col gap-3 pt-1 text-sm">
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<span className="text-text-muted">
|
||||
Original tokens: <strong className="text-text">{preview.originalTokens}</strong>
|
||||
{t("originalTokens")}:{" "}
|
||||
<strong className="text-text">{preview.originalTokens}</strong>
|
||||
</span>
|
||||
<span className="text-text-muted">
|
||||
Compressed tokens: <strong className="text-text">{preview.compressedTokens}</strong>
|
||||
{t("compressedTokens")}:{" "}
|
||||
<strong className="text-text">{preview.compressedTokens}</strong>
|
||||
</span>
|
||||
<span className="text-text-muted">
|
||||
Savings: <strong className="text-primary">{preview.savingsPct.toFixed(1)}%</strong>
|
||||
{t("savings")}:{" "}
|
||||
<strong className="text-primary">{preview.savingsPct.toFixed(1)}%</strong>
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-text-muted">
|
||||
Original
|
||||
{t("original")}
|
||||
</h3>
|
||||
<pre className="max-h-72 overflow-auto rounded border border-border bg-background p-3 whitespace-pre-wrap break-words text-text">
|
||||
{preview.original ?? ""}
|
||||
@@ -365,7 +401,7 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-text-muted">
|
||||
Compressed
|
||||
{t("compressed")}
|
||||
</h3>
|
||||
<pre className="max-h-72 overflow-auto rounded border border-border bg-background p-3 whitespace-pre-wrap break-words text-text">
|
||||
{preview.compressed ?? ""}
|
||||
@@ -375,10 +411,15 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
{preview.diff && preview.diff.length > 0 && (
|
||||
<div className="flex flex-col gap-2" data-testid="compression-preview-diff">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wide text-text-muted">
|
||||
Diff
|
||||
{t("diff")}
|
||||
</h3>
|
||||
<div className="flex max-h-72 flex-col gap-2 overflow-auto rounded border border-border p-2">
|
||||
{preview.diff.map(renderDiffSegment)}
|
||||
{preview.diff.map((segment, index) =>
|
||||
renderDiffSegment(segment, index, (label) => {
|
||||
const key = `diffLabels.${label}`;
|
||||
return t.has(key) ? t(key) : label;
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -388,20 +429,23 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
|
||||
{/* ── Analytics strip ── */}
|
||||
<div className="flex flex-col gap-3 rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="text-sm font-semibold text-text">Last 7 days</h2>
|
||||
<h2 className="text-sm font-semibold text-text">{t("last7Days")}</h2>
|
||||
{analytics && analytics.runs === 0 ? (
|
||||
<p className="text-sm text-text-muted">No data yet</p>
|
||||
<p className="text-sm text-text-muted">{t("noDataYet")}</p>
|
||||
) : analytics ? (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<StatCard label="Runs" value={analytics.runs.toLocaleString()} />
|
||||
<StatCard label="Tokens saved" value={analytics.tokensSaved.toLocaleString()} />
|
||||
<StatCard label={t("runs")} value={analytics.runs.toLocaleString(locale)} />
|
||||
<StatCard
|
||||
label="Average savings"
|
||||
label={t("tokensSaved")}
|
||||
value={analytics.tokensSaved.toLocaleString(locale)}
|
||||
/>
|
||||
<StatCard
|
||||
label={t("averageSavings")}
|
||||
value={`${analytics.avgSavingsPercent.toFixed(1)}%`}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-text-muted">No data yet</p>
|
||||
<p className="text-sm text-text-muted">{t("noDataYet")}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user