import * as preact from 'preact'; import { ComponentChildren } from 'preact'; import { DiagnosticBag } from '@clean-jsdoc-theme/utils'; /** * The chrome catalog — the canonical list of UI strings the theme renders. * * This is the single source of truth for translatable chrome (per the plan's * `chrome.*` namespace). It's authored as a nested object for readability, then * flattened to dotted keys (`chrome.search.placeholder`) for the runtime `t` * lookup. The dotted-key union {@link ChromeKey} is *derived from this object*, * so every `t('chrome.…')` call site is compile-checked against the real keys — * a typo or a removed key is a type error, not a silent fallback. * * Values may carry `{name}` interpolation tokens (see {@link interpolate}); the * names here are the contract translators must preserve. * * Pure + browser-safe: this module is bundled into the browser by rang. */ /** * The default (English) chrome strings, grouped by the component that owns them. * Strings mirror the literals currently hardcoded in rang — this is the baseline * the rang refactor (Phase 1) replaces with `t(key)` calls. */ declare const EN_CHROME: { readonly common: { /** Dialog/drawer close button + the palette footer's Close button. */ readonly close: "Close"; }; readonly search: { /** CtrlK search input placeholder. */ readonly placeholder: "Search docs..."; /** Accessible label for the search input. */ readonly inputLabel: "Search query"; /** Accessible label for the header search trigger. */ readonly triggerLabel: "Search"; /** Tooltip on the header search trigger. */ readonly triggerTitle: "Search (Ctrl K)"; /** Accessible label for the search dialog. */ readonly dialogLabel: "Search"; /** Accessible label for the results list. */ readonly resultsLabel: "Search results"; /** Section heading above recent searches. */ readonly recent: "Recent"; /** Section heading above favorite searches. */ readonly favorite: "Favorite"; /** Empty-state when a query matches nothing. */ readonly noResults: "No matching pages"; /** Empty-state when there's no query and no saved searches. */ readonly emptyHint: "Type to search the docs"; /** Connector for a result's parent context ("Title in Parent"). Carries the context. */ readonly inContext: "in {context}"; /** Keyboard-hint line in the palette footer. */ readonly footerHint: "↑↓ to navigate · ↵ to open · esc to close"; /** Star-button label (favorites). Carries the page title. */ readonly saveToFavorites: "Save {title} to favorites"; /** Un-star-button label (favorites). Carries the page title. */ readonly removeFromFavorites: "Remove {title} from favorites"; /** Forget-button label (recents). Carries the page title. */ readonly removeFromRecent: "Remove {title} from recent searches"; }; readonly footer: { /** Repository link label. */ readonly repository: "Repository"; }; readonly nav: { /** Accessible label for the sidebar navigation landmark. */ readonly docNavLabel: "Documentation navigation"; /** Accessible label for the mobile-nav open button. */ readonly open: "Open navigation"; /** Tooltip on the mobile-nav trigger. */ readonly menu: "Menu"; /** Accessible label for the mobile-nav drawer dialog. */ readonly drawerLabel: "Navigation"; }; readonly toc: { /** Accessible label for the on-this-page table of contents. */ readonly label: "On this page"; }; readonly theme: { /** Tooltip on the theme toggle. */ readonly toggleTitle: "Toggle theme"; /** Accessible label on the theme toggle. Carries the target mode. */ readonly switchTo: "Switch to {mode} theme"; }; readonly settings: { /** Dialog title + trigger label. */ readonly title: "Settings"; /** Font-size group heading/label. */ readonly fontSize: "Font size"; /** Line-spacing group heading/label. */ readonly lineSpacing: "Line spacing"; readonly sizeSmall: "Small"; readonly sizeDefault: "Default"; readonly sizeLarge: "Large"; readonly spacingCompact: "Compact"; readonly spacingDefault: "Default"; readonly spacingRelaxed: "Relaxed"; }; readonly pager: { /** Accessible label for the prev/next pager landmark. */ readonly label: "Pagination"; /** "Previous page" eyebrow label. */ readonly previous: "Previous"; /** "Next page" eyebrow label. */ readonly next: "Next"; }; readonly code: { /** Header label shown on a code block when it has no `filename`. */ readonly label: "CODE"; /** Accessible label for the code-variant tablist. */ readonly variantsLabel: "Code variants"; /** Copy-to-clipboard button accessible label (idle). */ readonly copy: "Copy to clipboard"; /** Copy-to-clipboard button accessible label (after copy). */ readonly copied: "Copied to clipboard"; /** Visible copy-button text (idle). */ readonly copyShort: "Copy"; /** Visible copy-button text (after copy). */ readonly copiedShort: "Copied!"; /** Accessible label for the heading-anchor copy-link button. */ readonly copyLink: "Copy link to this section"; }; readonly copyPage: { /** Accessible label for the split-button's extra-options trigger. */ readonly moreOptions: "More copy options"; /** Accessible label for the dropdown menu of copy actions. */ readonly menuLabel: "Copy page options"; readonly copyTitle: "Copy page"; /** Primary-button text after a successful copy. */ readonly copied: "Copied"; readonly copyDescription: "Copy page as Markdown for LLMs"; readonly viewTitle: "View Markdown"; readonly viewDescription: "View this page as plain text"; readonly claudeTitle: "Open in Claude"; readonly claudeDescription: "Ask Claude about this page"; readonly chatgptTitle: "Open in ChatGPT"; readonly chatgptDescription: "Ask ChatGPT about this page"; readonly perplexityTitle: "Open in Perplexity"; readonly perplexityDescription: "Ask Perplexity about this page"; }; readonly embed: { /** Click-to-load button label (untitled embed). */ readonly load: "Load embedded content"; /** Click-to-load button label, carrying the embed title. */ readonly loadTitled: "Load embedded content: {title}"; /** Poster heading when the embed has no title. */ readonly posterTitle: "Embedded content"; /** The small "Load" pill on the click-to-load poster. */ readonly loadPill: "Load"; }; readonly language: { /** Accessible label + tooltip for the language switcher. */ readonly label: "Language"; }; readonly playground: { /** Trigger label + menu accessible label for the "open in playground" dropdown. */ readonly openIn: "Open Code in"; /** A single playground provider menu item, carrying the provider name. */ readonly openInProvider: "Open in {provider}"; }; }; /** The authored shape of {@link EN_CHROME}. */ type ChromeCatalog = typeof EN_CHROME; /** * Recursive dotted-path union of a nested string object: `{ a: { b: '' } }` * yields `'a.b'`. Stops at string leaves. */ type DotPaths = { [K in keyof T & string]: T[K] extends string ? `${Prefix}${K}` : DotPaths; }[keyof T & string]; /** * Every chrome key, fully qualified under the `chrome.` namespace — e.g. * `'chrome.search.placeholder'`. This is the compile-checked key set for `t`. */ type ChromeKey = `chrome.${DotPaths}`; /** A flat message map: dotted key → string. Both `chrome.*` and `api.*` live here. */ type Messages = Record; /** * The default chrome catalog as a flat `chrome.*` message map — the ultimate * fallback in the resolution chain and the canonical reference for validation. */ declare const EN_CHROME_FLAT: Messages; /** * Named `{token}` interpolation — the one substitution grammar shared by the * runtime `t` and the token-parity validator, so what `t` substitutes and what * validation checks can never drift apart. * * A token is `{identifier}` where identifier is a JS-style name * (`[A-Za-z_][A-Za-z0-9_]*`). This deliberately excludes: * - `{@link Foo}` — starts with `@`, not an identifier; * - `{ some.code }` — contains spaces/dots; * so JSDoc inline tags and code spans in a slot pass through untouched. * * ICU plurals/selects are out of scope (deferred per the plan). * * Pure + browser-safe. */ /** Variables for substitution. Numbers are stringified with `String()`. */ type InterpolationVars = Record; /** * Substitute `{name}` tokens in `template` from `vars`. A token with no matching * var is left **verbatim** (so a missing variable degrades to visible `{name}` * rather than an empty hole, and non-token braces survive). With no `vars`, the * template is returned unchanged. */ declare function interpolate(template: string, vars?: InterpolationVars): string; /** * The unique set of interpolation token names in `template`, in first-seen * order. Used by token-parity validation to compare a translation against its * source string. */ declare function interpolationTokens(template: string): string[]; /** * The translation core: the immutable per-render i18n value and the lookup * functions that resolve a key through the fallback chain. * * **Fallback chain (the locked decision):** active locale → default locale → * the key itself (chrome) or the source text (API slots). An *empty-string* * value counts as untranslated and falls through — so a half-filled catalog * shows the default, never a blank. * * Pure + browser-safe — no store, no reactivity, no I/O. */ /** * The immutable i18n value carried per render. There is no setter: locale never * changes at runtime (it's a build dimension), so this is a static carrier, not * a store. `messages` is the active locale; `fallback` is the default locale * (typically English) consulted when a key is missing/empty. */ interface I18n { /** Active locale code, e.g. `'fr'`. */ locale: string; /** Default locale code, e.g. `'en'`. */ defaultLocale: string; /** Active-locale messages (`chrome.*` + `api.*`, flat dotted keys). */ messages: Messages; /** Default-locale messages, the second link in the fallback chain. */ fallback: Messages; } /** * The `t` function shape. {@link ChromeKey} gives autocomplete + compile-checks * on chrome keys; `(string & {})` keeps the union open for dynamic `api.*` slot * keys without widening away the literal suggestions. */ type TFunc = (key: ChromeKey | (string & {}), vars?: InterpolationVars) => string; /** * Build an {@link I18n} value, defaulting both `defaultLocale` and the * `fallback` map to the canonical English chrome catalog. Callers (islands, SSR) * pass the active `messages`; the EN chrome baseline guarantees chrome keys * always resolve to *something* even before any translation exists. */ declare function createI18n(opts: { locale: string; messages: Messages; defaultLocale?: string; /** Default-locale messages. Defaults to the EN chrome baseline. */ fallback?: Messages; }): I18n; /** * The default i18n value used when no provider is mounted — pure English chrome, * active === fallback. This is what guarantees the **byte-identical no-locale * path**: with no locales configured, `t` returns exactly the EN strings. */ declare const DEFAULT_I18N: I18n; /** * Resolve a key to its raw (un-interpolated) string via active → default, or * `undefined` if neither locale has it. Exposed for callers that need to detect * a miss (e.g. to substitute API source text as the final fallback). */ declare function resolve(i18n: I18n, key: string): string | undefined; /** * Translate a chrome key. Fallback chain: active → default → **the key itself** * (a visible miss beats a blank), then interpolate. */ declare function translate(i18n: I18n, key: string, vars?: InterpolationVars): string; /** * Translate an API slot. Same chain, but the final fallback is the **source * text** (the original doclet description), per the locked decision that API * text falls back to source, never to a key. */ declare function translateSlot(i18n: I18n, key: string, sourceText: string, vars?: InterpolationVars): string; /** * Scope an immutable {@link I18n} value over `children`. Immutable by design — * there is no setter; to "change language" you navigate to another locale's * statically-rendered site. */ declare function LanguageProvider({ value, children, }: { value: I18n; children: ComponentChildren; }): preact.JSX.Element; /** * Read the active i18n value (for cases that need `locale` directly, e.g. the * language switcher's current selection). */ declare function useI18n(): I18n; /** * The translation hook. Returns a memoized `t` (stable across renders while the * carried value is unchanged — safe because the value is immutable) plus the * active `locale`. */ declare function useTranslation(): { t: TFunc; locale: string; }; /** * API-slot key scheme + source-hash for staleness. * * Every translatable API string (a doclet description, a param description, an * example's prose) gets a stable key derived from the symbol's `longname` plus * the field path to that string. Keys are **opaque, deterministic identifiers**: * generated the same way on every build, compared by equality, never parsed * back — so the same symbol+field always lands on the same catalog entry, and a * rename produces a *different* key (caught as new/obsolete by aadesh's merge, * never silently re-pointed). * * Pure + browser-safe — no `node:crypto`; the hash is a plain FNV-1a. */ /** The API namespace prefix (sibling to `chrome.`). */ declare const API_NAMESPACE = "api"; /** * Build the catalog key for an API slot. * * Shape: `api.#`. The field path is joined with `.` * (e.g. `params.0.description`) and is composed of identifiers and array * indices — **never a `#`**. Because the field is `#`-free, everything after the * *last* `#` is unambiguously the field, so the key is injective over * (longname, fieldPath) even when the `longname` itself contains JSDoc namepath * punctuation (`.`, `#`, `~`, `:`, `/`). This invariant is what keeps the same * symbol+field mapping to the same catalog entry on every build. * * @param longname - The doclet longname, e.g. `Foo#bar` or `module:x~Y`. * @param fieldPath - The (`#`-free) path to the string within the doclet, e.g. * `'description'` or `['params', '0', 'description']`. */ declare function apiSlotKey(longname: string, fieldPath: string | readonly string[]): string; /** `true` if `key` belongs to the `api.*` namespace. */ declare function isApiKey(key: string): boolean; /** `true` if `key` belongs to the `chrome.*` namespace. */ declare function isChromeKey(key: string): boolean; /** * A short, stable content hash of a source string, used to detect staleness: a * translation carries the hash of the source text it was made from; when the * source text changes, the hash changes and the translation is flagged stale. * * FNV-1a (32-bit) over UTF-16 code units → 8-char lowercase hex. Deterministic * and platform-independent (no `node:crypto`, so it runs in the browser too). * Not cryptographic — collisions are acceptable for staleness detection. */ declare function sourceHash(input: string): string; /** * Validation primitives for catalogs and slot values. * * These reuse the theme's existing diagnostics spine (`DiagnosticBag`, * `suggestKey` from `@clean-jsdoc-theme/utils/config`) rather than re-inventing * a reporter — so localization findings format and gate exactly like opts * validation. Each primitive **appends to a bag** (creating one if none is * passed) and reports **against the key**, so a downstream `--strict` build can * fail fast on the offending entry before a full N-locale render. * * Posture mirrors `opts.strict`: a *gap* (missing translation) is a warning; a * *malformation* (broken markdown, dropped `{var}`, unknown key) is an error. * * Pure + browser-safe — `utils/config` is itself node-free. */ /** * Catalog shape check against a reference key set (the EN catalog for chrome, or * the freshly-extracted template for API). A key present in `reference` but * missing/empty in `messages` is a **gap** (warning, falls back); a key present * in `messages` but absent from `reference` is **unknown** (error — likely a * stale/renamed key; obsolete keys should have been moved to `_obsolete` by the * caller before validating). Unknown keys get a near-miss "did you mean?" hint. */ declare function validateCatalogShape(messages: Messages, reference: Messages, bag?: DiagnosticBag): DiagnosticBag; /** * Coverage of `messages` against a reference key set: how many reference keys * have a non-empty value. Drives the "fr: 80% translated" report line. */ declare function catalogCoverage(messages: Messages, reference: Messages): { translated: number; total: number; ratio: number; }; /** * Lint a translated slot value for MDX-hostile malformations a translator might * introduce. A broken slot would abort the page compile in dwar (it skips + * reports), so we catch it here, named against the key. Checks: * - code-fence parity (an odd number of ``` ``` ``` markers); * - `{@link …}` termination (every `{@link` has a closing `}`); * - curly-brace balance (MDX reads a stray `{` as a JS expression). * * Escaped braces (`\{`, `\}`) are ignored in the balance check. */ declare function lintSlotMarkdown(value: string, key: string, bag?: DiagnosticBag): DiagnosticBag; /** * Interpolation-token parity between a source string and its translation. Tokens * the source has but the translation **drops**, and tokens the translation * **adds** that the source lacks (a rename or typo), are both errors — a dropped * `{count}` renders a broken sentence, a renamed one never substitutes. */ declare function validateTokenParity(source: string, translated: string, key: string, bag?: DiagnosticBag): DiagnosticBag; export { API_NAMESPACE, type ChromeCatalog, type ChromeKey, DEFAULT_I18N, EN_CHROME, EN_CHROME_FLAT, type I18n, type InterpolationVars, LanguageProvider, type Messages, type TFunc, apiSlotKey, catalogCoverage, createI18n, interpolate, interpolationTokens, isApiKey, isChromeKey, lintSlotMarkdown, resolve, sourceHash, translate, translateSlot, useI18n, useTranslation, validateCatalogShape, validateTokenParity };