Files
fusion/packages/dashboard/app/components/NativeStructurePreview.tsx
Fusion Agent d295202768 FN-013: establish i18n lint baseline and localize dashboard copy
Add a production i18n lint baseline while replacing dashboard copy with translation keys.

- Localize task, board, mailbox, report, graph, settings, and shared dashboard surfaces.
- Regenerate locale catalogs and resource types for the migration keys.
- Add the i18n lint-baseline regression test and contributor guidance.

Files changed:
 .changeset/fn-013-i18n-lint-baseline.md            |   7 +
 docs/i18n-contributing.md                          |  11 +
 .../dashboard/app/components/ActiveAgentsPanel.tsx |   2 +-
 .../dashboard/app/components/AgentDetailView.tsx   |   2 +-
 packages/dashboard/app/components/Board.tsx        |  19 +-
 packages/dashboard/app/components/ChatView.tsx     |   2 +-
 .../dashboard/app/components/ComposeChatPanel.tsx  |  14 +-
 packages/dashboard/app/components/DockTaskList.tsx |  16 +-
 .../dashboard/app/components/FloatingWindow.tsx    |   6 +-
 .../dashboard/app/components/GitHubImportModal.tsx |   4 +-
 .../dashboard/app/components/KnowledgeGraphPanel.tsx |  21 +-
 packages/dashboard/app/components/ListView.tsx     |   2 +-
 .../app/components/MailboxArtifactAttachment.tsx   |  16 +-
 packages/dashboard/app/components/MailboxModal.tsx |  12 +-
 .../app/components/MailboxStructuralItem.tsx       |  13 +-
 .../app/components/MailboxTaskProposal.tsx         |  10 +-
 .../app/components/MailboxTaskRecommendations.tsx  |  12 +-
 packages/dashboard/app/components/MailboxView.tsx  |  14 +-
 packages/dashboard/app/components/MermaidDiagram.tsx |   4 +-
 packages/dashboard/app/components/MeshTopology.tsx |   6 +-
 .../dashboard/app/components/MessageComposer.tsx   |  24 +-
 .../app/components/NativeStructurePreview.tsx      |  14 +-
 packages/dashboard/app/components/ProviderLoginDialog.tsx |  24 +-
 packages/dashboard/app/components/ReportActionMenu.tsx |   6 +-
 packages/dashboard/app/components/ReportModal.tsx  |  26 +-
 packages/dashboard/app/components/TaskChatTab.tsx  |  10 +-
 packages/dashboard/app/components/TaskDetailModal.tsx |  28 +-
 .../app/components/TaskVerificationStatus.tsx      |  10 +-
 .../dashboard/app/components/WhatsAppChatPairingPanel.tsx |  38 +-
 .../components/command-center/CommandCenter.tsx    |   4 +-
 .../components/command-center/IdeationPanel.tsx    |  12 +-
 .../components/settings/sections/McpServersCard.tsx |   2 +-
 .../components/settings/sections/MemorySection.tsx |   4 +-
 .../app/task-modal-touch-resize-e2e-fixture.tsx    |  28 +-
 packages/i18n/locales/en/app.json                  | 247 +++++++-
 packages/i18n/locales/es/app.json                  | 256 +++++++-
 packages/i18n/locales/fr/app.json                  | 256 +++++++-
 packages/i18n/locales/ko/app.json                  | 256 +++++++-
 packages/i18n/locales/pt-BR/app.json               | 705 ++++++++++++++++++++
 packages/i18n/locales/zh-CN/app.json               | 256 +++++++-
 packages/i18n/locales/zh-TW/app.json               | 256 +++++++-
 .../i18n/src/__tests__/i18n-lint-baseline.test.ts  |  31 +
 packages/i18n/src/resources.d.ts                   | 229 ++++++-
 43 files changed, 2627 insertions(+), 288 deletions(-)

Fusion-Task-Id: FN-013
Fusion-Task-Lineage: 846ac221-c7c3-4ff0-a30d-69479b614765
Co-authored-by: Fusion <noreply@runfusion.ai>
2026-08-20 03:55:38 +00:00

124 lines
6.1 KiB
TypeScript

import { memo, useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { BarChart3, CircleAlert, Flag, Lightbulb, Map, Target } from "lucide-react";
import type { NativeStructurePreviewResult, NativeStructureRef } from "@fusion/core";
import { fetchNativeStructurePreview } from "../api";
import "./NativeStructurePreview.css";
export interface NativeStructurePreviewProps {
ref: NativeStructureRef;
payload?: NativeStructurePreviewResult;
/** Attach-time label used only when a persisted target is no longer available. */
capturedLabel?: string;
onOpen: (ref: NativeStructureRef, payload: NativeStructurePreviewResult) => void;
}
const icons = {
mission: Map,
milestone: Flag,
"research-finding": Lightbulb,
"eval-result": BarChart3,
goal: Target,
"roadmap-item": Map,
} satisfies Record<NativeStructureRef["kind"], typeof Map>;
function isSupportedKind(kind: string): kind is NativeStructureRef["kind"] {
return Object.prototype.hasOwnProperty.call(icons, kind);
}
function unavailableLabel(kind: string): string {
return kind.replace(/-/g, " ");
}
/**
* FNXC:NativeStructureEmbed 2026-07-16-12:00:
* Chat and mail use this one memoized renderer for compact structure cards. Navigation remains
* owned by each consumer through `onOpen` because dashboard views use callback/view state rather
* than URL routes; rendering an anchor here would create dead destinations.
*
* FNXC:NativeStructureEmbed 2026-07-19-12:45:
* Roadmap items join this shared renderer with the roadmap icon and callback-only open action.
*/
export const NativeStructurePreview = memo(function NativeStructurePreview({ ref, payload, capturedLabel, onOpen }: NativeStructurePreviewProps) {
const { t } = useTranslation("app");
const supportedKind = isSupportedKind(ref.kind);
const refKey = `${ref.kind}\u0000${ref.id}\u0000${ref.projectId ?? ""}`;
const [fetchedPayload, setFetchedPayload] = useState<{ refKey: string; result: NativeStructurePreviewResult } | undefined>();
const [error, setError] = useState(false);
// FNXC:NativeStructureEmbed 2026-07-16-12:00: A ref update must not briefly render a prior fetch result; consumers can replace cards while messages or drafts rehydrate.
const result = payload ?? (fetchedPayload?.refKey === refKey ? fetchedPayload.result : undefined);
const Icon = supportedKind ? icons[ref.kind] : CircleAlert;
useEffect(() => {
/*
FNXC:NativeStructureEmbed 2026-07-19-18:00:
Refs can arrive from persisted chat/mail content, so reject a malformed kind before fetching.
The six-kind route owns resolution; an invalid icon lookup must not trigger a plugin read or
turn into a render crash.
*/
if (payload || !supportedKind) return;
let active = true;
setFetchedPayload(undefined);
setError(false);
void fetchNativeStructurePreview(ref)
.then((nextPayload) => {
if (active) setFetchedPayload({ refKey, result: nextPayload });
})
.catch(() => {
if (active) setError(true);
});
return () => { active = false; };
}, [payload, ref.kind, ref.id, ref.projectId, refKey]);
if (!supportedKind) {
return (
<section className="native-structure-preview native-structure-preview--unavailable" data-testid="native-structure-preview-unavailable" data-reason="missing">
<Icon aria-hidden="true" />
<div className="native-structure-preview__content"><span className="native-structure-preview__label">{t("nativeStructure.previewUnavailable", "Preview unavailable")}</span><p>{t("nativeStructure.unavailable", "This structure is unavailable.")}</p></div>
</section>
);
}
// FNXC:NativeStructureEmbed 2026-07-16-14:05: A caller-supplied projection is authoritative after a transient fetch failure, so it must replace the error placeholder for the same ref.
if (error && !payload) {
return (
<section className="native-structure-preview native-structure-preview--unavailable" data-testid="native-structure-preview-error">
<Icon aria-hidden="true" />
<div className="native-structure-preview__content"><span className="native-structure-preview__label">{t("nativeStructure.previewUnavailable", "Preview unavailable")}</span><p>{t("nativeStructure.loadFailed", "Could not load this {{kind}}.", { kind: unavailableLabel(ref.kind) })}</p></div>
</section>
);
}
if (!result) {
return (
<section className="native-structure-preview" data-testid="native-structure-preview-loading" aria-busy="true">
<Icon aria-hidden="true" />
<div className="native-structure-preview__content"><span className="native-structure-preview__label">{t("nativeStructure.loading", "Loading {{kind}}", { kind: unavailableLabel(ref.kind) })}</span></div>
</section>
);
}
if (!result.available) {
return (
<section className="native-structure-preview native-structure-preview--unavailable" data-testid="native-structure-preview-unavailable" data-reason={result.reason}>
<Icon aria-hidden="true" />
<div className="native-structure-preview__content"><span className="native-structure-preview__label">{unavailableLabel(result.kind)}</span><strong className="native-structure-preview__title">{capturedLabel?.trim() || t("nativeStructure.previewUnavailable", "Preview unavailable")}</strong><p>{t("nativeStructure.unavailable", "This structure is unavailable.")}</p></div>
</section>
);
}
return (
<section className="native-structure-preview" data-testid="native-structure-preview" data-kind={result.kind}>
<Icon aria-hidden="true" />
<div className="native-structure-preview__content">
<span className="native-structure-preview__label">{result.kindLabel}</span>
<strong className="native-structure-preview__title">{result.title}</strong>
<p className="native-structure-preview__excerpt">{result.excerpt}</p>
</div>
<button className="btn native-structure-preview__open" type="button" onClick={() => onOpen(ref, result)} aria-label={t("nativeStructure.openAria", "Open {{kind}}: {{title}}", { kind: result.kindLabel, title: result.title })}>
{t("actions.open", "Open")}
</button>
</section>
);
});