LintPDF LoupePDF

title: "Theme, i18n, telemetry, read-only mode" description: "Brand tokens, translation strings, analytics hooks, and the read-only flag for share-link viewers. All optional — the no-op defaults keep OSS hosts fast." group: "Reference" order: 8

Theme, i18n, telemetry, read-only mode

Three of the ViewerServices fields are infrastructure rather than data: tokens, i18n, telemetry. They always have safe defaults; override when you need to plug into your brand palette, translation table, or analytics. The readOnly flag lives on ViewerHostContext and toggles write-only UI.

Theme tokens

interface ThemeTokens {
  readonly primary: string;   // brand primary
  readonly accent: string;    // brand accent
  readonly bg: string;        // surface background
  readonly fg: string;        // foreground / body text
  readonly border: string;    // hairline border
  // Optional brand-identity fields read by <LoupePDFDemo> as a
  // fallback when the equivalent props (`brand`, `brandLogoUrl`)
  // aren't set. Lets a host bundle palette + logo + label into a
  // single tokens object.
  readonly logoUrl?: string;        // brand logo image URL
  readonly logoText?: string;       // brand label (default: "LoupePDF")
  readonly logoMaxHeight?: number;  // pixel cap on logo height (default: 24)
  readonly logoAlt?: string;        // alt text for the logo <img>
}

defaultThemeTokens is a neutral light palette:

import { defaultThemeTokens } from "@printwithsynergy/loupe-pdf/plugin";

// {
//   primary: "#0f172a",
//   accent:  "#3b82f6",
//   bg:      "#ffffff",
//   fg:      "#0f172a",
//   border:  "#e2e8f0",
// }

darkThemeTokens is a dark palette preset for demo and dark-mode UIs:

import { darkThemeTokens } from "@printwithsynergy/loupe-pdf/plugin";

// {
//   primary: "#0f172a",
//   accent:  "#3b82f6",
//   bg:      "#0e0a14",
//   fg:      "#f5f3f7",
//   border:  "#2b2138",
// }

<LoupePDFDemo> uses darkThemeTokens by default; <LoupePDFViewer> uses defaultThemeTokens.

Pass your own through services.tokens:

const services: ViewerServices = {
  // …
  tokens: {
    primary: "#1a3a7a",
    accent: "#2563eb",
    bg: "#ffffff",
    fg: "#0f172a",
    border: "#e2e8f0",
  },
};

Plugins read from ctx.services.tokens rather than hardcoding hex strings, so swapping a brand palette is a single context-value change.

i18n

interface I18nService {
  t(key: string, params?: Record<string, string | number>): string;
}

The noopI18n default returns the key unchanged with {param} placeholders substituted. Drop in a real translator as needed:

import type { I18nService } from "@printwithsynergy/loupe-pdf/plugin";

export const i18n: I18nService = {
  t: (key, params) => translateWithICU(key, params),
};

Suitable for English-only environments and tests, the no-op behaves like:

noopI18n.t("hello.name", { name: "Ada" });    // "hello.name"
// (the key is returned because no entry exists; placeholders still
// substitute when present in the key text itself)

Telemetry

interface TelemetryService {
  track(event: string, properties?: Record<string, unknown>): void;
}

noopTelemetry drops every event on the floor. Wire your analytics by overriding:

import type { TelemetryService } from "@printwithsynergy/loupe-pdf/plugin";

export const telemetry: TelemetryService = {
  track: (event, props) => window.analytics?.track(event, props),
};

OSS hosts that don't want to ship analytics can leave the no-op default — no events will leave the browser.

Read-only mode

Set ViewerHostContext.readOnly to true to suppress write-only UI.

<ViewerHostContext.Provider
  value={{
    apiBase: "/api/share/abc123",
    jobApiBase: "/api/share/abc123",
    readOnly: true,
  }}
>
  …
</ViewerHostContext.Provider>

What flips:

  • AnnotationCanvas skips its autosave path entirely (reads still work, saves are a no-op).
  • MobileDrawer hides annotation, share, and verdict controls based on the same flag.
  • Your own host UI should branch on useViewerHost().readOnly to hide any control that mutates server state.

Public-token / share-link viewers typically run with readOnly: true and a constrained apiBase (/api/share/<token> etc.), with annotations read but not written. The annotation service can be wired to a no-op saveForPage / remove even when readOnly is false, but flipping the host flag is the standard pattern.