mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-13 18:52:18 +03:00
* 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>
175 lines
5.5 KiB
TypeScript
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>
|
|
);
|
|
}
|