first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# @suaveplan/i18n-formatter-collator - Intl.Collator wrapper for the @suaveplan/i18n family — locale-aware string comparison and sorting (sensitivity, numeric-aware natural sort, case-first, ignorePunctuation, custom collation). Per-(locale, options-hash) cache, Zod-validated options, OTel histogram + cache-hit counter, structured I18nError mapping. Brand-new formatter — closes the gap from the original i18n-formatters change scope. Universal leaf package.
## Summary
Wrapper around `Intl.Collator` for the `@suaveplan/i18n` family —
locale-aware string comparison and sorting (sensitivity, numeric-aware
natural sort, case-first, ignorePunctuation, custom collation).
This is a **brand-new formatter** in the i18n family — `Intl.Collator`
was listed in the original `i18n-formatters` change scope but was
never implemented in the monolith. This package closes that gap.
<!-- begin:auto-docs -->
## Documentation tree
- [formatter](./src/formatter/formatter.md) — `CollatorFormatter` and the `compareStrings` / `formatCollation` standalone helpers expose `Intl.Collator` for the `@su…
- [formatter](./src/formatter/formatter.md) — `CollatorFormatter` and the `compareStrings` / `formatCollation` standalone helpers expose `Intl.Collator` for the `@su…
- [helpers](./src/helpers/helpers.md) — The three ergonomic helpers (`compareCaseInsensitive`, `compareNumericAware`, `sortStrings`) pre-bind `compareStrings`…
- [helpers](./src/helpers/helpers.md) — The three ergonomic helpers (`compareCaseInsensitive`, `compareNumericAware`, `sortStrings`) pre-bind `compareStrings`…
- [observability](./src/observability/observability.md) — The Collator formatter exports two OpenTelemetry instruments and a small in-memory state ring so spec-conformance tests…
- [observability](./src/observability/observability.md) — The Collator formatter exports two OpenTelemetry instruments and a small in-memory state ring so spec-conformance tests…
<!-- end:auto-docs -->
## Why this exists
The 14-package i18n modular split (cross-cutting design
`2026-05-05-i18n-modular-split.md`) lifts each `Intl.*` wrapper into
its own sibling package and adds the two formatters
(`Collator` + `Segmenter`) that the original change scope listed but
never shipped. Locale-aware comparison is the right primitive for
sorting names and substring search, and is required by downstream
consumers like `@suaveplan/dedup` and `@suaveplan/react-data-table`.
## Install
```bash
bun add @suaveplan/i18n-formatter-collator
```
## Quick Start
```ts
import {
CollatorFormatter,
compareStrings,
formatCollation,
sortStrings,
compareCaseInsensitive,
compareNumericAware,
} from "@suaveplan/i18n-formatter-collator";
compareStrings("apple", "banana", "en-GB"); // -1
sortStrings(["c", "a", "b"], "en-GB"); // ["a", "b", "c"]
compareCaseInsensitive("a", "A", "en-GB"); // 0
compareNumericAware("file2", "file10", "en-GB"); // -1
const f = new CollatorFormatter({ numeric: true });
f.sort(["log10.txt", "log2.txt", "log1.txt"], "en-GB");
// ["log1.txt", "log2.txt", "log10.txt"]
```
## Modules
| Subpath | Description |
|---|---|
| `@suaveplan/i18n-formatter-collator` | Root barrel — class + standalones + helpers + observability |
| `@suaveplan/i18n-formatter-collator/formatter` | `CollatorFormatter`, `compareStrings`, `formatCollation`, types, schema |
| `@suaveplan/i18n-formatter-collator/helpers` | `compareCaseInsensitive`, `compareNumericAware`, `sortStrings` |
## Modules Documentation
- [formatter.md](src/formatter.md) — `CollatorFormatter`, `compareStrings`, `formatCollation`, `clearCollatorCache`
- [helpers.md](src/helpers.md) — three ergonomic helpers
- [observability.md](src/observability.md) — OTel histogram + cache-hit counter
## Public Surface (snapshot)
| Export | Kind | Origin |
|---|---|---|
| `CollatorFormatter` | class | `formatter.ts` |
| `compareStrings` | function | `formatter.ts` |
| `formatCollation` | function | `formatter.ts` |
| `clearCollatorCache` | function | `formatter.ts` |
| `CollatorFormatterOptionsSchema` | Zod schema | `formatter.schemas.ts` |
| `CollatorFormatterOptions` | type | `formatter.types.ts` |
| `CollatorUsage` | type | `formatter.types.ts` |
| `CollatorSensitivity` | type | `formatter.types.ts` |
| `CollatorCaseFirst` | type | `formatter.types.ts` |
| `CollatorNumeric` | type | `formatter.types.ts` |
| `CollatorFormatterCacheKey` | type | `formatter.types.ts` |
| `compareCaseInsensitive` | function | `helpers.ts` |
| `compareNumericAware` | function | `helpers.ts` |
| `sortStrings` | function | `helpers.ts` |
| `recordCollatorFormat` | function | `observability.ts` |
| `recordCollatorCacheHit` | function | `observability.ts` |
| `getCollatorMetricsState` | function | `observability.ts` |
| `resetCollatorMetricsState` | function | `observability.ts` |
## Versioning
Part of the 14-package i18n modular-split rollout. Stays at `0.1.0`
until the slim core (Wave 6) cuts the family `1.0.0` release.

View File

@@ -0,0 +1,121 @@
# CollatorFormatter
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`CollatorFormatter` and the `compareStrings` / `formatCollation`
standalone helpers expose `Intl.Collator` for the `@suaveplan/i18n`
family at the canonical Wave-3 formatter surface. Locale-aware string
comparison is the right primitive for everything from sorting names
in a locale-correct order to powering substring search that ignores
diacritics.
This is a brand-new formatter — `Intl.Collator` was listed in the
original `i18n-formatters` change scope but never implemented in the
monolith. The split package closes that gap with the canonical Wave-3
shape.
## Features
- `CollatorFormatter` class with `compare(a, b, locale)` and `sort(values, locale)` methods
- `compareStrings(a, b, locale, options?)` standalone — returns `-1 | 0 | 1`
- `formatCollation(values, locale, options?)` standalone — returns a NEW sorted array (input never mutated)
- All `Intl.Collator` options supported: `usage`, `sensitivity`, `ignorePunctuation`, `numeric`, `caseFirst`, `collation`
- Construction-time Zod validation with `INVALID_FORMATTER_OPTIONS` mapping
- `Intl.Collator` `RangeError` mapped to `UNSUPPORTED_LOCALE` with original error attached as `cause`
- Per-(locale, options-hash) cache — repeated calls reuse the same `Intl.Collator`
- `clearCollatorCache()` test-only helper
- Compare result normalised to `-1 | 0 | 1` (matches familiar `Array.sort` callback shape)
## Usage: Basic Example
```ts
import {
compareStrings,
formatCollation,
} from "@suaveplan/i18n-formatter-collator";
compareStrings("apple", "banana", "en-GB"); // -1
formatCollation(["c", "a", "b"], "en-GB"); // ["a", "b", "c"]
// Numeric-aware natural sort.
formatCollation(
["file10", "file2", "file1"],
"en-GB",
{ numeric: true }
); // ["file1", "file2", "file10"]
```
## Usage: Advanced Example
```ts
import {
CollatorFormatter,
type CollatorFormatterOptions,
} from "@suaveplan/i18n-formatter-collator";
// Substring-search collator — accent- and case-insensitive.
const search: CollatorFormatterOptions = {
usage: "search",
sensitivity: "base",
};
const f = new CollatorFormatter(search);
// Search "café" matches "cafe" / "Cafe" / "CAFE".
function matches(needle: string, haystack: string, locale: string) {
// For substring search the collator's compare function
// returns 0 for matching items.
return f.compare(needle, haystack, locale) === 0;
}
matches("café", "cafe", "en-GB"); // true
// Phonebook-style sort for German.
const phonebook = new CollatorFormatter({ collation: "phonebk" });
phonebook.sort(["Müller", "Schmidt", "Mueller"], "de-DE");
```
## API Reference
### `compareStrings(a, b, locale, options?): -1 | 0 | 1`
Standalone helper. Validates options via Zod, looks up or constructs
the cached `Intl.Collator`, and returns the normalised compare result.
Throws `I18nError(INVALID_FORMATTER_OPTIONS)` on Zod failure or
`I18nError(UNSUPPORTED_LOCALE)` on `Intl.Collator` `RangeError`.
### `formatCollation(values, locale, options?): string[]`
Standalone helper. Returns a NEW array sorted via the configured
collator. Input is never mutated.
### `class CollatorFormatter`
- `constructor(options?: CollatorFormatterOptions)` — Zod-validates options
- `compare(a, b, locale): -1 | 0 | 1`
- `sort(values: readonly string[], locale): string[]` — returns a NEW sorted array
### `clearCollatorCache(): void`
Test-only helper that empties the per-(locale, options-hash) cache.
## Implementation Notes
- The cache key is `${locale}|${JSON.stringify(options)}`. JSON
serialisation is stable for the option shape — every field is a
string, boolean, or undefined.
- `compareStrings` normalises the raw `Intl.Collator.compare` return
value (any signed number) to the `-1 | 0 | 1` triple so callers can
use it directly as an `Array.sort` callback or as a switch
discriminant.
- `formatCollation` uses `[...values].sort(collator.compare)` — the
spread copies the input, then `Array.sort` mutates the copy in
place. Result is referentially distinct from the input.
- `compareStrings` and `formatCollation` BOTH go through the same
cache. A single collator can serve both compare and sort patterns.
- `Intl.Collator` is supported in Node 14+ and every current browser.
The package does not provide a polyfill — Node 14 is below the
family's documented support floor anyway.

View File

@@ -0,0 +1,107 @@
# Collator ergonomic helpers
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The three ergonomic helpers (`compareCaseInsensitive`,
`compareNumericAware`, `sortStrings`) pre-bind `compareStrings` and
`formatCollation` with the most common option configurations so
consumers don't have to remember which `sensitivity` value matches
case-insensitive comparison or which boolean flag enables natural
sort.
## Features
- `compareCaseInsensitive(a, b, locale)` — pre-binds `sensitivity: "accent"` (case-insensitive but accent-aware)
- `compareNumericAware(a, b, locale)` — pre-binds `numeric: true` (natural sort: `"file2" < "file10"`)
- `sortStrings(values, locale, options?)` — alias for `formatCollation` with a more familiar name
## Usage: Basic Example
```ts
import {
compareCaseInsensitive,
compareNumericAware,
sortStrings,
} from "@suaveplan/i18n-formatter-collator";
compareCaseInsensitive("apple", "Apple", "en-GB"); // 0
compareNumericAware("file2", "file10", "en-GB"); // -1
sortStrings(["c", "a", "b"], "en-GB"); // ["a", "b", "c"]
sortStrings(["file10", "file2"], "en-GB", { numeric: true });
// ["file2", "file10"]
```
## Usage: Advanced Example
```ts
import {
compareCaseInsensitive,
compareNumericAware,
sortStrings,
} from "@suaveplan/i18n-formatter-collator";
// Build a case-insensitive search filter.
function findItems(needle: string, items: string[], locale: string) {
return items.filter(
(item) => compareCaseInsensitive(needle, item, locale) === 0
);
}
// Sort a list of file names naturally.
const files = ["log10.txt", "log2.txt", "log1.txt"];
const sorted = sortStrings(files, "en-GB", { numeric: true });
// ["log1.txt", "log2.txt", "log10.txt"]
// Sort then group names case-insensitively.
function groupBy<T>(
items: readonly T[],
keyFn: (item: T) => string,
locale: string
): Record<string, T[]> {
const sortedItems = [...items].sort((x, y) =>
compareCaseInsensitive(keyFn(x), keyFn(y), locale)
);
const out: Record<string, T[]> = {};
for (const item of sortedItems) {
const key = keyFn(item);
const bucket = out[key] ?? [];
bucket.push(item);
out[key] = bucket;
}
return out;
}
```
## API Reference
### `compareCaseInsensitive(a, b, locale): -1 | 0 | 1`
Pre-binds `sensitivity: "accent"`. Treats `"a"` and `"A"` as equal
(case-insensitive) but distinguishes `"a"` from `"á"`
(accent-sensitive). For full base-letter comparison (treating `"a"`
and `"á"` equal as well), call `compareStrings` directly with
`sensitivity: "base"`.
### `compareNumericAware(a, b, locale): -1 | 0 | 1`
Pre-binds `numeric: true`. Embedded numbers sort by their numeric
value, not their lexicographic order: `"file2"` sorts before
`"file10"` (lexicographic would put `"file10"` first).
### `sortStrings(values, locale, options?): string[]`
Alias for `formatCollation` with a familiar name. Returns a NEW
sorted array; the input is never mutated. All options forwarded.
## Implementation Notes
- Each helper is a one-line wrapper around `compareStrings` /
`formatCollation`. The underlying per-(locale, options-hash) cache
applies — calling `compareCaseInsensitive` twice with the same
locale reuses one `Intl.Collator`.
- The helpers are pure functions with no shared state.

View File

@@ -0,0 +1,100 @@
# Collator observability
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
The Collator formatter exports two OpenTelemetry instruments and a
small in-memory state ring so spec-conformance tests can assert exact
attribute payloads without standing up a full SDK exporter. The
instrument names match the canonical Wave-3 formatter contract —
every sibling formatter emits the same two metric names so dashboards
and alerts compose cleanly across the family.
## Features
- Histogram `i18n.format.collator.duration_ms` (unit `ms`) — per-compare/-sort wall-clock time
- Counter `i18n.format.cache_hit{formatter="collator"}` — increments on cache reuse only
- In-memory state ring (`getCollatorMetricsState()`) for spec-conformance
- `resetCollatorMetricsState()` test-only helper
## Usage: Basic Example
```ts
import { metrics } from "@opentelemetry/api";
import { MeterProvider } from "@opentelemetry/sdk-metrics";
const provider = new MeterProvider({ readers: [/* reader */] });
metrics.setGlobalMeterProvider(provider);
import { compareStrings } from "@suaveplan/i18n-formatter-collator";
compareStrings("a", "b", "en-GB");
// emits via your reader
```
## Usage: Advanced Example
```ts
import { describe, expect, it } from "@suaveplan/testing/runner";
import {
clearCollatorCache,
compareStrings,
} from "@suaveplan/i18n-formatter-collator";
import {
getCollatorMetricsState,
resetCollatorMetricsState,
} from "@suaveplan/i18n-formatter-collator";
it("emits histogram + cache-hit counter", () => {
clearCollatorCache();
resetCollatorMetricsState();
compareStrings("a", "b", "en-GB", { sensitivity: "base", usage: "sort" });
compareStrings("a", "b", "en-GB", { sensitivity: "base", usage: "sort" });
const state = getCollatorMetricsState();
expect(state.durationSamples).toHaveLength(2);
expect(state.cacheHitSamples).toHaveLength(1);
expect(state.durationSamples[0]?.attributes).toMatchObject({
locale: "en-GB",
usage: "sort",
sensitivity: "base",
});
});
```
## API Reference
### `recordCollatorFormat(locale, usage, sensitivity, durationMs): void`
Records the duration histogram sample. Tags: `locale`, `usage`,
`sensitivity`. Called internally by `compareStrings` /
`formatCollation`.
### `recordCollatorCacheHit(): void`
Increments the cache-hit counter with the constant tag
`{ formatter: "collator" }`.
### `getCollatorMetricsState(): CollatorMetricsState`
Returns the in-memory ring of `durationSamples` and
`cacheHitSamples`. Used by spec-conformance tests.
### `resetCollatorMetricsState(): void`
Clears both ring buffers. Test-only helper.
## Implementation Notes
- The OTel meter is named `suaveplan.i18n.formatter.collator`. Every
formatter in the family has its own meter so OTel resource
attributes can be tied per-package if a consumer chooses.
- Defaults supplied to the histogram tag when caller omits options:
`usage = "sort"`, `sensitivity = "variant"` (matching `Intl.Collator`
defaults). Every sample carries non-null values for all three tags.
- Sample dashboard query (PromQL/Grafana over OTLP):
`histogram_quantile(0.99, sum(rate(i18n_format_collator_duration_ms_bucket[5m])) by (le, locale, usage))`
- The in-memory ring is intentionally unbounded — tests are short and
call `resetCollatorMetricsState()` between cases. A long-running
test loop should call reset to avoid memory growth.