Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,142 @@
# FormatBridge
## Purpose
`FormatBridge` is the dependency-injection seam that lets
`@suaveplan/i18n-icu-parser` render ICU format placeholders
(`{value, number}`, `{amount, number, currency=USD}`,
`{date, date, short}`, `{ts, time, long}`,
`{delta, relativeTime}`) without statically depending on any
formatter package. The renderer (`renderNodes`) consults whichever
methods are present on the bridge and falls back to
`String(value)` when a method (or the bridge itself) is undefined.
This keeps the parser package a true universal leaf with no
`Intl.*` runtime cost when the consumer doesn't need locale-aware
formatting.
The slim `@suaveplan/i18n` core (Wave 6) constructs a default
bridge by importing the formatter packages
(`@suaveplan/i18n-formatter-number`, `-date`, `-currency`,
`-relative-time`) and threads it through every render call so
consumers who use `createI18n({ ... })` get full ICU output with
zero wiring. Consumers who construct a `MessageParser` directly
MAY pass their own bridge or accept the no-Intl fallback.
## Features
- Five method slots, every one OPTIONAL: `number`, `currency`,
`date`, `time`, `relativeTime`
- Renderer never throws on missing bridge or missing method —
always falls back to `String(value)` (or
`${currency} ${String(value)}` for currency)
- Style enums (`NumberFormatStyle`, `DateStyle`,
`RelativeTimeStyle`) declared in `format-bridge.types.ts` for
type-only imports without runtime cost
- `noopFormatBridge` exported as a frozen singleton consumers can
use directly or spread-and-extend
- Single-file subpath export (`@suaveplan/i18n-icu-parser/parser/format-bridge`)
so the slim core's wiring imports only this one module — keeps
bundle size minimal
## Usage: Basic Example
```typescript
import { renderNodes, type FormatBridge } from "@suaveplan/i18n-icu-parser/parser";
const bridge: FormatBridge = {
number: (value, locale) =>
new Intl.NumberFormat(locale).format(value),
};
const nodes = [
{ type: "text" as const, value: "Total: " },
{
type: "typed-argument" as const,
name: "amount",
argType: "number" as const,
style: "decimal",
},
];
const text = renderNodes(nodes, { amount: 1234.5 }, {
locale: "en-US",
formatBridge: bridge,
});
// "Total: 1,234.5"
```
## Usage: Advanced Example
```typescript
import {
MessageParser,
noopFormatBridge,
type FormatBridge,
} from "@suaveplan/i18n-icu-parser/parser";
// Spread-and-extend pattern: keep the no-op fallback for anything
// you don't care about, override just the methods you need.
const bridge: FormatBridge = {
...noopFormatBridge,
currency: (value, currency, locale) =>
new Intl.NumberFormat(locale, {
style: "currency",
currency,
}).format(value),
date: (value, locale, style = "medium") => {
const d = value instanceof Date ? value : new Date(value);
const presets: Record<string, Intl.DateTimeFormatOptions> = {
short: { dateStyle: "short" },
medium: { dateStyle: "medium" },
long: { dateStyle: "long" },
full: { dateStyle: "full" },
};
return new Intl.DateTimeFormat(locale, presets[style]).format(d);
},
};
const parser = new MessageParser();
const out = parser.format(
"Total: {amount, number, currency/USD} on {when, date, long}",
{ amount: 1234.5, when: new Date("2026-05-05T00:00:00Z") },
{ locale: "en-US", formatBridge: bridge }
);
```
## API Reference
### `interface FormatBridge`
| Method | Signature | Notes |
|---|---|---|
| `number?` | `(value, locale, style?) => string` | Style is `NumberFormatStyle \| string \| undefined` so notation styles (`compact`, `scientific`) pass through as raw strings |
| `currency?` | `(value, currency, locale) => string` | `currency` is an ISO 4217 code |
| `date?` | `(value, locale, style?) => string` | Default style is `medium` |
| `time?` | `(value, locale, longForm?) => string` | `longForm: true` should include seconds |
| `relativeTime?` | `(value, locale, style?) => string` | Style is `"long" \| "short" \| "narrow"` |
### `noopFormatBridge: Readonly<FormatBridge>`
Frozen identity bridge. Every method coerces inputs to
`String(value)` (or `${currency} ${String(value)}` for
`currency`). Safe to share by reference; mutation throws TypeError.
## Implementation Notes
- The bridge is intentionally OPTIONAL across the entire surface
area. The renderer never asserts `formatBridge != null`; it
uses optional chaining (`bridge?.number?.(...)`) and falls
through to `String(value)` on undefined results. This means a
partial bridge (only `number` defined) still renders date and
currency placeholders — they just emit raw `String(value)` for
the missing methods.
- `noopFormatBridge` is `Object.freeze`d at module load. This
guards against accidental mutation in tests or downstream
packages corrupting the shared singleton.
- `currency` returns `${code} ${String(value)}` rather than
`String(value)` alone so the no-op fallback still surfaces the
ISO 4217 code in test output, making fallback hits visible
during development.
- The style enums are declared in `format-bridge.types.ts` to keep
this implementation file zero-runtime-cost when imported for
the bridge interface alone.

View File

@@ -0,0 +1,117 @@
# message-render
## Purpose
`message-render` provides two standalone utility functions for `@suaveplan/i18n`: `extractVariables`, which collects all unique variable names referenced in a parsed message AST, and `renderNodes`, which renders an AST to a string using caller-supplied values. These are the hot-path primitives consumed by `MessageParser.render` and exposed as public exports for callers that need to work with pre-parsed ASTs directly.
Separating these pure functions from the `MessageParser` class allows them to be imported without pulling in the full parser, which is useful for rendering pre-parsed messages in tight loops or edge environments.
## Features
- `extractVariables` — collects unique variable names from a `MessageNode[]`, recursing into plural and select choices
- `renderNodes` — renders a `MessageNode[]` to a string, supporting text, variable, plural, and select node types
- Plural `offset` support: `count - offset` is passed as `"#"` into nested renders
- Exact-match plural keys (`=0`, `=1`) take priority over category keys (`one`, `other`)
- Nullable/undefined values are rendered as empty strings
- Optional `pluralSelect` callback for language-specific plural category resolution
## Usage: Basic Example
```ts
import { extractVariables, renderNodes } from "@suaveplan/i18n-icu-parser/parser";
const nodes = [
{ type: "text", value: "Hello, " },
{ type: "variable", name: "name" },
{ type: "text", value: "!" },
];
const vars = extractVariables(nodes);
console.log(vars); // ["name"]
const text = renderNodes(nodes, { name: "Alice" });
console.log(text); // "Hello, Alice!"
```
## Usage: Advanced Example
```ts
import { renderNodes } from "@suaveplan/i18n-icu-parser/parser";
import type { MessageNode } from "@suaveplan/i18n-icu-parser/parser";
// Pre-parsed plural node (normally produced by MessageParser)
const nodes: MessageNode[] = [
{
type: "plural",
variable: "count",
offset: 0,
choices: {
one: [{ type: "variable", name: "#" }, { type: "text", value: " item" }],
other: [{ type: "variable", name: "#" }, { type: "text", value: " items" }],
},
},
];
// Custom plural selector for a language with more categories
const pluralSelect = (n: number) => (n === 1 ? "one" : "other");
console.log(renderNodes(nodes, { count: 1 }, pluralSelect)); // "1 item"
console.log(renderNodes(nodes, { count: 5 }, pluralSelect)); // "5 items"
```
## API Reference
### `extractVariables(nodes: MessageNode[]): string[]`
Extract all unique variable names referenced in `nodes`. Recurses into `plural` and `select` choice branches. Excludes the special `"#"` sentinel used inside plural branches.
**Returns:** Array of unique variable name strings in order of first appearance.
### `renderNodes(nodes: MessageNode[], values: Record<string, unknown>, options?: RenderOptions | RenderContext | pluralSelect): string`
Render `nodes` to a string.
| Node type | Behaviour |
|-----------|-----------|
| `text` | Appended verbatim |
| `variable` | Replaced with `String(values[name])`, or `""` if `null`/`undefined`. Throws `I18nError(I18N.INTERPOLATION_ERROR)` when the key is ABSENT. `"#"` resolves from `values["#"]` |
| `plural` | Resolves count, adjusts by offset, selects branch by exact key then category then `"other"`, recurses |
| `select` | Selects branch by string value of variable, falls back to `"other"`, recurses |
| `typed-argument` | Routes through `options.formatBridge` (`number` / `currency` / `date` / `time`); falls back to `String(value)` if bridge or method is undefined |
| `tag` | If `values[tagName]` is a function, calls it with the rendered inner string; otherwise renders inner content as-is |
The `pluralSelect(count, category)` callback is used to select plural categories for plural nodes. When absent, falls back to `count === 1 ? "one" : "other"`.
### `RenderOptions`
```typescript
interface RenderOptions extends RenderContext {
readonly formatBridge?: FormatBridge;
}
interface RenderContext {
readonly locale?: string;
readonly pluralSelect?: (count: number, category: string) => string;
readonly ordinalSelect?: (count: number) => string;
}
```
The `formatBridge` slot is the dependency-injection seam between
this parser package and the per-formatter packages
(see `format-bridge.md`). When supplied, the renderer dispatches
ICU format placeholders (`{value, number}`, `{amount, number,
currency=USD}`, `{date, date, short}`, `{ts, time, long}`) to the
bridge methods. When the bridge OR a specific method is undefined,
the renderer coerces the value to `String(value)` and never throws.
## Implementation Notes
`renderNodes` is recursive: plural and select nodes recursively call `renderNodes` on the matched choice's node array. For plural nodes, the adjusted count (`count - offset`) is injected as `values["#"]` into the recursive call so that `#` placeholders inside plural branches resolve correctly.
Exact-match keys (e.g. `=0`, `=2`) in plural choices are checked before category keys, matching the ICU specification for exact-value overrides.
The `formatBridge` slot keeps this package a true universal
leaf with no `Intl.*` dependency. The slim `@suaveplan/i18n` core
(Wave 6) constructs a default bridge over the formatter packages
and threads it through `createI18n({ ... })` so consumers get
locale-aware output automatically.

View File

@@ -0,0 +1,99 @@
# MessageParser
## Purpose
`MessageParser` converts ICU-like message strings into a typed AST (Abstract Syntax Tree), and provides utilities to render that AST back to a string with caller-supplied values. It enables rich localised messages that include plural selection, gender/select branching, and nested variable substitution — without pulling in a full ICU implementation.
The parser is also available in a simpler non-ICU mode that only processes `{variable}` substitution with configurable delimiters, which is sufficient for many template-string use cases.
## Features
- Parses ICU-style plural: `{count, plural, one {# item} other {# items}}`
- Parses ICU-style select: `{gender, select, male {He} female {She} other {They}}`
- Nested ICU expressions and recursive choices
- Plural `offset:` support (`{count, plural, offset:1 ...}`)
- Exact-value plural matching: `=0 {no results}`, `=1 {one result}`
- `#` placeholder inside plural branches resolves to `count - offset`
- Non-ICU simple-variable mode with configurable open/close delimiters
- `extractVariables(nodes)` — extracts all referenced variable names from an AST
- `renderNodes(nodes, values, pluralSelect?)` — renders an AST to a string
## Usage: Basic Example
```typescript
import { MessageParser } from "@suaveplan/i18n-icu-parser/parser";
const parser = new MessageParser();
// Simple variable
const { nodes } = parser.parse("Hello, {name}!");
const text = parser.render({ nodes, variables: ["name"] }, { name: "World" });
// text === "Hello, World!"
// One-liner
const msg = parser.format(
"{count, plural, one {# apple} other {# apples}}",
{ count: 3 }
);
// msg === "3 apples"
```
## Usage: Advanced Example
```typescript
import { MessageParser, renderNodes, extractVariables } from "@suaveplan/i18n-icu-parser/parser";
import { PluralizationEngine } from "@suaveplan/i18n-icu-parser/pluralization";
const parser = new MessageParser();
const engine = new PluralizationEngine();
const template = "{gender, select, male {He has} female {She has} other {They have}} {count, plural, one {# item} other {# items}}.";
const parsed = parser.parse(template);
// Use PluralizationEngine for the plural select callback
const text = parser.render(parsed, { gender: "female", count: 5 }, (count) =>
engine.getCategory(count, "en")
);
// text === "She has 5 items."
// Extract variable names from a template without rendering
const vars = extractVariables(parsed.nodes);
// vars === ["gender", "count"]
```
## API Reference
### `new MessageParser(options?: ParserOptions)`
| Option | Default | Description |
|--------|---------|-------------|
| `variablePrefix` | `"{"` | Simple-mode open delimiter |
| `variableSuffix` | `"}"` | Simple-mode close delimiter |
| `icuSyntax` | `true` | Enable plural/select parsing |
### `.parse(message): ParsedMessage`
Returns `{ nodes: MessageNode[]; variables: string[] }`.
### `.render(parsed, values, pluralSelect?): string`
Renders a `ParsedMessage` to a string. `pluralSelect(count, category)` is used to select plural forms — defaults to `one`/`other` if omitted.
### `.format(message, values, pluralSelect?): string`
Convenience: `parse` + `render` in one call.
### `extractVariables(nodes: MessageNode[]): string[]`
Standalone export. Returns all unique variable names (excluding `"#"`) referenced in the node tree.
### `renderNodes(nodes, values, pluralSelect?): string`
Standalone export. Renders an array of `MessageNode` to a string.
## Implementation Notes
- The parser is a hand-written recursive descent parser; it does not use a grammar generator or regex for structural parsing (only for simple-mode delimiter matching)
- ICU `plural` and `select` nodes are parsed in a single forward pass; backtracking is never required
- The `#` sentinel in plural branches is emitted as a `VariableNode` with `name === "#"` and resolved during rendering by injecting `{ "#": adjusted }` into the values map
- `renderNodes` is exported to allow rendering pre-parsed ASTs without re-parsing, which is the expected hot-path in production (parse once, render many times)