@ultimat3/i18n 24.0.0 → 25.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -14,6 +14,7 @@ Imported by every package that renders a string.
14
14
  | `locales.ts` | supported set, normalize, negotiate, RTL. No catalogs. |
15
15
  | `context.ts` | request locale via ALS, the registry + the base layer under it, the ambient `t` and `useI18n`. |
16
16
  | `framework.ts` | the framework's own strings, installed as the base layer at module scope. |
17
+ | `app-catalogs.ts` | an app's declared locales, read by importing its catalog module — `loadAppCatalogs` (`undefined` when undeclared) and `appLocaleSet` (`UNDECLARED_LOCALES` then). The ONE reader: a CLI step never reads `localeConfig()`/`routedLocales()` after `loadApp`; it reads `LoadedApp.locales`. Tooling only: the `./app-catalogs` entry, never `index.ts` (it is in every island). |
17
18
  | `registration.ts` | are the catalogs an app SHIPS in the registry it READS? No file access. |
18
19
  | `extract.ts` | static scan + audit for `x verify`. |
19
20
  | `catalogs/en.json` | data, not code. |
@@ -35,7 +36,7 @@ Imported by every package that renders a string.
35
36
  puts it back. It was `registerFrameworkCatalog()`, whose ONE caller was `defineCatalogs`, so an
36
37
  app whose catalog module nothing imported served `⟦errors.notFound.title⟧` on its 404 page and
37
38
  `⟦ui.*⟧` in every design-system control, with `x verify` green (issue #249). Under
38
- `FRAMEWORK_CATALOG_LOCALE` (`en`) **only**: it ran per locale until 2026-08, so an `es`-only app
39
+ core's `DEFAULT_LOCALE` (`en`) **only**: it ran per locale until 2026-08, so an `es`-only app
39
40
  served `Page not found` with `isMiss` reading FALSE — the fallback chain the line above refuses,
40
41
  wearing registration as a disguise. A non-`en` app translates the framework keys it renders into
41
42
  its own catalog.
@@ -105,7 +106,7 @@ Imported by every package that renders a string.
105
106
  `@ultimat3/mail` take a bare `Translator`. `has()` and `raw()` stay `string` — they are probes.
106
107
  - Nothing here formats a number, date or money. That is `@ultimat3/money` / `@ultimat3/time`.
107
108
  - **The ambient locale IS `Ctx.locale`**, core's own declared field — this package writes no context
108
- field of its own and publishes no writer. `createContext({ locale })` and
109
+ field of its own and publishes no writer. `ctxOf({ locale })` and
109
110
  `withChildContext({ locale })` are the only ways in, `currentLocale()` the only way out.
110
111
  `attachLocale`/`localeOf` existed until 1.3.0 with zero callers; `@ultimat3/time` had the same
111
112
  pair over a field name (`ctx['timeZone']`) that disagreed with core's `tz`, which is what made
package/README.md CHANGED
@@ -99,6 +99,22 @@ catalog is a fallback chain, it reads as `isMiss === false`, and `assertCatalogs
99
99
  see it because `CatalogSet.catalogs` carries app strings only. Translate the framework keys your
100
100
  app renders into your own catalog — that is the one path, and it is the same merge an override is.
101
101
 
102
+ ## Reading an app's declared locales (tooling)
103
+
104
+ `@ultimat3/i18n/app-catalogs` — server-only, its own entry so no island bundle carries it:
105
+
106
+ | Export | What |
107
+ |---|---|
108
+ | `APP_CATALOGS_PATH` | `'packages/i18n/src/index.ts'`, where `x new` writes the app's `defineCatalogs()` |
109
+ | `loadAppCatalogs(root)` | imports that module and answers `{ locales, defaultLocale }` (default first), or `undefined` when no `defineCatalogs()` was declared — never the ambient `localeConfig()` read blind. An exported set answers for itself, whatever the locale config says by then. A module that throws rejects with its own error |
110
+ | `appLocaleSet(root)` | the same answer, with `UNDECLARED_LOCALES` for an app that declared nothing — what every consumer that needs *a* locale asks |
111
+ | `UNDECLARED_LOCALES` | `{ locales: ['en'], defaultLocale: 'en' }`, frozen: the one answer for an app with no catalogs, so the PWA manifest, the service worker, the prerender and every `x` command agree |
112
+ | `AppLocaleSet` | the type of both answers |
113
+
114
+ The one reader the CLI (`loadApp`'s `locales`, the pwa manifest and service worker, the static export, `x shot`, `x g`, `x i18n`) and `@ultimat3/testing`'s e2e preload share. `@ultimat3/testing` asks `loadAppCatalogs` deliberately: an app that declared nothing pins no browser language rather than a guessed one.
115
+
116
+ `DEFAULT_LOCALE`, `directionOf` and `isRtl` are `@ultimat3/core`'s since 25.0.0 — import them from there. The barrel still re-exports the three until `@ultimat3/mail` and `@ultimat3/ui` move (marked `PENDING` in `src/index.ts`); `Direction` stays a type re-export, because `LocaleResolution.direction` names it.
117
+
102
118
  ## Enforcement
103
119
 
104
120
  `extractKeys()` scans source for `t('...')` calls; `auditCatalogs()` reports keys used,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/i18n",
3
- "version": "24.0.0",
3
+ "version": "25.0.0",
4
4
  "description": "Dependency-free translator, catalog flattening, locale negotiation and loud missing-key rendering",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -18,7 +18,8 @@
18
18
  "provenance": true
19
19
  },
20
20
  "exports": {
21
- ".": "./src/index.ts"
21
+ ".": "./src/index.ts",
22
+ "./app-catalogs": "./src/app-catalogs.ts"
22
23
  },
23
24
  "files": [
24
25
  "src",
@@ -29,13 +30,13 @@
29
30
  "LICENSE"
30
31
  ],
31
32
  "engines": {
32
- "bun": ">=1.4.0"
33
+ "bun": ">=1.4.2"
33
34
  },
34
35
  "scripts": {
35
36
  "typecheck": "tsc --noEmit -p tsconfig.json",
36
37
  "test": "bun test"
37
38
  },
38
39
  "dependencies": {
39
- "@ultimat3/core": "24.0.0"
40
+ "@ultimat3/core": "25.0.0"
40
41
  }
41
42
  }
@@ -0,0 +1,77 @@
1
+ // The ONE reader of an app's declared locales: its catalog module's `defineCatalogs()`, imported
2
+ // and read back. The CLI (manifest, worker, prerender, `x shot`, `x g`, `x i18n`) and the e2e
3
+ // preload each kept a copy, and the copies disagreed. Server-only, so its own entry
4
+ // (`@ultimat3/i18n/app-catalogs`): `index.ts` is in every island.
5
+
6
+ // why: Bun exposes no path-join primitive; the catalog module is joined to the app root.
7
+ import { join } from 'node:path';
8
+ import { DEFAULT_LOCALE } from '@ultimat3/core';
9
+ import { localeConfig, routedLocales } from './context';
10
+ import type { CatalogSet, CatalogSources } from './define-catalogs';
11
+ import { catalogDeclarationCount, isDeclaredCatalogSet } from './define-catalogs';
12
+
13
+ /** Where an app declares its catalogs — `x new` writes it, and its `defineCatalogs()` runs there. */
14
+ export const APP_CATALOGS_PATH = 'packages/i18n/src/index.ts';
15
+
16
+ export interface AppLocaleSet {
17
+ /** Every locale the app routes, its default first. */
18
+ readonly locales: readonly string[];
19
+ readonly defaultLocale: string;
20
+ }
21
+
22
+ /**
23
+ * `root`'s declared locales, or `undefined` when no declaration ran: no catalog module, or one
24
+ * that calls no `defineCatalogs()`. Never `localeConfig()` read blind — it ALWAYS answers a
25
+ * fallback, so a module declaring nothing would inherit whatever an earlier import declared, and a
26
+ * caller would pin a language no visitor sees. Declared means this import ran the call, or the
27
+ * module exports the set one returned (a cached module evaluates once: the boot scan imported it).
28
+ *
29
+ * A module that will not import throws its own error: each caller decides whether that is a
30
+ * refusal or "nothing declared".
31
+ */
32
+ export async function loadAppCatalogs(root: string): Promise<AppLocaleSet | undefined> {
33
+ const path = join(root, APP_CATALOGS_PATH);
34
+ if (!(await Bun.file(path).exists())) return undefined;
35
+ const before = catalogDeclarationCount();
36
+ const module = (await import(path)) as Readonly<Record<string, unknown>>;
37
+ const ran = catalogDeclarationCount() !== before;
38
+ // The exported set FIRST: it is the declaration itself, where the locale config is shared,
39
+ // process-wide state a later `configureLocales()` or a reset may have moved since the module ran
40
+ // — and a cached module never runs its `defineCatalogs()` again to put it back.
41
+ const exported = Object.values(module).find(isDeclaredCatalogSet) as DeclaredSet | undefined;
42
+ if (exported !== undefined) return setOf(exported.default, exported.locales);
43
+ if (!ran) return undefined;
44
+ // Declared by this very import and not exported: the config is what that call just wrote.
45
+ const defaultLocale = localeConfig().fallback;
46
+ if (defaultLocale === '') return undefined;
47
+ return { locales: [...routedLocales()], defaultLocale };
48
+ }
49
+
50
+ /** What `isDeclaredCatalogSet` vouches for: a `CatalogSet` some `defineCatalogs()` returned. */
51
+ type DeclaredSet = Pick<CatalogSet<CatalogSources>, 'default' | 'locales'>;
52
+
53
+ /** Default first, as `routedLocales()` orders them — every consumer enumerates in this order. */
54
+ const setOf = (defaultLocale: string, locales: readonly string[]): AppLocaleSet => ({
55
+ locales: [defaultLocale, ...locales.filter((locale) => locale !== defaultLocale)],
56
+ defaultLocale,
57
+ });
58
+
59
+ /**
60
+ * The answer for an app that declares no catalogs: the framework default, alone. ONE value, so the
61
+ * PWA manifest, the service worker, the prerender and every `x` command name the same language for
62
+ * such an app — before it, the manifest said `en` off its own literal while the worker and the
63
+ * prerender read `localeConfig()` blind and could inherit an earlier import's declaration.
64
+ */
65
+ export const UNDECLARED_LOCALES: AppLocaleSet = Object.freeze({
66
+ locales: Object.freeze([DEFAULT_LOCALE]),
67
+ defaultLocale: DEFAULT_LOCALE,
68
+ });
69
+
70
+ /**
71
+ * `root`'s locales whether it declared any or not: `loadAppCatalogs`, else `UNDECLARED_LOCALES`.
72
+ * The caller that must tell the two apart (`x g`, which writes no catalog for an app without one)
73
+ * asks `loadAppCatalogs`; every other caller asks this. A module that will not import still throws.
74
+ */
75
+ export async function appLocaleSet(root: string): Promise<AppLocaleSet> {
76
+ return (await loadAppCatalogs(root)) ?? UNDECLARED_LOCALES;
77
+ }
@@ -65,7 +65,9 @@
65
65
  "action": "Try again"
66
66
  },
67
67
  "page": {
68
- "builtWith": "Built with Ultimate"
68
+ "builtWith": "Built with Ultimate",
69
+ "code": "code",
70
+ "request": "request"
69
71
  }
70
72
  },
71
73
  "validation": {
@@ -183,7 +185,18 @@
183
185
  "name": "Admin"
184
186
  },
185
187
  "dashboard": {
186
- "title": "Dashboard"
188
+ "title": "Dashboard",
189
+ "records": {
190
+ "title": "Records",
191
+ "label": "Records per resource",
192
+ "resource": "Resource",
193
+ "total": "records in all"
194
+ },
195
+ "access": {
196
+ "title": "Access",
197
+ "allowed": "{allowed} of {total} allowed",
198
+ "open": "Open the list"
199
+ }
187
200
  },
188
201
  "denied": {
189
202
  "body": "This account is missing {permission} ({reason})."
@@ -487,7 +500,27 @@
487
500
  },
488
501
  "timeline": {
489
502
  "title": "Timeline",
490
- "question": "where did the time go in this request?"
503
+ "question": "where did the time go in this request?",
504
+ "requests": "Recent requests",
505
+ "spans": "Spans, on the request's own clock",
506
+ "byKind": "Time by kind",
507
+ "ms": "{ms} ms",
508
+ "empty": "No request has been traced yet — open a page of the app, then reload this tab.",
509
+ "kind": {
510
+ "http": "http",
511
+ "render": "render",
512
+ "action": "action",
513
+ "policy": "policy",
514
+ "sql": "sql",
515
+ "cache": "cache",
516
+ "job": "job"
517
+ },
518
+ "nPlusOne": {
519
+ "title": "Repeated statements",
520
+ "fix": "fix:",
521
+ "clean": "no statement repeated past the detector's threshold in this request",
522
+ "unwired": "no N+1 detector is attached to this process — x dev wires one"
523
+ }
491
524
  },
492
525
  "live": {
493
526
  "title": "Live",
@@ -495,7 +528,19 @@
495
528
  },
496
529
  "jobs": {
497
530
  "title": "Jobs",
498
- "question": "is the queue moving, and which step failed?"
531
+ "question": "is the queue moving, and which step failed?",
532
+ "chart": {
533
+ "label": "Recent runs by state",
534
+ "total": "recent runs",
535
+ "state": "State",
536
+ "runs": "Runs"
537
+ },
538
+ "state": {
539
+ "ok": "done",
540
+ "running": "running",
541
+ "failed": "failed",
542
+ "dead": "dead"
543
+ }
499
544
  },
500
545
  "db": {
501
546
  "title": "DB",
package/src/context.ts CHANGED
@@ -7,20 +7,15 @@ import {
7
7
  assertLocale,
8
8
  cachedFormatter,
9
9
  canonicalLocale,
10
+ DEFAULT_LOCALE,
11
+ type Direction,
12
+ directionOf,
10
13
  readCookie,
11
14
  tryUseContext,
12
15
  } from '@ultimat3/core';
13
16
  import { type Catalog, mergeCatalogs } from './catalog';
14
- import {
15
- DEFAULT_LOCALE,
16
- type Direction,
17
- directionOf,
18
- type Locale,
19
- negotiateLocale,
20
- normalizeLocale,
21
- SUPPORTED_LOCALES,
22
- } from './locales';
23
- import { createTranslator, type TranslateVars, type Translator } from './translator';
17
+ import { type Locale, negotiateLocale, normalizeLocale, SUPPORTED_LOCALES } from './locales';
18
+ import { catalogTranslator, type TranslateVars, type Translator } from './translator';
24
19
 
25
20
  /** The cookie an explicit language switcher writes. */
26
21
  export const LOCALE_COOKIE = 'x_locale';
@@ -162,7 +157,7 @@ export function localeCookieOf(cookieHeader?: string | null): string | undefined
162
157
  /**
163
158
  * Ambient locale for the in-flight request; the configured fallback outside one.
164
159
  *
165
- * The store is **`Ctx.locale`**, core's own declared field, so `createContext({ locale })` and
160
+ * The store is **`Ctx.locale`**, core's own declared field, so `ctxOf({ locale })` and
166
161
  * `withChildContext({ locale })` are the only writers and this package publishes no second one.
167
162
  * `@ultimat3/time`'s `currentTimeZone()` is the same shape over `Ctx.tz`, deliberately.
168
163
  */
@@ -298,10 +293,10 @@ export function catalogFor(locale: Locale): Catalog {
298
293
  export function translatorFor<TCatalog = Catalog>(locale: Locale): Translator<TCatalog> {
299
294
  const key = translatorKey(locale);
300
295
  // Resolved BEFORE the memo, so what lands under `key` is the catalog that key stands for and
301
- // not whichever spelling asked first. The caller's own tag reaches `createTranslator` either
296
+ // not whichever spelling asked first. The caller's own tag reaches `catalogTranslator` either
302
297
  // way — the catalog is chosen by locale, the formatting by what the caller actually sent.
303
298
  const translator = cachedFormatter(translators, key, () =>
304
- createTranslator(catalogFor(registeredUnder(locale, key) ?? locale), locale),
299
+ catalogTranslator(catalogFor(registeredUnder(locale, key) ?? locale), locale),
305
300
  );
306
301
  // One cast, one place. `TCatalog` narrows the key parameter and nothing else, so the memoized
307
302
  // object already is the right value — the registry cannot be keyed by an app's catalog type.
@@ -38,6 +38,18 @@ let declarations = 0;
38
38
  */
39
39
  export const catalogDeclarationCount = (): number => declarations;
40
40
 
41
+ /**
42
+ * Every set `defineCatalogs()` returned in this process — held weakly, so a set nothing references
43
+ * is never kept alive by being asked about. `app-catalogs.ts` asks it of a module's exports: a
44
+ * module evaluates once, so on its second `import()` the count above has not moved, and the set it
45
+ * exports is the one fact left that it declared.
46
+ */
47
+ const declaredSets = new WeakSet<object>();
48
+
49
+ /** Whether `value` is a set some `defineCatalogs()` call returned — never a lookalike object. */
50
+ export const isDeclaredCatalogSet = (value: unknown): boolean =>
51
+ typeof value === 'object' && value !== null && declaredSets.has(value);
52
+
41
53
  export function defineCatalogs<TLocales extends CatalogSources>(
42
54
  input: DefineCatalogsInput<TLocales>,
43
55
  ): CatalogSet<TLocales> {
@@ -62,10 +74,12 @@ export function defineCatalogs<TLocales extends CatalogSources>(
62
74
  declarations += 1;
63
75
 
64
76
  const catalogs = Object.fromEntries(loaded) as Readonly<Record<keyof TLocales & string, Catalog>>;
65
- return {
77
+ const set: CatalogSet<TLocales> = {
66
78
  default: input.default,
67
79
  locales,
68
80
  catalogs,
69
81
  keys: () => catalogKeys(mergeCatalogs(...loaded.map(([, catalog]) => catalog))),
70
82
  };
83
+ declaredSets.add(set);
84
+ return set;
71
85
  }
package/src/framework.ts CHANGED
@@ -3,10 +3,10 @@
3
3
  * Under every app catalog, so an app overrides any framework key and forgets none.
4
4
  */
5
5
 
6
+ import { DEFAULT_LOCALE } from '@ultimat3/core';
6
7
  import { type Catalog, loadCatalog } from './catalog';
7
8
  import en from './catalogs/en.json';
8
9
  import { registerBaseCatalog } from './context';
9
- import { DEFAULT_LOCALE, type Locale } from './locales';
10
10
 
11
11
  /**
12
12
  * `errors.*`, `auth.*`, `pagination.*`, `admin.*`, `validation.*`, `common.*`, `time.*`, `ui.*`.
@@ -17,20 +17,10 @@ import { DEFAULT_LOCALE, type Locale } from './locales';
17
17
  */
18
18
  export const FRAMEWORK_CATALOG: Catalog = loadCatalog(en);
19
19
 
20
- /**
21
- * The ONE locale this catalog is written in.
22
- *
23
- * Registering it under every locale was a fallback chain with no name: an app shipping only `es`
24
- * served `Page not found` and English `ui.*` chrome with `isMiss` reading FALSE, so nothing
25
- * downstream could see the gap — the one thing `⟦key⟧` exists to make impossible. A non-`en` app
26
- * translates the framework keys it renders into its own catalog.
27
- */
28
- export const FRAMEWORK_CATALOG_LOCALE: Locale = DEFAULT_LOCALE;
29
-
30
20
  // At module scope, and that is the whole point: `index.ts` re-exports this file, so anything that
31
21
  // can call `t()` has already imported the module that installs these strings. It was a function
32
22
  // (`registerFrameworkCatalog`) that only `defineCatalogs` called until 5.1.0 — so an app whose
33
23
  // catalog module nothing imported rendered `⟦errors.notFound.title⟧` on its 404 page, `⟦ui.*⟧` in
34
24
  // every design-system control, and had no way to notice (issue #249). Registration that an app
35
25
  // can forget is not registration; the framework's half of it is now unforgettable by construction.
36
- registerBaseCatalog(FRAMEWORK_CATALOG_LOCALE, FRAMEWORK_CATALOG);
26
+ registerBaseCatalog(DEFAULT_LOCALE, FRAMEWORK_CATALOG);
package/src/index.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  /** Public surface of @ultimat3/i18n. Explicit exports only. */
2
2
 
3
- // Anchored on purpose, and NOT by the `sideEffects` array: Bun reads any array as `false` and drops
4
- // the module regardless (oven-sh/bun#40650). `registerBaseCatalog` here fills the catalog `t()`
3
+ // Anchored on purpose, and not by the `sideEffects` array alone: Bun before 1.4.1 read any array as
4
+ // `false` and dropped the module regardless (oven-sh/bun#40650), and a bare import holds on every
5
+ // bundler. The array still lists it, because a bundler that honours the array drops a bare import
6
+ // of a module it does not list. `registerBaseCatalog` here fills the catalog `t()`
5
7
  // falls back to, and `t()` never imports this module — so without the bare import every framework
6
8
  // string renders as its ⟦key⟧ placeholder. `SIDE_EFFECTS_ANCHORS` carries the argument and
7
9
  // `bun run side-effects` enforces it.
@@ -73,7 +75,7 @@ export {
73
75
  type LocaleAudit,
74
76
  mergeExtractions,
75
77
  } from './extract';
76
- export { FRAMEWORK_CATALOG, FRAMEWORK_CATALOG_LOCALE } from './framework';
78
+ export { FRAMEWORK_CATALOG } from './framework';
77
79
  export {
78
80
  type InterpolationValue,
79
81
  type InterpolationVars,
@@ -87,17 +89,13 @@ export {
87
89
  } from './interpolate';
88
90
  export {
89
91
  type LocalePrefix,
90
- localeSegment,
91
92
  localizedPath,
92
93
  splitLocalePrefix,
93
94
  unlocalizedPath,
94
95
  } from './locale-path';
95
96
  export {
96
97
  assertSupportedLocale,
97
- DEFAULT_LOCALE,
98
98
  type Direction,
99
- directionOf,
100
- isRtl,
101
99
  isSupportedLocale,
102
100
  type LanguageRange,
103
101
  type Locale,
@@ -112,7 +110,7 @@ export {
112
110
  catalogRegistrationGaps,
113
111
  } from './registration';
114
112
  export {
115
- createTranslator,
113
+ catalogTranslator,
116
114
  isMiss,
117
115
  type TranslateVars,
118
116
  type TranslationKey,
@@ -6,11 +6,10 @@
6
6
  * `meta` and the prerender writes files from one answer.
7
7
  */
8
8
 
9
- import { type LocalePathSplit, localeSegment, localizePath, splitLocalePath } from '@ultimat3/core';
9
+ import { type LocalePathSplit, localizePath, splitLocalePath } from '@ultimat3/core';
10
10
  import { localeConfig, routedLocales } from './context';
11
11
  import type { Locale } from './locales';
12
12
 
13
- export { localeSegment };
14
13
  export type LocalePrefix = LocalePathSplit;
15
14
 
16
15
  /** `/en/precios` → `{ locale: 'en', path: '/precios', isDefault: false }`, or `undefined`. */
package/src/locales.ts CHANGED
@@ -3,13 +3,12 @@
3
3
  * HTTP `Accept-Language` negotiation. Knows nothing about catalogs or interpolation.
4
4
  */
5
5
 
6
+ import { DEFAULT_LOCALE } from '@ultimat3/core';
6
7
  import { localeUnsupported } from './errors';
7
8
 
8
9
  /** A normalized BCP-47 tag — primary subtag, lowercase (`pt`), or `lang-script` (`zh-hant`). */
9
10
  export type Locale = string;
10
11
 
11
- export const DEFAULT_LOCALE: Locale = 'en';
12
-
13
12
  /** Locales the framework's own catalogs may ship for. Apps narrow or extend this set. */
14
13
  export const SUPPORTED_LOCALES: readonly Locale[] = [
15
14
  'en',
@@ -44,10 +43,11 @@ export const SUPPORTED_LOCALES: readonly Locale[] = [
44
43
  'zh',
45
44
  ];
46
45
 
47
- // Direction moved to `@ultimat3/core` (`locale-direction.ts`) so a browser chunk that needs only
48
- // `directionOf` does not reach this barrel and the framework catalog it installs (issue #490).
49
- // Re-exported under the same names: every caller of this package is unchanged.
50
- export { type Direction, directionOf, isRtl } from '@ultimat3/core';
46
+ // Direction is `@ultimat3/core`'s (`locale-direction.ts`), so a browser chunk that needs only
47
+ // `directionOf` does not reach this barrel and the framework catalog it installs (issue #490). The
48
+ // TYPE is re-exported because `LocaleResolution.direction` names it; the functions are not — their
49
+ // one import path is `@ultimat3/core`.
50
+ export type { Direction } from '@ultimat3/core';
51
51
 
52
52
  /**
53
53
  * The registered spelling of `lower`, compared case-insensitively. A registered `pt-BR` used to
package/src/translator.ts CHANGED
@@ -4,9 +4,10 @@
4
4
  * visible in dev and in review screenshots instead of invisible in production.
5
5
  */
6
6
 
7
+ import { DEFAULT_LOCALE } from '@ultimat3/core';
7
8
  import type { Catalog } from './catalog';
8
9
  import { type InterpolationVars, interpolate, selectPluralKey } from './interpolate';
9
- import { DEFAULT_LOCALE, type Locale } from './locales';
10
+ import type { Locale } from './locales';
10
11
 
11
12
  export interface TranslateVars extends InterpolationVars {
12
13
  /** Present ⇒ plural selection runs against the locale's CLDR categories. */
@@ -52,7 +53,7 @@ export interface Translator<TCatalog = Catalog> {
52
53
  readonly locale: Locale;
53
54
  }
54
55
 
55
- export function createTranslator(catalog: Catalog, locale: Locale = DEFAULT_LOCALE): Translator {
56
+ export function catalogTranslator(catalog: Catalog, locale: Locale = DEFAULT_LOCALE): Translator {
56
57
  const hasExact = (key: string): boolean => Object.hasOwn(catalog, key);
57
58
 
58
59
  const resolveKey = (key: string, vars?: TranslateVars): string => {