diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 399894584..1f1c68f02 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -25,6 +25,13 @@ recovery/equivalence checks; affected product paths fail closed during each cutover rather than substituting a local estimate. See `docs/doctoring/python-mathematical-compute-boundary-audit.md`. +ADR 0237 also keeps accelerator deployment outside this repository. MLX runs +as a native Apple-silicon inference service behind contextual-orchestrator; +scientific CPU/CUDA/OpenCL profiles belong to TEPP or fast-mlsirm. RankWeave +remains the dependency-free Python retrieval-fusion/evaluation owner behind its +published contract. LineageWeave Compose therefore does not reserve devices or +mount host drivers; its provider-neutral connectors consume versioned results +and fail closed when an owning service is unavailable. ## Data flow ```mermaid diff --git a/docs/adr/0237-accelerator-runtime-service-boundary.md b/docs/adr/0237-accelerator-runtime-service-boundary.md new file mode 100644 index 000000000..621a61382 --- /dev/null +++ b/docs/adr/0237-accelerator-runtime-service-boundary.md @@ -0,0 +1,100 @@ +# ADR 0237 — Accelerator runtimes stay behind owning service contracts + +**Decision status:** Accepted +**Date:** 2026-08-26 +**Related:** ADR 0076, ADR 0083, ADR 0208 + +## Context + +LineageWeave runs with Docker Compose on Linux, macOS, and Windows hosts, but +does not own model inference or scientific computation. Adding MLX, CUDA, or +OpenCL devices to its backend would duplicate upstream capability selection, +couple the evidence API to host drivers, and make CPU-only installations less +portable. + +The accelerator mechanisms are platform-specific. MLX targets Apple silicon's +unified CPU/GPU memory. Docker Compose can reserve an NVIDIA GPU only when the +host and daemon expose it, while the NVIDIA Container Toolkit injects host +devices and driver mounts into a Linux container. OpenCL discovers vendor +implementations through an installable-client-driver loader, so an image alone +cannot prove that a compatible device and vendor driver are present. + +## Decision + +1. LineageWeave owns no MLX, CUDA, OpenCL, GPU, or scientific CPU runtime. + Its backend and frontend remain portable consumers of authenticated, + versioned service contracts. +2. LLM, VISION, and embedding acceleration belongs to + contextual-orchestrator or a provider-neutral inference service registered + behind it. On Apple silicon, an MLX process runs natively as such a service; + LineageWeave does not pass Metal devices into its Linux VM or encode an MLX + URL, model, port, or chat template. +3. TEPP and fast-mlsirm own their construct-specific scientific and + psychometric Rust cores. Their compute services may publish separate CPU and + accelerator deployment profiles: deterministic multithreaded CPU is the + portable required path; CUDA uses an explicit Compose GPU reservation plus + a compatible host driver/toolkit; OpenCL uses an explicitly mounted device + and matching vendor ICD. RankWeave remains the dependency-free Python owner + of retrieval fusion and evaluation behind its published contract; this ADR + neither changes its implementation language nor transfers psychometric + ownership to it. None of these profiles are added to LineageWeave Compose. +4. LineageWeave connectors accept only the owner's provider-neutral envelope. + Persisted evidence records the owner, contract/model version, input/output + digest, execution-device class reported by the owner, convergence or + completion state, and uncertainty where the construct requires it. A device + label is provenance, not a quality score. +5. Missing devices, drivers, ICDs, or owner services fail at the owning service + boundary. LineageWeave shows unavailable/failed status and the next valid + action; it never retries on a guessed backend, computes a Python substitute, + or claims GPU execution from configuration alone. + +```mermaid +flowchart LR + LW[LineageWeave API and UI] -->|provider-neutral contract| CO[contextual-orchestrator] + LW -->|measurement contract| M[TEPP / fast-mlsirm service] + LW -->|retrieval-fusion contract| R[RankWeave] + CO --> N[Native MLX service on Apple silicon] + CO --> P[Remote or container inference provider] + M --> C[Deterministic multithreaded CPU] + M --> G[Owner CUDA or OpenCL profile] +``` + +## Considered alternatives + +- **Add accelerator profiles to LineageWeave Compose.** Rejected because this + repository does not own the computation and cannot validate host drivers for + another service's algorithm. +- **Run every accelerator natively.** Rejected because CUDA containers are a + supported owner deployment when their host prerequisites are explicit. +- **Use CPU fallback inside LineageWeave.** Rejected because it would reproduce + the formula on the wrong side of the contract. + +## Consequences and acceptance + +- LineageWeave Compose remains CPU-portable and contains no device reservation. +- TEPP and fast-mlsirm bear deployment and recovery-test work for every + advertised scientific-compute profile. RankWeave retains its own retrieval + fusion/evaluation conformance contract. +- A scientific-compute integration is accepted only when its owning repository + proves the same versioned synthetic input on deterministic CPU and each + advertised accelerator, reports bounded numerical tolerance and device + provenance, and LineageWeave proves malformed, mismatched, and unavailable + envelopes fail closed without exposing implementation details in customer + copy. +- Native MLX availability is verified at contextual-orchestrator's provider + boundary; CUDA/OpenCL availability is verified in the compute owner's health + and conformance evidence. A Compose declaration by itself is insufficient. + +## References (APA 7th) + +Docker, Inc. (2026). *Run Docker Compose services with GPU access*. +https://docs.docker.com/compose/how-tos/gpu-support/ + +Khronos Group. (2026). *OpenCL registry*. +https://registry.khronos.org/OpenCL/ + +MLX Contributors. (2026). *Unified memory*. MLX documentation. +https://ml-explore.github.io/mlx/build/html/usage/unified_memory.html + +NVIDIA Corporation. (2026). *NVIDIA Container Toolkit architecture overview*. +https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/arch-overview.html diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 7704fa748..9b9c5e3a8 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -384,6 +384,7 @@ this file per §3.5 of the prior snapshot). | Frontend delivery performance | #644 implements a native dynamic-import boundary for conditional workspace surfaces and retains accessible loading/error states; exact-head checks passed but the PR is not protected-main evidence | Merge #644 normally, rebuild the protected-main production bundle, and retain the measured chunk inventory rather than raising the warning limit | | External integrations | Search, Zotero, calendar, Keyverse, orchestrator, RankWeave, ThreadWeave, TEPP, DiskSage, wardnet | Provider conformance, failure/reconciliation behavior, and provenance-bearing integration evidence | | MSA / modular reuse | LineageWeave must run standalone and as a consumer of org packages | Do not reimplement RankWeave/TEPP/orchestrator/ThreadWeave/Keyverse; fix upstream and PR there | +| Accelerator runtime ownership | ADR 0076/0208 already prohibit local model and mathematical ownership; ADR 0237 now defines MLX as a native orchestrator-side service and TEPP/fast-mlsirm CUDA/OpenCL/CPU profiles as scientific-compute-owner deployments, so LineageWeave Compose remains device-neutral. RankWeave remains the dependency-free Python retrieval-fusion/evaluation owner behind its published contract | TEPP and fast-mlsirm must publish deterministic CPU recovery plus conformance evidence for every advertised CUDA/OpenCL profile; contextual-orchestrator must prove native MLX availability through its provider-neutral health/contract boundary. LineageWeave accepts only versioned, provenance-bearing envelopes and fails closed when the owner is unavailable | | Product contract authority | The current LineageWeave PRD records exact-case ecosystem authorities. TEPP, fast-mlsirm, keyverse, ThreadWeave, and RankWeave PR #41 have standalone PRDs; RankWeave's remains unmerged. contextual-orchestrator, disksage, and wardnet still rely on product/architecture documents, and naruon has only a scoped Topic Intelligence PRD | Keep ADRs normative, preserve canonical repository case in machine references, land the pending PRDs, and add standalone PRDs in each remaining owning repository before cross-product release claims exceed its documented boundary | | Release quality | PR #660 is now on protected `main`; its pre-merge full Python suite passed 1,352 tests with 17 skips, but release-wide frontend, Storybook, security, browser, and runtime acceptance remain unproven on one exact protected head | Repository-wide coverage, docstrings, Storybook, security, browser, and release evidence on one exact head | | PII | Masking would paralyze the product; ADR 0001 forbids identifying artifacts in git | ABAC + authorized runtime; synthetic fixtures in git; no mask-in-place that drops names the operator must read | diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx index 2dee4513d..bf36b3510 100644 --- a/frontend/src/App.test.tsx +++ b/frontend/src/App.test.tsx @@ -2183,7 +2183,7 @@ describe("App, authenticated", () => { "hint-disclosure", ); - const authorSection = screen.getByRole("region", { name: "Source author evidence" }); + const authorSection = screen.getByRole("region", { name: "Author context" }); expect(within(authorSection).getByText("AUTH-HINT · Hint only").closest("details")).toHaveClass( "hint-disclosure", ); @@ -2456,13 +2456,13 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: "View post: Public post" })); await waitFor(() => expect(screen.getByText("이것은 요약입니다.")).toBeInTheDocument()); - const provenance = screen.getByText("Evidence provenance").closest("details"); + const provenance = screen.getByText("Why this item is listed").closest("details"); expect(provenance).not.toBeNull(); expect(provenance).not.toHaveAttribute("open"); - await userEvent.click(screen.getByText("Evidence provenance")); - expect(screen.getByText(/Ontology class:/)).toBeInTheDocument(); - expect(screen.getByText(/Extraction source: Semantic extraction/)).toBeInTheDocument(); - expect(screen.getByText(/Evidence field: Stored semantic evidence/)).toBeInTheDocument(); + await userEvent.click(screen.getByText("Why this item is listed")); + expect(screen.getByText(/Category:/)).toBeInTheDocument(); + expect(screen.getByText(/How this item was found: Semantic extraction/)).toBeInTheDocument(); + expect(screen.getByText(/Recorded evidence: Stored semantic evidence/)).toBeInTheDocument(); expect(screen.queryByText("contextual_orchestrator_semantic")).not.toBeInTheDocument(); expect(screen.queryByText("https://contextualwisdomlab.github.io/LineageWeave/ontology#Project")).not.toBeInTheDocument(); expect(screen.getByText("첫 번째 이벤트")).toBeInTheDocument(); @@ -2696,7 +2696,11 @@ describe("App, authenticated", () => { await userEvent.click(await screen.findByRole("button", { name: /verify against web search/i })); await waitFor(() => - expect(screen.getByText("Verification unavailable (search is not configured).")).toBeInTheDocument(), + expect( + screen.getByText( + "Verification is unavailable because public search is not configured yet. Ask an administrator to enable it, then retry.", + ), + ).toBeInTheDocument(), ); expect(screen.queryByText(/HTTP 503/)).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /verify against web search/i })).not.toBeInTheDocument(); @@ -3088,11 +3092,15 @@ describe("App, authenticated", () => { ); }); - it("names RankWeave unavailability on home rankings instead of inventing a fused score", async () => { + it("names rankings unavailability on home rankings instead of inventing a score", async () => { stubBackend(); render(); - expect(await screen.findByText("Rankings · RankWeave not available")).toBeInTheDocument(); + expect( + await screen.findByText( + "Rankings are not available right now. Reopen this post later to load them.", + ), + ).toBeInTheDocument(); expect(screen.queryByText("Pricing renegotiation: revised quote sent")).not.toBeInTheDocument(); }); @@ -3196,11 +3204,11 @@ describe("App, authenticated", () => { name: /open ranking: public post/i, }); expect(rankingButton).toHaveTextContent("Public post"); - expect(rankingButton).toHaveTextContent("Rankings · rankweave"); + expect(rankingButton).toHaveTextContent("Rankings"); expect(rankingButton).toHaveTextContent("rank 1"); expect( screen.getByText( - "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.", + "Rankings combine newest-first and title-overlap evidence and are not calibrated scores. Open a ranked post to see its evidence.", ), ).toBeInTheDocument(); expect( @@ -3249,7 +3257,7 @@ describe("App, authenticated", () => { expect(list).toHaveTextContent("TEPP measurement · Failed · Demo Corp"); expect(list).toHaveTextContent("Period report · Succeeded · Demo Corp"); expect(list).toHaveTextContent( - "Open this run to see why it failed, then connect the measurement service and re-run.", + "Open this run to see why it failed, then retry with the latest available records.", ); expect(list).toHaveTextContent("3 documents"); expect(list).not.toHaveTextContent("postgresql://"); @@ -3432,8 +3440,9 @@ describe("App, authenticated", () => { ); expect(lineageButton).not.toHaveTextContent("measurement service"); expect(teppButton).toHaveTextContent( - "Open this run to see why it failed, then connect the measurement service and re-run.", + "Open this run to see why it failed, then retry with the latest available records.", ); + expect(teppButton).not.toHaveTextContent("measurement service"); expect(teppButton).not.toHaveTextContent("reconstruction"); }); @@ -3721,7 +3730,7 @@ describe("App, authenticated", () => { ).not.toBeInTheDocument(); expect( await screen.findByText( - "No posts were available at this cutoff for the period report. Open a later run, or ask an administrator to capture a newer snapshot.", + "No posts were available at this cutoff for the period report. Open a later run or retry after a newer snapshot is available.", ), ).toBeInTheDocument(); }); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index fbba1d9f2..7c7fd8c86 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -157,7 +157,7 @@ function LanguageSwitcher({ accessToken }: { accessToken?: string }) { function searchUnavailableMessage(err: unknown): string { if (err instanceof BackendError && err.status === 503) { - return t("Verification unavailable (search is not configured)."); + return t("Verification is unavailable because public search is not configured yet. Ask an administrator to enable it, then retry."); } return String(err); } @@ -1218,7 +1218,7 @@ function KeymanPanel({ onClick={() => setOntologyOpen((open) => !open)} aria-expanded={ontologyOpen} > - {t("Inspect ontology neighborhood")} + {t("View related information")} {canExtract && !orchestratorOff && (
@@ -1232,7 +1232,7 @@ function KeymanPanel({ {error &&

{error}

} {sourceAuthorContext ? (
- {t("Source author evidence")} · {t("Hint only")} + {t("Author context")} · {t("Hint only")}

{sourceAuthorContext.source_author_name || sourceAuthorContext.source_author_code || t("Unknown")} @@ -2044,10 +2044,10 @@ function PostDetailPopup({ setPostActionStatus(t("Permanent link copied.")); return; } - setPostActionStatus(t("Share unavailable.")); + setPostActionStatus(t("Sharing did not start. Copy the link from the browser address bar to share this post.")); } catch (err) { if (err instanceof DOMException && err.name === "AbortError") return; - setPostActionStatus(t("Share unavailable.")); + setPostActionStatus(t("Sharing did not start. Copy the link from the browser address bar to share this post.")); } } @@ -2058,7 +2058,7 @@ function PostDetailPopup({ const next = await setPostBookmark(accessToken, postId, !bookmarked); setBookmarked(next.bookmarked); } catch { - setPostActionStatus(t("Bookmark unavailable.")); + setPostActionStatus(t("Bookmark could not be saved. Try again in a moment; the post itself stays open.")); } finally { setBookmarkSaving(false); } @@ -2145,7 +2145,9 @@ function PostDetailPopup({ ) : (

- {t("Source body was not imported; summary and semantic extraction are unavailable.")} + {t( + "The original text of this post was not imported, so its summary and semantic extraction are unavailable. Open the post directly or ask the source owner to re-import it with its body.", + )}

)} @@ -2167,8 +2169,8 @@ function PostDetailPopup({ post.source_project_name || post.source_system_code || post.source_record_key) && ( -
-

{t("Original source state")}

+
+

{t("Earlier source version")}

{post.source_stage_code ? ( <> @@ -2279,15 +2281,15 @@ function PostDetailPopup({ ) : null}
-

{t("Raw source codes are shown; no state label was inferred.")}

+

{t("Use these recorded details to confirm the record with your source system.")}

)} {post.project_evidence && post.project_evidence.length > 0 ? ( -
-

{t("Projects / semantic evidence")}

+
+

{t("Related projects")}

    {post.project_evidence.map((project) => (
  • @@ -2306,15 +2308,15 @@ function PostDetailPopup({ : `(${Math.round(project.confidence * 100)}%)`} : {project.evidence}
    - {t("Evidence provenance")} + {t("Why this item is listed")} - {t("Ontology class")}: {t(project.ontology_label ?? "Project")} + {t("Category")}: {t(project.ontology_label ?? "Project")} - {t("Extraction source")}: {projectExtractionLabel(project.extraction_method)} + {t("How this item was found")}: {projectExtractionLabel(project.extraction_method)} - {t("Evidence field")}: {projectProvenanceLabel(project.provenance)} + {t("Recorded evidence")}: {projectProvenanceLabel(project.provenance)}
  • @@ -2688,9 +2690,9 @@ function analysisRunNextAction(run: AnalysisRun): string | null { case "analysis_status_failed": switch (run.run_kind_code) { case "analysis_run_tepp": - return "Open this run to see why it failed, then connect the measurement service and re-run."; + return "Open this run to see why it failed, then retry with the latest available records."; case "analysis_run_topic_lineage": - return "Open this run to see why it failed, then connect the TEPP transport and re-run."; + return "Open this run to see why it failed, then retry with the latest available records."; case "analysis_run_lineage": return "Open this run to see why it failed, then retry reconstruction from a current snapshot."; case "analysis_run_report": @@ -2721,22 +2723,22 @@ function analysisRunEmptyPostsHint(run: AnalysisRun): string { case "analysis_run_tepp": return ( "No posts were available at this cutoff for TEPP to measure. " + - "Open a later run, or ask an administrator to capture a newer snapshot." + "Open a later run or retry after a newer snapshot is available." ); case "analysis_run_topic_lineage": return ( "No posts were available at this cutoff for topic-lineage analysis. " + - "Open a later run, or ask an administrator to capture a newer snapshot." + "Open a later run or retry after a newer snapshot is available." ); case "analysis_run_lineage": return ( "No posts were available at this cutoff for reconstruction. " + - "Open a later run, or ask an administrator to capture a newer snapshot." + "Open a later run or retry after a newer snapshot is available." ); case "analysis_run_report": return ( "No posts were available at this cutoff for the period report. " + - "Open a later run, or ask an administrator to capture a newer snapshot." + "Open a later run or retry after a newer snapshot is available." ); default: { const unexpected: never = run.run_kind_code; @@ -3139,8 +3141,8 @@ function AnalysisRunsPanel({ {(error || entitiesLoadError) &&

    {error ?? entitiesLoadError}

    } {runs.length === 0 ? (

    - No analysis runs visible to this account yet. Request a lineage - reconstruction, or ask an administrator to run make seed. + No analysis runs are visible to this account yet. Add source records, + then request a new analysis run.

    ) : (
      @@ -3361,28 +3363,30 @@ function RankingsPanel({

      {t("Rankings")}

      {ranking && ( - {ranking.status === "accepted" - ? "rankweave" - : `rankweave · ${ranking.status_reason ?? "unavailable"}`} + {t("Rankings")} )} {error &&

      {error}

      } {ranking === null && !error &&

      {t("Loading rankings...")}

      } {ranking && ranking.status === "unavailable" && ( -

      {t("Rankings · RankWeave not available")}

      +

      + {t("Rankings are not available right now. Reopen this post later to load them.")} +

      )} {ranking && ranking.status === "accepted" && ranking.rankings.length === 0 && ( -

      {t("No fused rankings from RankWeave.")}

      +

      + {t("No ranked posts yet. Ranked posts appear after the next rankings refresh.")} +

      )} {ranking && ranking.rankings.length > 0 && ( <>

      {t( - "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.", + "Rankings combine newest-first and title-overlap evidence and are not calibrated scores. Open a ranked post to see its evidence.", )}

      -
        +
          {ranking.rankings.map((hit) => (
        • {(hit.channel_evidence ?? []).length > 0 ? ( @@ -4735,7 +4739,7 @@ function CustomerMasterPanel({ ) : null} {master && master.source_author_hints.length > 0 ? (
          -

          {t("Source author evidence")}

          +

          {t("Author context")}

          {master.source_author_hints.length > HINT_RENDER_LIMIT && (

          {tf("Showing the first {shown} of {total} observed source authors, ranked by post count.", { diff --git a/frontend/src/components/FiveW1H.test.tsx b/frontend/src/components/FiveW1H.test.tsx index 278d656c3..7ac38eaaa 100644 --- a/frontend/src/components/FiveW1H.test.tsx +++ b/frontend/src/components/FiveW1H.test.tsx @@ -88,9 +88,9 @@ describe("FiveW1H", () => { />, ); - await userEvent.click(screen.getByText("Evidence provenance")); + await userEvent.click(screen.getByText("Why this item is listed")); expect(screen.getByText("“we renewed the contract”")).toBeInTheDocument(); - expect(screen.getByText("Ontology class: Contract renewal")).toBeInTheDocument(); + expect(screen.getByText("Category: Contract renewal")).toBeInTheDocument(); }); it("falls back to the ontology code when no ontology label is annotated", async () => { @@ -112,8 +112,8 @@ describe("FiveW1H", () => { />, ); - await userEvent.click(screen.getByText("Evidence provenance")); - expect(screen.getByText("Ontology class: evt-42")).toBeInTheDocument(); + await userEvent.click(screen.getByText("Why this item is listed")); + expect(screen.getByText("Category: evt-42")).toBeInTheDocument(); }); it("renders one definition entry per slot with its human label", () => { diff --git a/frontend/src/components/FiveW1H.tsx b/frontend/src/components/FiveW1H.tsx index bb340a2c8..4989478db 100644 --- a/frontend/src/components/FiveW1H.tsx +++ b/frontend/src/components/FiveW1H.tsx @@ -44,12 +44,12 @@ export function FiveW1H({ slots }: { slots: FiveW1HSlot[] | null }) {

        • {value.text}
          - {t("Evidence provenance")} + {t("Why this item is listed")} {evidenceSourceLabel(value.source)} {value.evidence_text ? {value.evidence_text} : null} {value.ontology_codes.map((code) => ( - {t("Ontology class")}: {t(value.ontology_annotations.ontology_label ?? code)} + {t("Category")}: {t(value.ontology_annotations.ontology_label ?? code)} ))}
          diff --git a/frontend/src/components/OntologyExplorer.stabilization.test.tsx b/frontend/src/components/OntologyExplorer.stabilization.test.tsx index e14209344..4c1fba7ab 100644 --- a/frontend/src/components/OntologyExplorer.stabilization.test.tsx +++ b/frontend/src/components/OntologyExplorer.stabilization.test.tsx @@ -99,7 +99,7 @@ describe("OntologyExplorer stabilization contracts", () => { expect( screen.getByText( - "This neighborhood is bound to a knowledge cutoff. Compare with live evidence next.", + "This information reflects an earlier view. Compare it with the current record next.", ), ).toBeInTheDocument(); }); @@ -150,7 +150,7 @@ describe("OntologyExplorer stabilization contracts", () => { expect( await screen.findByText( - "Neighborhood truncated. Load the next relation page or inspect one edge.", + "Some related information is not shown. Open a source post to continue.", ), ).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Load next relation page" })); @@ -251,7 +251,7 @@ describe("OntologyExplorer stabilization contracts", () => { }); expect( screen.getByText( - "No visible ontology relations for this focus. Open a Keyman or affiliated organization next.", + "No related information is available. Open a visible post next.", ), ).toBeInTheDocument(); }); diff --git a/frontend/src/components/OntologyExplorer.test.tsx b/frontend/src/components/OntologyExplorer.test.tsx index 015219da5..c61f02fff 100644 --- a/frontend/src/components/OntologyExplorer.test.tsx +++ b/frontend/src/components/OntologyExplorer.test.tsx @@ -182,10 +182,10 @@ describe("OntologyExplorer", () => { await userEvent.click(await screen.findByRole("button", { name: "Select node: Post Demo public post" })); expect(screen.getByRole("heading", { name: "Demo public post" })).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Load next relation page" })); - expect(await screen.findByText("Loading ontology neighborhood...")).toBeInTheDocument(); + expect(await screen.findByText("Loading related information...")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Select node: Post Demo public post" })).toBeInTheDocument(); rejectContinuation(new BackendError("/api/ontology/neighborhood", 500)); - expect(await screen.findByText("Ontology neighborhood is unavailable. Open a visible post next.")).toBeInTheDocument(); + expect(await screen.findByText("Related information is unavailable. Open a visible post next.")).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Select node: Post Demo public post" })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: "Demo public post" })).toBeInTheDocument(); expect(fetchNeighborhood).toHaveBeenNthCalledWith( @@ -212,10 +212,10 @@ describe("OntologyExplorer", () => { ); await userEvent.click(await screen.findByRole("button", { name: "Load next relation page" })); - expect(await screen.findByText("Ontology neighborhood is unavailable. Open a visible post next.")).toBeInTheDocument(); + expect(await screen.findByText("Related information is unavailable. Open a visible post next.")).toBeInTheDocument(); await userEvent.click(screen.getByRole("button", { name: "Load next relation page" })); await waitFor(() => expect(fetchNeighborhood).toHaveBeenCalledTimes(3)); - expect(screen.queryByText("Ontology neighborhood is unavailable. Open a visible post next.")).not.toBeInTheDocument(); + expect(screen.queryByText("Related information is unavailable. Open a visible post next.")).not.toBeInTheDocument(); expect(fetchNeighborhood).toHaveBeenNthCalledWith( 3, "synthetic-access-token", @@ -240,7 +240,7 @@ describe("OntologyExplorer", () => { expect( await screen.findByText( - "Access denied for this ontology neighborhood. Open a visible post next.", + "Related information is unavailable for this record. Open a visible post next.", ), ).toBeInTheDocument(); }); @@ -258,7 +258,7 @@ describe("OntologyExplorer", () => { />, ); expect( - screen.getByText(/This is an ontology neighborhood, not Event Lineage/), + screen.getByText("Review related records and open a source post for details."), ).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "Valid from" })).toBeInTheDocument(); expect(screen.getByRole("columnheader", { name: "Valid to" })).toBeInTheDocument(); @@ -337,7 +337,7 @@ describe("OntologyExplorer", () => { ); expect( screen.getAllByText( - "No visible ontology relations for this focus. Open a Keyman or affiliated organization next.", + "No related information is available. Open a visible post next.", ).length, ).toBeGreaterThan(0); rerender( @@ -360,7 +360,7 @@ describe("OntologyExplorer", () => { status="denied" />, ); - expect(screen.getByText("Access denied for this ontology neighborhood. Open a visible post next.")).toBeInTheDocument(); + expect(screen.getByText("Related information is unavailable for this record. Open a visible post next.")).toBeInTheDocument(); rerender( { status="rejected" />, ); - expect(screen.getByText("Rejected proposal. Open the evidence and do not treat it as authoritative.")).toBeInTheDocument(); + expect(screen.getByText("This suggestion was not accepted. Open the evidence to review it.")).toBeInTheDocument(); }); it("does not hide rejected or cutoff warnings behind truncation", () => { @@ -388,7 +388,7 @@ describe("OntologyExplorer", () => { neighborhood={rejected} />, ); - expect(screen.getByText("Rejected proposal. Open the evidence and do not treat it as authoritative.")).toBeInTheDocument(); + expect(screen.getByText("This suggestion was not accepted. Open the evidence to review it.")).toBeInTheDocument(); expect(screen.queryByText(/Load the next relation page or inspect one edge/)).not.toBeInTheDocument(); rerender( @@ -400,7 +400,7 @@ describe("OntologyExplorer", () => { />, ); expect( - screen.getByText("This neighborhood is bound to a knowledge cutoff. Compare with live evidence next."), + screen.getByText("This information reflects an earlier view. Compare it with the current record next."), ).toBeInTheDocument(); }); diff --git a/frontend/src/components/OntologyExplorer.tsx b/frontend/src/components/OntologyExplorer.tsx index a70d1d436..e8416d8bf 100644 --- a/frontend/src/components/OntologyExplorer.tsx +++ b/frontend/src/components/OntologyExplorer.tsx @@ -202,14 +202,13 @@ export function OntologyExplorer({ } return ( -
          +
          -

          {t("Ontology neighborhood")}

          -

          {t("Typed relations, not Event Lineage")}

          +

          {t("Related information")}

          +

          {t("View related information")}

          - {t("This is an ontology neighborhood, not Event Lineage.")}{" "} - {t("Event Lineage shows reconstructed post-to-post parents. This graph shows typed people, organizations, teams, projects, and posts.")} + {t("Review related records and open a source post for details.")} {visible?.nodes.some((node) => node.node_type_code === "node_occupational_construct") ? ` ${occupationalConstructText("Select a work-evidence node to review the records that support it.")}` : ""} @@ -339,13 +338,13 @@ function statusMessage( } const messages: Record = { ready: "", - loading: t("Loading ontology neighborhood..."), - empty: t("No visible ontology relations for this focus. Open a Keyman or affiliated organization next."), - truncated: t("Neighborhood truncated. Page visible relations, then inspect one edge."), - denied: t("Access denied for this ontology neighborhood. Open a visible post next."), - stale: t("This neighborhood is bound to a knowledge cutoff. Compare with live evidence next."), - rejected: t("Rejected proposal. Open the evidence and do not treat it as authoritative."), - error: t("Ontology neighborhood is unavailable. Open a visible post next."), + loading: t("Loading related information..."), + empty: t("No related information is available. Open a visible post next."), + truncated: t("Some related information is not shown. Open a source post to continue."), + denied: t("Related information is unavailable for this record. Open a visible post next."), + stale: t("This information reflects an earlier view. Compare it with the current record next."), + rejected: t("This suggestion was not accepted. Open the evidence to review it."), + error: t("Related information is unavailable. Open a visible post next."), }; const text = messages[status]; if (!text) return null; @@ -401,7 +400,7 @@ function OntologyGraph({ width="100%" height={Math.max(180, layout.height)} > - {t("Ontology neighborhood")} + {t("View related information")} {layout.edges.map((edge) => { const midX = (edge.fromX + edge.toX) / 2; const midY = (edge.fromY + edge.toY) / 2; @@ -519,7 +518,7 @@ function OntologyExactValueTable({ >

          {t("Exact values")}

          {payload.exact_value_rows.length === 0 ? ( -

          {t("No visible ontology relations for this focus. Open a Keyman or affiliated organization next.")}

          +

          {t("No related information is available. Open a visible post next.")}

          ) : ( @@ -578,7 +577,7 @@ function OntologyNodeDrawer({

          {nodeTypeLabel(node.node_type_code)} · {t(TRUTH_LABEL[node.truth_status_code ?? ""] ?? node.truth_status_code ?? "Unknown")}

          -

          {t("Ontology class")}: {node.ontology_class_iri}

          +

          {t("Category")}: {node.ontology_class_iri}

          {t("Recorded at")}: {node.recorded_at?.slice(0, 10) ?? t("Unknown")}

          {canRefocus ? ( @@ -592,7 +591,7 @@ function OntologyNodeDrawer({ ) : null}
          @@ -650,7 +649,7 @@ function OntologyEdgeDrawer({

          )} ); diff --git a/frontend/src/i18n.test.ts b/frontend/src/i18n.test.ts index ae085d010..d2d91d745 100644 --- a/frontend/src/i18n.test.ts +++ b/frontend/src/i18n.test.ts @@ -73,7 +73,7 @@ describe("i18n", () => { "This is an ontology neighborhood, not Event Lineage.", "Rankings", "Title overlap", - "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.", + "Rankings combine newest-first and title-overlap evidence and are not calibrated scores. Open a ranked post to see its evidence.", "Workspace navigation", "Observed calendar events", "No observed calendar events are available.", diff --git a/frontend/src/i18n.ts b/frontend/src/i18n.ts index 390be07d8..1e442aa24 100644 --- a/frontend/src/i18n.ts +++ b/frontend/src/i18n.ts @@ -37,16 +37,17 @@ const TRANSLATIONS: Partial>> = { "Log out": "로그아웃", Calendar: "캘린더", Rankings: "순위", - "Rankings · RankWeave not available": "순위 · RankWeave를 사용할 수 없음", - "Rankings · rankweave": "순위 · rankweave", + "Rankings are not available right now. Reopen this post later to load them.": + "순위를 지금 불러올 수 없습니다. 이 글을 나중에 다시 열어 확인하세요.", "Loading rankings...": "순위를 불러오는 중...", - "No fused rankings from RankWeave.": "RankWeave가 융합한 순위가 없습니다.", - "Fused rankings": "융합 순위", + "No ranked posts yet. Ranked posts appear after the next rankings refresh.": + "아직 순위로 선정된 글이 없습니다. 다음 순위 갱신 후 표시됩니다.", + "Ranked posts": "순위 목록", "Open ranking: {title}": "순위 열기: {title}", "rank {rank}": "순위 {rank}", "Title overlap": "제목 겹침", - "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.": - "RankWeave가 최신순과 제목 겹침 순위를 융합했습니다. 보정된 점수가 아닙니다.", + "Rankings combine newest-first and title-overlap evidence and are not calibrated scores. Open a ranked post to see its evidence.": + "순위는 최신순·제목 겹침 근거를 결합한 것이며 보정된 점수가 아닙니다. 순위에 포함된 글을 열어 근거를 확인하세요.", "Ranking evidence for {title}": "{title}의 순위 근거", "{label} rank {rank}, contribution {contribution}": "{label} 순위 {rank}, 기여 {contribution}", @@ -76,7 +77,7 @@ const TRANSLATIONS: Partial>> = { "Stored semantic evidence": "저장된 의미 기반 근거", "Recorded evidence": "기록된 근거", "Lineage maintenance": "계보 관리", - "Verification unavailable (search is not configured).": "검증을 사용할 수 없습니다(검색이 설정되지 않았습니다).", + "Verification is unavailable because public search is not configured yet. Ask an administrator to enable it, then retry.": "검증 기능이 아직 준비되지 않았습니다. 관리자에게 공개 검색 사용 설정을 요청한 뒤 다시 시도하세요.", "No customer commitment found in this post.": "이 글에서 고객 약속을 찾지 못했습니다.", due: "기한", "Ticket created": "티켓 생성", @@ -121,14 +122,13 @@ const TRANSLATIONS: Partial>> = { "Source process unit name": "원천 사업부(PU) 이름", "Source sales pool": "원천 수주풀", "Source sales pool name": "원천 수주풀 이름", - "Source body was not imported; summary and semantic extraction are unavailable.": - "원천 본문이 수집되지 않아 요약과 Semantic 추출을 사용할 수 없습니다.", + "The original text of this post was not imported, so its summary and semantic extraction are unavailable. Open the post directly or ask the source owner to re-import it with its body.": "원천 본문이 수집되지 않아 요약과 의미 기반 추출을 사용할 수 없습니다. 글을 직접 열어 확인하거나 원문 담당자에게 본문 포함 재등록을 요청하세요.", "Source customer code": "원천 고객 코드", "Source customer name": "원천 고객사 이름", "Source project code": "원천 프로젝트 코드", "Source project name": "원천 프로젝트 이름", "Business unit (PU)": "사업부(PU)", - "Raw source codes are shown; no state label was inferred.": "원천 코드를 그대로 표시하며 상태 라벨은 추정하지 않습니다.", + "Use these recorded details to confirm the record with your source system.": "기록된 세부 정보로 원천 시스템의 기록을 확인하세요.", "Search and filter posts": "글 검색 및 필터", "Search semantic evidence": "의미 기반 증거 검색", Search: "검색", @@ -441,8 +441,8 @@ const TRANSLATIONS: Partial>> = { Bookmark: "북마크", Bookmarked: "북마크됨", "Permanent link copied.": "영구 링크를 복사했습니다.", - "Share unavailable.": "공유할 수 없습니다.", - "Bookmark unavailable.": "북마크를 사용할 수 없습니다.", + "Sharing did not start. Copy the link from the browser address bar to share this post.": "공유가 시작되지 않았습니다. 브라우저 주소 표시줄에서 링크를 복사해 이 글을 공유하세요.", + "Bookmark could not be saved. Try again in a moment; the post itself stays open.": "북마크를 저장하지 못했습니다. 잠시 후 다시 시도하세요. 글은 그대로 열려 있습니다.", "No summary is available for this record yet.": "이 기록의 요약이 아직 없습니다.", "Saved evidence is still available.": "저장된 근거는 계속 확인할 수 있습니다.", "Showing the first {shown} of {total} posts known at this cutoff.": @@ -564,16 +564,17 @@ const TRANSLATIONS: Partial>> = { "Log out": "退出登录", Calendar: "日历", Rankings: "排名", - "Rankings · RankWeave not available": "排名 · RankWeave 不可用", - "Rankings · rankweave": "排名 · rankweave", + "Rankings are not available right now. Reopen this post later to load them.": + "暂时无法加载排名。请稍后重新打开这篇文章查看。", "Loading rankings...": "正在加载排名...", - "No fused rankings from RankWeave.": "RankWeave 未返回融合排名。", - "Fused rankings": "融合排名", + "No ranked posts yet. Ranked posts appear after the next rankings refresh.": + "还没有入选排名的文章。排名将在下次刷新后显示。", + "Ranked posts": "排名列表", "Open ranking: {title}": "打开排名:{title}", "rank {rank}": "排名 {rank}", "Title overlap": "标题重叠", - "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.": - "RankWeave 融合了最新优先和标题重叠排名。这不是校准分数。", + "Rankings combine newest-first and title-overlap evidence and are not calibrated scores. Open a ranked post to see its evidence.": + "排名结合了最新优先与标题重叠证据,并非校准分数。打开排名中的文章可查看其证据。", "Ranking evidence for {title}": "{title} 的排名证据", "{label} rank {rank}, contribution {contribution}": "{label} 排名 {rank},贡献 {contribution}", @@ -603,7 +604,7 @@ const TRANSLATIONS: Partial>> = { "Stored semantic evidence": "已存储的语义证据", "Recorded evidence": "已记录的证据", "Lineage maintenance": "谱系维护", - "Verification unavailable (search is not configured).": "无法验证(未配置搜索)。", + "Verification is unavailable because public search is not configured yet. Ask an administrator to enable it, then retry.": "验证功能尚未配置公开搜索。请请求管理员启用后再试。", "No customer commitment found in this post.": "未在此文章中找到客户承诺。", due: "截止日期", "Ticket created": "工单已创建", @@ -648,14 +649,13 @@ const TRANSLATIONS: Partial>> = { "Source process unit name": "来源事业部名称 (PU)", "Source sales pool": "来源销售池", "Source sales pool name": "来源销售池名称", - "Source body was not imported; summary and semantic extraction are unavailable.": - "尚未导入来源正文,因此无法进行摘要和语义提取。", + "The original text of this post was not imported, so its summary and semantic extraction are unavailable. Open the post directly or ask the source owner to re-import it with its body.": "这篇文章的原文尚未导入,因此无法提供摘要和语义提取。请直接打开文章,或请数据负责人连同正文重新导入。", "Source customer code": "来源客户代码", "Source customer name": "来源客户名称", "Source project code": "来源项目代码", "Source project name": "来源项目名称", "Business unit (PU)": "事业部 (PU)", - "Raw source codes are shown; no state label was inferred.": "显示原始来源代码;未推断状态标签。", + "Use these recorded details to confirm the record with your source system.": "请使用这些记录的详细信息在来源系统中确认该记录。", "Search and filter posts": "搜索和筛选文章", "Search semantic evidence": "搜索语义证据", Search: "搜索", @@ -961,8 +961,8 @@ const TRANSLATIONS: Partial>> = { Bookmark: "书签", Bookmarked: "已加书签", "Permanent link copied.": "已复制永久链接。", - "Share unavailable.": "分享不可用。", - "Bookmark unavailable.": "书签不可用。", + "Sharing did not start. Copy the link from the browser address bar to share this post.": "分享未能开始。请从浏览器地址栏复制链接来分享这篇文章。", + "Bookmark could not be saved. Try again in a moment; the post itself stays open.": "书签保存失败。请稍后重试;文章本身仍保持打开。", "No summary is available for this record yet.": "此记录暂时没有摘要。", "Saved evidence is still available.": "仍可查看已保存的证据。", "Showing the first {shown} of {total} posts known at this cutoff.": @@ -1107,16 +1107,17 @@ const TRANSLATIONS: Partial>> = { "Log out": "ログアウト", Calendar: "カレンダー", Rankings: "ランキング", - "Rankings · RankWeave not available": "ランキング · RankWeave を利用できません", - "Rankings · rankweave": "ランキング · rankweave", + "Rankings are not available right now. Reopen this post later to load them.": + "現在ランキングを読み込めません。後でこの投稿を開き直してご確認ください。", "Loading rankings...": "ランキングを読み込み中...", - "No fused rankings from RankWeave.": "RankWeave の融合ランキングはありません。", - "Fused rankings": "融合ランキング", + "No ranked posts yet. Ranked posts appear after the next rankings refresh.": + "ランク入り投稿はまだありません。次のランキング更新後に表示されます。", + "Ranked posts": "ランク一覧", "Open ranking: {title}": "ランキングを開く: {title}", "rank {rank}": "順位 {rank}", "Title overlap": "タイトル一致", - "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.": - "RankWeave が新しい順とタイトル一致の順位を融合しました。校正されたスコアではありません。", + "Rankings combine newest-first and title-overlap evidence and are not calibrated scores. Open a ranked post to see its evidence.": + "ランキングは新しい順とタイトル一致の根拠を組み合わせたもので、校正されたスコアではありません。ランク入り投稿を開いて根拠をご確認ください。", "Ranking evidence for {title}": "{title} の順位根拠", "{label} rank {rank}, contribution {contribution}": "{label} 順位 {rank}、寄与 {contribution}", @@ -1146,7 +1147,7 @@ const TRANSLATIONS: Partial>> = { "Stored semantic evidence": "保存された意味的証拠", "Recorded evidence": "記録された証拠", "Lineage maintenance": "系譜管理", - "Verification unavailable (search is not configured).": "確認できません(検索が設定されていません)。", + "Verification is unavailable because public search is not configured yet. Ask an administrator to enable it, then retry.": "検証機能はまだ公開検索が設定されていません。管理者に有効化を依頼してから再試行してください。", "No customer commitment found in this post.": "この投稿に顧客コミットメントは見つかりませんでした。", due: "期限", "Ticket created": "チケットを作成", @@ -1191,14 +1192,13 @@ const TRANSLATIONS: Partial>> = { "Source process unit name": "原典の事業部名 (PU)", "Source sales pool": "原典の受注プール", "Source sales pool name": "原典の受注プール名", - "Source body was not imported; summary and semantic extraction are unavailable.": - "原典本文が取り込まれていないため、要約とセマンティック抽出は利用できません。", + "The original text of this post was not imported, so its summary and semantic extraction are unavailable. Open the post directly or ask the source owner to re-import it with its body.": "この投稿の本文が取り込まれていないため、要約と意味抽出を利用できません。投稿を直接開くか、本文ごとの再取り込みを担当者に依頼してください。", "Source customer code": "原典の顧客コード", "Source customer name": "原典の顧客名", "Source project code": "原典のプロジェクトコード", "Source project name": "原典のプロジェクト名", "Business unit (PU)": "事業部 (PU)", - "Raw source codes are shown; no state label was inferred.": "原典コードをそのまま表示し、状態ラベルは推定していません。", + "Use these recorded details to confirm the record with your source system.": "記録された詳細を使って、元のシステムでこの記録を確認してください。", "Search and filter posts": "投稿を検索・絞り込み", "Search semantic evidence": "意味に基づく証拠を検索", Search: "検索", @@ -1482,8 +1482,8 @@ const TRANSLATIONS: Partial>> = { Bookmark: "ブックマーク", Bookmarked: "ブックマーク済み", "Permanent link copied.": "固定リンクをコピーしました。", - "Share unavailable.": "共有できません。", - "Bookmark unavailable.": "ブックマークを利用できません。", + "Sharing did not start. Copy the link from the browser address bar to share this post.": "共有を開始できませんでした。ブラウザのアドレスバーからリンクをコピーして共有してください。", + "Bookmark could not be saved. Try again in a moment; the post itself stays open.": "ブックマークを保存できませんでした。少し待ってから再試行してください。投稿自体は開いたままです。", "No summary is available for this record yet.": "この記録の概要はまだありません。", "Saved evidence is still available.": "保存された証拠は引き続き確認できます。", "Showing the first {shown} of {total} posts known at this cutoff.": @@ -1629,16 +1629,17 @@ const TRANSLATIONS: Partial>> = { "Log out": "Đăng xuất", Calendar: "Lịch", Rankings: "Xếp hạng", - "Rankings · RankWeave not available": "Xếp hạng · RankWeave không khả dụng", - "Rankings · rankweave": "Xếp hạng · rankweave", + "Rankings are not available right now. Reopen this post later to load them.": + "Hiện không tải được xếp hạng. Hãy mở lại bài viết này sau để xem.", "Loading rankings...": "Đang tải xếp hạng...", - "No fused rankings from RankWeave.": "Không có xếp hạng hợp nhất từ RankWeave.", - "Fused rankings": "Xếp hạng hợp nhất", + "No ranked posts yet. Ranked posts appear after the next rankings refresh.": + "Chưa có bài viết nào vào xếp hạng. Các bài sẽ hiển thị sau lần làm mới tiếp theo.", + "Ranked posts": "Danh sách xếp hạng", "Open ranking: {title}": "Mở xếp hạng: {title}", "rank {rank}": "hạng {rank}", "Title overlap": "Trùng tiêu đề", - "RankWeave fused newest-first and title-overlap ranks. This is not a calibrated score.": - "RankWeave đã hợp nhất hạng mới nhất trước và trùng tiêu đề. Đây không phải điểm đã hiệu chỉnh.", + "Rankings combine newest-first and title-overlap evidence and are not calibrated scores. Open a ranked post to see its evidence.": + "Xếp hạng kết hợp bằng chứng mới nhất trước và trùng tiêu đề, không phải điểm đã hiệu chỉnh. Mở bài viết trong bảng xếp hạng để xem bằng chứng.", "Ranking evidence for {title}": "Bằng chứng xếp hạng cho {title}", "{label} rank {rank}, contribution {contribution}": "{label} hạng {rank}, đóng góp {contribution}", @@ -1668,7 +1669,7 @@ const TRANSLATIONS: Partial>> = { "Stored semantic evidence": "Bằng chứng ngữ nghĩa đã lưu", "Recorded evidence": "Bằng chứng đã ghi nhận", "Lineage maintenance": "Bảo trì dòng sự kiện", - "Verification unavailable (search is not configured).": "Không thể xác minh (chưa cấu hình tìm kiếm).", + "Verification is unavailable because public search is not configured yet. Ask an administrator to enable it, then retry.": "Tính năng xác minh chưa được cấu hình tìm kiếm công khai. Hãy yêu cầu quản trị viên bật rồi thử lại.", "No customer commitment found in this post.": "Không tìm thấy cam kết của khách hàng trong bài viết này.", due: "Hạn", "Ticket created": "Đã tạo phiếu", @@ -1713,14 +1714,13 @@ const TRANSLATIONS: Partial>> = { "Source process unit name": "Tên đơn vị kinh doanh nguồn (PU)", "Source sales pool": "Nhóm bán hàng nguồn", "Source sales pool name": "Tên nhóm bán hàng nguồn", - "Source body was not imported; summary and semantic extraction are unavailable.": - "Nội dung nguồn chưa được nhập nên không thể tóm tắt hoặc trích xuất ngữ nghĩa.", + "The original text of this post was not imported, so its summary and semantic extraction are unavailable. Open the post directly or ask the source owner to re-import it with its body.": "Bản gốc của bài viết chưa được nhập nên không thể tóm tắt và trích xuất ngữ nghĩa. Hãy mở trực tiếp bài viết hoặc yêu cầu người phụ trách nhập lại kèm bản gốc.", "Source customer code": "Mã khách hàng nguồn", "Source customer name": "Tên khách hàng nguồn", "Source project code": "Mã dự án nguồn", "Source project name": "Tên dự án nguồn", "Business unit (PU)": "Đơn vị kinh doanh (PU)", - "Raw source codes are shown; no state label was inferred.": "Hiển thị mã nguồn gốc; không suy đoán nhãn trạng thái.", + "Use these recorded details to confirm the record with your source system.": "Hãy dùng các chi tiết đã ghi để xác nhận bản ghi trong hệ thống nguồn.", "Search and filter posts": "Tìm kiếm và lọc bài viết", "Search semantic evidence": "Tìm kiếm bằng chứng ngữ nghĩa", Search: "Tìm", @@ -2004,8 +2004,8 @@ const TRANSLATIONS: Partial>> = { Bookmark: "Dấu trang", Bookmarked: "Đã đánh dấu", "Permanent link copied.": "Đã sao chép liên kết cố định.", - "Share unavailable.": "Không thể chia sẻ.", - "Bookmark unavailable.": "Không thể dùng dấu trang.", + "Sharing did not start. Copy the link from the browser address bar to share this post.": "Chia sẻ chưa bắt đầu. Hãy sao chép liên kết từ thanh địa chỉ trình duyệt để chia sẻ bài viết này.", + "Bookmark could not be saved. Try again in a moment; the post itself stays open.": "Không thể lưu dấu trang. Hãy thử lại sau; bài viết vẫn đang mở.", "No summary is available for this record yet.": "Chưa có bản tóm tắt cho bản ghi này.", "Saved evidence is still available.": "Bằng chứng đã lưu vẫn có thể xem.", "Showing the first {shown} of {total} posts known at this cutoff.": @@ -2110,6 +2110,51 @@ const TRANSLATIONS: Partial>> = { }, }; +/** Customer-facing labels keep implementation vocabulary out of the reader UI. */ +const CUSTOMER_COPY: Record> = { + en: { + "View related information": "View related information", + "Related information": "Related information", + "Author context": "Author context", + "Record details": "Record details", + "Related projects": "Related projects", + "Why this item is listed": "Why this item is listed", + "Category": "Category", + "How this item was found": "How this item was found", + "Earlier source version": "Earlier source version", + "Loading related information...": "Loading related information...", + "Close record details": "Close record details", + }, + ko: { + "View related information": "관련 정보 보기", + "Related information": "관련 정보", + "Author context": "작성자 맥락", + "Record details": "기록 세부 정보", + "Related projects": "관련 프로젝트", + "Why this item is listed": "이 항목이 표시된 이유", + "Category": "분류", + "How this item was found": "이 항목을 찾은 방법", + "Earlier source version": "이전 기록 버전", + "Loading related information...": "관련 정보를 불러오는 중...", + "Close record details": "기록 세부 정보 닫기", + "No related information is available. Open a visible post next.": "관련 정보가 없습니다. 다음으로 표시되는 글을 여세요.", + "Some related information is not shown. Open a source post to continue.": "일부 관련 정보가 표시되지 않습니다. 계속하려면 원본 글을 여세요.", + "Related information is unavailable for this record. Open a visible post next.": "이 기록의 관련 정보를 사용할 수 없습니다. 다음으로 표시되는 글을 여세요.", + "This information reflects an earlier view. Compare it with the current record next.": "이 정보는 이전 기준으로 작성되었습니다. 다음으로 현재 기록과 비교하세요.", + "This suggestion was not accepted. Open the evidence to review it.": "이 제안은 수락되지 않았습니다. 근거를 열어 검토하세요.", + "Related information is unavailable. Open a visible post next.": "관련 정보를 사용할 수 없습니다. 다음으로 표시되는 글을 여세요.", + }, + zh: { + "View related information": "查看相关信息", "Related information": "相关信息", "Author context": "作者背景", "Record details": "记录详情", "Related projects": "相关项目", "Why this item is listed": "显示此项目的原因", "Category": "分类", "How this item was found": "此项目的发现方式", "Earlier source version": "较早的记录版本", "Loading related information...": "正在加载相关信息…", "Close record details": "关闭记录详情", "No related information is available. Open a visible post next.": "暂无相关信息。接下来打开一条可见记录。", "Some related information is not shown. Open a source post to continue.": "部分相关信息未显示。请打开来源文章继续。", "Related information is unavailable for this record. Open a visible post next.": "此记录的暂无信息。请打开一条可见的记录。", "This information reflects an earlier view. Compare it with the current record next.": "此信息反映较早的视图。请与当前记录进行比较。", "This suggestion was not accepted. Open the evidence to review it.": "此建议未被接受。请打开证据进行复核。", "Related information is unavailable. Open a visible post next.": "相关信息不可用。请打开一条可见的记录。", + }, + ja: { + "View related information": "関連情報を見る", "Related information": "関連情報", "Author context": "作成者の背景", "Record details": "記録の詳細", "Related projects": "関連プロジェクト", "Why this item is listed": "この項目が表示される理由", "Category": "分類", "How this item was found": "この項目の見つけ方", "Earlier source version": "以前の記録バージョン", "Loading related information...": "関連情報を読み込んでいます…", "Close record details": "記録の詳細を閉じる", "No related information is available to open a visible post next.": "関連情報はありません。次に表示される投稿を開いてください。", "Some related information is not shown. Open a source post to continue.": "一部の関連情報は表示されません。続けるには元の投稿を開いてください。", "Related information is unavailable for this record. Open a visible post next.": "この記録の関連情報を利用できません。次に表示される投稿を開いてください。", "This information reflects an earlier view. Compare it with the current record next.": "この情報は以前の基準を反映しています。次に現在の記録と比較してください。", "This suggestion was not accepted. Open the evidence to review it.": "この提案は受け入れられませんでした。証拠を開いて確認してください。", "Related information is unavailable. Open a visible post next.": "関連情報を利用できません。次に表示される投稿を開いてください。", + }, + vi: { + "View related information": "Xem thông tin liên quan", "Related information": "Thông tin liên quan", "Author context": "Bối cảnh tác giả", "Record details": "Chi tiết bản ghi", "Related projects": "Dự án liên quan", "Why this item is listed": "Lý do mục này được hiển thị", "Category": "Phân loại", "How this item was found": "Cách tìm thấy mục này", "Earlier source version": "Phiên bản bản ghi trước đó", "Loading related information...": "Đang tải thông tin liên quan…", "Close record details": "Đóng chi tiết bản ghi", "No related information is available to open a visible post next.": "Chưa có thông tin liên quan. Hãy mở bài viết hiển thị tiếp theo.", "Related information is unavailable for this record. Open a visible post next.": "Không có thông tin liên quan cho bản ghi này. Hãy mở bài viết hiển thị tiếp theo.", "This information reflects an earlier view. Compare it with the current record next.": "Thông tin này phản ánh chế độ xem trước đó. Hãy so sánh với bản ghi hiện tại tiếp theo.", "This suggestion was not accepted. Open the evidence to review it.": "Đề xuất này chưa được chấp nhận. Hãy mở bằng chứng để xem lại.", "Related information is unavailable. Open a visible post next.": "Không có thông tin liên quan. Hãy mở bài viết hiển thị tiếp theo.", + }, +}; + function isLocale(value: string | null | undefined): value is Locale { return Boolean(value && SUPPORTED_LOCALES.includes(value as Locale)); } @@ -2163,7 +2208,7 @@ export function useLocale(): Locale { } export function t(key: string): string { - return TRANSLATIONS[currentLocale]?.[key] ?? key; + return CUSTOMER_COPY[currentLocale][key] ?? TRANSLATIONS[currentLocale]?.[key] ?? key; } export function tf(key: string, values: Record): string { diff --git a/frontend/src/ontologyExplorerI18n.ts b/frontend/src/ontologyExplorerI18n.ts index 18892fedc..961dbb338 100644 --- a/frontend/src/ontologyExplorerI18n.ts +++ b/frontend/src/ontologyExplorerI18n.ts @@ -4,7 +4,7 @@ const ONTOLOGY_EXPLORER_COPY = { en: { "Load next relation page": "Load next relation page", "Neighborhood truncated. Load the next relation page or inspect one edge.": - "Neighborhood truncated. Load the next relation page or inspect one edge.", + "Some related information is not shown. Open a source post to continue.", "Neighborhood reached the authorized query bound. Narrow the property filter or reduce traversal depth.": "Neighborhood reached the authorized query bound. Narrow the property filter or reduce traversal depth.", "No direct evidence post is attached. Review the provenance reference above.": @@ -13,7 +13,7 @@ const ONTOLOGY_EXPLORER_COPY = { ko: { "Load next relation page": "다음 관계 페이지 불러오기", "Neighborhood truncated. Load the next relation page or inspect one edge.": - "이웃 그래프가 제한되었습니다. 다음 관계 페이지를 불러오거나 연결 하나를 검토하세요.", + "일부 관련 정보가 표시되지 않습니다. 계속하려면 원본 글을 여세요.", "Neighborhood reached the authorized query bound. Narrow the property filter or reduce traversal depth.": "권한 범위의 조회 한도에 도달했습니다. 관계 속성 필터를 좁히거나 탐색 깊이를 줄이세요.", "No direct evidence post is attached. Review the provenance reference above.": @@ -22,7 +22,7 @@ const ONTOLOGY_EXPLORER_COPY = { zh: { "Load next relation page": "加载下一页关系", "Neighborhood truncated. Load the next relation page or inspect one edge.": - "邻域图已截断。请加载下一页关系或检查一条边。", + "部分相关信息未显示。请打开来源文章继续。", "Neighborhood reached the authorized query bound. Narrow the property filter or reduce traversal depth.": "已达到授权查询上限。请缩小属性筛选范围或降低遍历深度。", "No direct evidence post is attached. Review the provenance reference above.": @@ -31,7 +31,7 @@ const ONTOLOGY_EXPLORER_COPY = { ja: { "Load next relation page": "次の関係ページを読み込む", "Neighborhood truncated. Load the next relation page or inspect one edge.": - "近傍グラフは制限されています。次の関係ページを読み込むか、1本のエッジを確認してください。", + "一部の関連情報は表示されません。続けるには元の投稿を開いてください。", "Neighborhood reached the authorized query bound. Narrow the property filter or reduce traversal depth.": "認可されたクエリ上限に達しました。プロパティの絞り込みを強めるか、探索深度を下げてください。", "No direct evidence post is attached. Review the provenance reference above.": @@ -40,7 +40,7 @@ const ONTOLOGY_EXPLORER_COPY = { vi: { "Load next relation page": "Tải trang quan hệ tiếp theo", "Neighborhood truncated. Load the next relation page or inspect one edge.": - "Vùng lân cận đã bị giới hạn. Hãy tải trang quan hệ tiếp theo hoặc kiểm tra một cạnh.", + "Một số thông tin liên quan không được hiển thị. Hãy mở bài viết nguồn để tiếp tục.", "Neighborhood reached the authorized query bound. Narrow the property filter or reduce traversal depth.": "Đã đạt giới hạn truy vấn được cấp quyền. Hãy thu hẹp bộ lọc thuộc tính hoặc giảm độ sâu duyệt.", "No direct evidence post is attached. Review the provenance reference above.":
          {t("Exact values")}