Files
OmniRoute/src/shared/components/CloudSyncStatus.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

111 lines
3.8 KiB
TypeScript

"use client";
/**
* CloudSyncStatus — Compact sync status indicator for the sidebar
*
* Shows cloud sync connection state with a small icon + label.
* Fetches status from /api/sync/cloud periodically.
* Listens for 'cloud-status-changed' events to re-poll immediately.
*
* @module shared/components/CloudSyncStatus
*/
import { useState, useEffect, useRef, useCallback } from "react";
import { useRouter } from "next/navigation";
import { useTranslations } from "next-intl";
// #6147 — user-facing labels renamed from "Cloud …" to "Remote Settings Sync"
// wording (this feature syncs the operator's own settings to their own remote
// store — it is not a cloud/telemetry service). Internal state keys, the
// `cloud_*` material icons and the cloudSync.* wiring are intentionally kept.
const STATUS_CONFIG = {
connected: { icon: "cloud_done", color: "text-green-500", labelKey: "synced" },
syncing: { icon: "cloud_sync", color: "text-blue-400 animate-pulse", labelKey: "syncing" },
disconnected: { icon: "cloud_off", color: "text-amber-500", labelKey: "off" },
error: { icon: "cloud_off", color: "text-red-400", labelKey: "error" },
disabled: { icon: "cloud_off", color: "text-text-muted/50", labelKey: "disabled" },
};
export default function CloudSyncStatus({ collapsed = false }) {
const t = useTranslations("cloudSyncStatus");
const [status, setStatus] = useState("disabled");
const [lastSync, setLastSync] = useState(null);
const mountedRef = useRef(true);
const router = useRouter();
const poll = useCallback(async () => {
try {
const res = await fetch("/api/sync/cloud");
if (!mountedRef.current) return;
if (!res.ok) {
setStatus("disconnected");
return;
}
const data = await res.json();
if (!mountedRef.current) return;
if (!data.enabled) setStatus("disabled");
else if (data.syncing) setStatus("syncing");
else if (data.connected) {
setStatus("connected");
if (data.lastSync) setLastSync(new Date(data.lastSync));
} else setStatus("disconnected");
} catch {
if (mountedRef.current) setStatus("disconnected");
}
}, []);
useEffect(() => {
mountedRef.current = true;
// Schedule initial poll outside of effect body to avoid setState-in-effect lint
queueMicrotask(poll);
const interval = setInterval(poll, 30000);
// Listen for immediate re-poll events from EndpointPageClient
const handleCloudChange = () => {
setTimeout(poll, 500); // Small delay to let backend settle
};
globalThis.addEventListener("cloud-status-changed", handleCloudChange);
return () => {
mountedRef.current = false;
clearInterval(interval);
globalThis.removeEventListener("cloud-status-changed", handleCloudChange);
};
}, [poll]);
// Don't render if cloud sync is disabled
if (status === "disabled") return null;
const config = STATUS_CONFIG[status];
const label = t(config.labelKey);
return (
<button
onClick={() => router.push("/dashboard/endpoint")}
className="flex items-center gap-2 px-3 py-1.5 text-xs rounded-lg hover:bg-white/5 transition-colors cursor-pointer w-full"
title={
lastSync
? t("lastSync", {
status: status === "connected" ? t("connected") : t("disconnected"),
time: lastSync.toLocaleTimeString(),
})
: label
}
aria-label={t("statusLabel", { status: label })}
>
<span className={`material-symbols-outlined text-[16px] ${config.color}`} aria-hidden="true">
{config.icon}
</span>
{!collapsed && (
<span
className={`truncate ${status === "connected" ? "text-green-500" : "text-text-muted"}`}
>
{label}
</span>
)}
</button>
);
}