Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
# `I18nMiddleware`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Normalises locale-related metadata across the chain. Injects an `x-locale` header, sets `x-direction: rtl` for right-to-left scripts, and (when configured with a clock) rewrites the envelope's timestamp to a single reference frame so peers in different timezones do not produce comparison-resistant timestamps.
## Features
- BCP-47 locale tag carried in the `x-locale` header so receivers can switch translations / formatters without inspecting the payload.
- RTL detection via configurable locale-prefix list (defaults to `ar`, `he`, `fa`, `ur` — covers the bulk of right-to-left scripts).
- Optional UTC normalisation: callers pass a `clock` (typically `Date.now`) and the middleware rewrites `envelope.timestamp` so cross-host comparisons stay reliable.
- Lives in the NORMAL band by default (`priority: 280`) so locale headers are present before transformation / metrics observe them.
## Usage: Basic Example
```ts
import {
I18nMiddleware,
MiddlewareManager,
} from "@suaveplan/network-plugin-middleware";
const manager = new MiddlewareManager();
manager.register(new I18nMiddleware({ locale: "ar-EG" }));
const result = await manager.dispatch(envelope, "outgoing");
result.envelope?.headers["x-locale"]; // "ar-EG"
result.envelope?.headers["x-direction"]; // "rtl"
```
## Usage: Advanced Example
```ts
import { I18nMiddleware } from "@suaveplan/network-plugin-middleware";
let now = 1_700_000_000_000;
const i18n = new I18nMiddleware({
id: "i18n-utc",
priority: 290,
locale: "en-US",
rtlLocales: ["ar", "he", "fa", "ur", "yi"],
clock: () => now,
});
```
## API Reference
- `class I18nMiddleware implements Middleware`
- `constructor(config?: I18nMiddlewareConfig)`.
- `apply(envelope, context): MessageEnvelope` — never drops.
- `interface I18nMiddlewareConfig` — `{ id?, priority?, direction?, locale?, rtlLocales?, clock? }`.
## Implementation Notes
- RTL detection compares the lower-cased locale tag against each prefix. A locale matches the prefix exactly (`"ar"`) or hyphen-bounded (`"ar-EG"`); `"argonaut"` does NOT match the `ar` prefix because the comparator only allows the empty suffix or a `-` separator.
- The middleware always returns a new envelope object; even when it leaves the headers untouched, callers can rely on identity-equality being a useful "did i18n run?" signal.
- The clock rewrite is opt-in: applications that already enforce monotonic timestamps elsewhere (e.g. via `network-plugin-protocol`) should leave it unset to avoid double-normalisation.
- `x-locale` is the conventional WebSocket extension header, deliberately reused so subscribers (logger, metrics) do not need to learn a new key per protocol surface.

View File

@@ -0,0 +1,71 @@
import type { MessageEnvelope } from "@suaveplan/network";
import type {
Middleware,
MiddlewareContext,
} from "../types/middleware.types.js";
export interface I18nMiddlewareConfig {
readonly id?: string;
readonly priority?: number;
readonly direction?: MiddlewareContext["direction"];
/** BCP-47 locale tag (default `"en"`). */
readonly locale?: string;
/** Set of locale prefixes considered RTL (default `["ar", "he", "fa", "ur"]`). */
readonly rtlLocales?: ReadonlyArray<string>;
/** Optional clock — when set, the middleware rewrites `envelope.timestamp` to `clock()`. */
readonly clock?: () => number;
}
const DEFAULT_RTL = Object.freeze(["ar", "he", "fa", "ur"]);
/**
* Locale-aware envelope normalisation: injects `x-locale` and (when
* applicable) `x-direction: rtl` headers, and optionally normalises
* `envelope.timestamp` to a caller-supplied clock so timestamps from
* peers in other timezones land in a single reference frame.
*/
export class I18nMiddleware implements Middleware {
public readonly id: string;
public readonly priority: number;
public readonly direction: MiddlewareContext["direction"] | undefined;
public enabled = true;
private readonly locale: string;
private readonly rtlPrefixes: ReadonlyArray<string>;
private readonly clock: (() => number) | undefined;
public constructor(config: I18nMiddlewareConfig = {}) {
this.id = config.id ?? "i18n";
this.priority = config.priority ?? 280;
this.direction = config.direction;
this.locale = config.locale ?? "en";
this.rtlPrefixes = config.rtlLocales ?? DEFAULT_RTL;
this.clock = config.clock;
}
public apply(
envelope: MessageEnvelope,
_context: MiddlewareContext
): MessageEnvelope {
const headers = { ...envelope.headers };
headers["x-locale"] = this.locale;
if (this.isRtl(this.locale)) {
headers["x-direction"] = "rtl";
}
const next: MessageEnvelope = {
...envelope,
headers,
};
if (this.clock !== undefined) {
return { ...next, timestamp: this.clock() };
}
return next;
}
private isRtl(locale: string): boolean {
const lower = locale.toLowerCase();
for (const prefix of this.rtlPrefixes) {
if (lower === prefix || lower.startsWith(`${prefix}-`)) return true;
}
return false;
}
}

View File

@@ -0,0 +1,56 @@
import { createEnvelope } from "@suaveplan/network";
import { describe, expect, it } from "@suaveplan/testing/runner";
import { I18nMiddleware } from "./i18n-middleware.js";
const env = () =>
createEnvelope({ type: "i18n:test", senderId: "peer-1", payload: {} });
const ctx = {
direction: "outgoing" as const,
state: {} as Record<string, unknown>,
};
describe("I18nMiddleware: locale headers, RTL, UTC normalisation", () => {
it("injects x-locale header (default en, no x-direction)", () => {
const i = new I18nMiddleware();
const result = i.apply(env(), ctx);
expect(result.headers["x-locale"]).toBe("en");
expect(result.headers["x-direction"]).toBeUndefined();
});
it("emits x-direction: rtl for RTL locales", () => {
const i = new I18nMiddleware({ locale: "ar-EG" });
const result = i.apply(env(), ctx);
expect(result.headers["x-direction"]).toBe("rtl");
});
it("matches RTL prefix exactly (no false positives)", () => {
const i = new I18nMiddleware({ locale: "argonaut" });
const result = i.apply(env(), ctx);
expect(result.headers["x-direction"]).toBeUndefined();
});
it("matches when locale equals an RTL prefix exactly", () => {
const i = new I18nMiddleware({ locale: "he" });
const result = i.apply(env(), ctx);
expect(result.headers["x-direction"]).toBe("rtl");
});
it("rewrites timestamp when clock is provided", () => {
const now = 1_700_000_000_000;
const i = new I18nMiddleware({ clock: () => now });
const e = env();
const before = e.timestamp;
const result = i.apply(e, ctx);
expect(result.timestamp).toBe(now);
expect(result.timestamp).not.toBe(before);
});
it("custom rtlLocales overrides defaults", () => {
const i = new I18nMiddleware({
locale: "yi",
rtlLocales: ["yi"],
});
expect(i.apply(env(), ctx).headers["x-direction"]).toBe("rtl");
});
});