Files
OmniRoute/src/shared/components/compression/CompressionPipelineEditor.tsx
nguyenha935 4012bac41d 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>
2026-07-21 13:41:02 -03:00

175 lines
5.5 KiB
TypeScript

"use client";
// T06 — drag-to-reorder editor for the stacked-compression pipeline (gaps v3.8.42).
//
// A controlled component: it renders `steps` and reports every edit back through `onChange`
// (parent owns the state + persistence). All mutations go through the pure
// `compressionPipelineModel` so invariants (valid intensity, non-empty pipeline) hold.
//
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
sortableKeyboardCoordinates,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { useTranslations } from "next-intl";
import {
allowedIntensities,
addLayer,
moveLayer,
removeLayer,
updateLayer,
type EngineIntensities,
type PipelineStep,
} from "./compressionPipelineModel";
export type { PipelineStep } from "./compressionPipelineModel";
type Props = {
steps: PipelineStep[];
onChange: (steps: PipelineStep[]) => void;
engineIntensities: EngineIntensities;
};
const SELECT_CLASS =
"rounded-lg border border-border bg-bg px-3 py-2 text-sm text-text-main w-full";
function SortableRow(props: {
id: string;
index: number;
step: PipelineStep;
engines: string[];
engineIntensities: EngineIntensities;
canRemove: boolean;
onPatch: (patch: Partial<PipelineStep>) => void;
onRemove: () => void;
}) {
const t = useTranslations("contextCombos");
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: props.id,
});
const style: React.CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.6 : 1,
};
return (
<div
ref={setNodeRef}
style={style}
data-testid={`pipeline-row-${props.index}`}
className="grid grid-cols-[auto_1fr_1fr_auto] items-center gap-2"
>
<button
type="button"
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}
{...listeners}
>
</button>
<select
aria-label={t("engine")}
value={props.step.engine}
onChange={(event) => props.onPatch({ engine: event.target.value })}
className={SELECT_CLASS}
>
{props.engines.map((engine) => (
<option key={engine} value={engine}>
{engine}
</option>
))}
</select>
<select
aria-label={t("intensity")}
value={props.step.intensity ?? ""}
onChange={(event) => props.onPatch({ intensity: event.target.value })}
className={SELECT_CLASS}
>
{allowedIntensities(props.step.engine, props.engineIntensities).map((intensity) => (
<option key={intensity} value={intensity}>
{intensity}
</option>
))}
</select>
<button
type="button"
onClick={props.onRemove}
disabled={!props.canRemove}
data-testid={`pipeline-remove-${props.index}`}
className="rounded-lg border border-border px-3 py-2 text-sm text-text-main disabled:opacity-50"
>
{t("removeStep")}
</button>
</div>
);
}
export function CompressionPipelineEditor({ steps, onChange, engineIntensities }: Props) {
const t = useTranslations("contextCombos");
const sensors = useSensors(
useSensor(PointerSensor),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates })
);
// Index-based stable ids: a controlled list with no per-row id. Ids are stable within a
// render (0..n-1); reorder maps id→index before delegating to the pure model.
const ids = steps.map((_, index) => String(index));
const engines = Object.keys(engineIntensities);
const firstEngine = engines[0] ?? "rtk";
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const from = ids.indexOf(String(active.id));
const to = ids.indexOf(String(over.id));
onChange(moveLayer(steps, from, to));
};
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">{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"
>
{t("addStep")}
</button>
</div>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={ids} strategy={verticalListSortingStrategy}>
<div className="space-y-2">
{steps.map((step, index) => (
<SortableRow
key={ids[index]}
id={ids[index]}
index={index}
step={step}
engines={engines}
engineIntensities={engineIntensities}
canRemove={steps.length > 1}
onPatch={(patch) => onChange(updateLayer(steps, index, patch, engineIntensities))}
onRemove={() => onChange(removeLayer(steps, index))}
/>
))}
</div>
</SortableContext>
</DndContext>
</div>
);
}