first commit
This commit is contained in:
104
docs/localization/i18n/i18n-formatter-collator/README.md
Normal file
104
docs/localization/i18n/i18n-formatter-collator/README.md
Normal 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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user