@softure-ai/core 0.1.11 → 0.2.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/CHANGELOG.md CHANGED
@@ -4,6 +4,31 @@ Newest first. Each version lists what changed for an app that uses `@softure-ai/
4
4
  production, the version gets a line `verified in: <app>@<commit>` ([docs/05](../../docs/05-adoption-playbook.md),
5
5
  "Definition of done"). Versions before the first one below are described in their GitHub Releases (`core@x.y.z`).
6
6
 
7
+ ## 0.2.0
8
+
9
+ - **Breaking (#446):** English is the only built-in language.
10
+ - `LOCALES` is removed; `Locale` is any BCP 47 tag, `isLocale(value)` says whether a value is a well-formed one,
11
+ `DEFAULT_LOCALE` is `"en"`. `defineSoftureConfig` takes any tag as `locale` and refuses one that is not.
12
+ - `coreMessages` and every module ship `en` only. `Dictionaries<T>` is a complete `en` plus any other locales;
13
+ `MessageOverrides<T>` is keyed by any tag, and `mergeMessages` builds a locale without its own defaults on `en`.
14
+ - New `selectMessages(dictionaries, locale)` (the locale, then its language, then `en`), `pickLocalizedText` and
15
+ `LocalizedText` for an app's per-locale option texts.
16
+ - Migrate: an app that shows Polish keeps its own translation and passes it per module as the overrides for its
17
+ locale, `auth({ messages: { pl: authPl } })`; a key it leaves out reads English. Read a module's copy with
18
+ `selectMessages(module.messages, config.locale)` instead of `module.messages[config.locale]`.
19
+ - **Breaking (#438):** core's formatters build in no language besides English.
20
+ - `formatCalendarMonth` drops the `"genitive"` style; `CalendarMonthStyle` is `"long" | "roman"`. A full date's
21
+ month form stays available through `formatCalendarDay`.
22
+ - `formatDuration(months, options?)` takes `{ locale?, messages? }` instead of a locale. Its words default to
23
+ English (`DEFAULT_DURATION_MESSAGES`); an app plugs in its own translation with `messages: DurationMessages` and
24
+ the `locale` whose plural rules pick a form. Migrate `formatDuration(n, "en")` to `formatDuration(n)`.
25
+ - `coreMessages` no longer has the `duration` group.
26
+ - `formatCalendarDay`, `formatCalendarMonth`, `formatMoney`, `formatPercent` and `selectPlural` take any BCP 47 locale
27
+ tag, not only `LOCALES` (#438). Exported: `DurationMessages`, `FormatDurationOptions`, `DEFAULT_DURATION_MESSAGES`.
28
+ - `formatPercent(basisPoints, locale, { fractionDigits })` prints exactly that many fraction digits (`19.00%`,
29
+ `+3.0%`), an integer from 0 to 20 (otherwise a `RangeError`). A value that rounds to zero at that precision stays
30
+ unsigned (`0.0%`). Without it, nothing changes: at most two digits (#455).
31
+
7
32
  ## 0.1.11
8
33
 
9
34
  - Formatting (#410): `formatCalendarMonth(month, locale, "long" | "genitive" | "roman")` for `YYYY-MM` months
package/README.md CHANGED
@@ -6,7 +6,7 @@ The contract every SOFTURE module stands on. Standard:
6
6
  ## 1. What it provides
7
7
 
8
8
  The app configuration (`defineSoftureConfig`), the module contract (`defineModule`), `Result`
9
- with namespaced error codes, an injectable `Clock`, `pl`/`en` messages with partial overrides,
9
+ with namespaced error codes, an injectable `Clock`, English messages with partial overrides and app-supplied translations,
10
10
  and `safeError` with `PublicError`. `@softure-ai/core/cli` loads the app's config for the modules' command-line tools.
11
11
  `@softure-ai/core/og` (server only) is the OG card font loader the modules share (`createOgFontLoader`,
12
12
  `loadOgFonts`); an app takes it from `@softure-ai/seo/server`, where it is documented.
@@ -27,7 +27,7 @@ interface SoftureConfigInput {
27
27
  url: string;
28
28
  handle?: () => DatabaseHandle | Promise<DatabaseHandle>; // the app's own handle, see below
29
29
  } | null;
30
- locale: "en" | "pl";
30
+ locale: string; // a BCP 47 tag, e.g. "en" or "de-AT"
31
31
  timezone: string; // IANA zone, e.g. "Europe/Warsaw"
32
32
  appOrigin: string; // http(s) origin without a path
33
33
  landingOrigin?: string; // the public landing site beside the app (default: appOrigin)
@@ -47,7 +47,7 @@ import { notes } from "@softure-ai/notes";
47
47
 
48
48
  const config = defineSoftureConfig({
49
49
  database: { url: process.env.DATABASE_URL ?? "" },
50
- locale: "pl",
50
+ locale: "en",
51
51
  timezone: "Europe/Warsaw",
52
52
  appOrigin: process.env.APP_ORIGIN ?? "http://localhost:3000",
53
53
  modules: [notes({ limit: 10, routes: { list: "/my-notes" }, messages: { en: { list: { title: "My notes" } } } })],
@@ -249,14 +249,32 @@ None. Tokens, slots and styles live in `@softure-ai/ui`.
249
249
 
250
250
  ## 9. Copy
251
251
 
252
- - `LOCALES` is `["en", "pl"]`; every module ships both dictionaries complete (`pl.ts` is typed
253
- `typeof en`).
254
- - `mergeMessages(defaults, overrides)` applies partial overrides per locale; unknown keys and a
255
- string in place of a group are ignored.
252
+ - English is the only built-in language: every module ships one complete `en` dictionary, and
253
+ `DEFAULT_LOCALE` is `"en"`. `Locale` is any BCP 47 tag; `isLocale(value)` says whether a value is a
254
+ well-formed one, and `defineSoftureConfig` refuses a `locale` that is not.
255
+ - `Dictionaries<T>` is a complete `en` plus any other locales; `MessageOverrides<T>` partial dictionaries keyed
256
+ by locale. `mergeMessages(defaults, overrides)` returns `en` and every locale the defaults or the overrides
257
+ name; a locale without its own defaults is built on `en`. Unknown keys and a string in place of a group are
258
+ ignored.
259
+ - `selectMessages(dictionaries, locale)` picks the exact locale, then its language (`de-AT` reads `de`), then
260
+ `en`. `pickLocalizedText(text, locale, fallback?)` does the same for an app's `LocalizedText`
261
+ (`{ en: "Monthly", de: "Monatlich" }`).
262
+ - **Another language is the app's own translation.** The app sets `locale` and passes its dictionary as the
263
+ overrides for that locale, per module; a key it leaves out reads English:
264
+
265
+ ```ts
266
+ defineSoftureConfig({
267
+ locale: "de",
268
+ // ...
269
+ modules: [auth({ messages: { de: authDe } })], // authDe: DeepPartial<AuthMessages>, kept in the app
270
+ });
271
+ ```
272
+
273
+ The ui and charts components take the same translation through their `messages` prop (or the ui provider).
256
274
  - `formatMessage("{count} of {total}", { count, total })` fills placeholders; a missing value
257
275
  stays visible as `{name}`.
258
276
  - `selectPlural(locale, count, { one, few, many, other })` uses `Intl.PluralRules`, built once per
259
- locale and reused.
277
+ locale and reused. `locale` is any BCP 47 tag; a category the forms leave out falls back to `other`.
260
278
  - `getMessage(dictionary, "errors.unexpected")` reads a dotted path.
261
279
  - Core's own keys (`coreMessages`): `errors.database_failed`, `errors.unexpected`, the copy for
262
280
  the `core.database_failed` and `core.unexpected` codes.
@@ -322,26 +340,43 @@ or a fractional count.
322
340
  ```ts
323
341
  formatCalendarDay("2026-10-04", "en"); // "October 4, 2026"; "medium": "Oct 4, 2026"; "numeric": "10/04/2026"
324
342
  formatCalendarDay(toCalendarDay(instant, config.timezone), config.locale); // an instant's day in the app's zone
325
- formatMoney(123456, "PLN", "en"); // "PLN 1,234.56"; pl groups four digits too
343
+ formatMoney(123456, "PLN", "en"); // "PLN 1,234.56"; thousands always grouped, also where a locale would not
326
344
  formatMoney(-123456, "PLN", "en", { rounded: true, signed: true }); // "-PLN 1,235"; { compact: true }: "PLN 1.2K"
327
345
  formatPercent(1250, "en"); // "12.5%" (basis points, at most two fraction digits)
328
346
  formatPercent(-50, "en", { signed: true, tight: true }); // "-0.5%"; 1250 signed: "+12.5%"
329
- formatCalendarMonth("2026-10", "en"); // "October 2026"; "genitive": pl's genitive month; "roman": "X 2026"
330
- formatDuration(38, "en"); // "3 years and 2 months" (whole months, as wholeMonthsBetween returns them)
347
+ formatPercent(1900, "en", { fractionDigits: 2 }); // "19.00%"; 300 at one digit, signed: "+3.0%"
348
+ formatCalendarMonth("2026-10", "en"); // "October 2026"; "roman": "X 2026"
349
+ formatDuration(38); // "3 years and 2 months" (whole months, as wholeMonthsBetween returns them)
331
350
  ```
332
351
 
333
- A day is formatted at UTC midnight in UTC, so no zone moves it; the Polish long style names the month in
334
- the genitive. `formatMoney` takes the amount in the currency's minor unit, read from the pinned ISO 4217
352
+ Every formatter takes any BCP 47 locale tag (an invalid tag throws `Intl`'s `RangeError`), and
353
+ writes it as `Intl` does. A day is formatted at UTC midnight in UTC, so no zone moves it. `formatMoney` takes the amount in the currency's minor unit, read from the pinned ISO 4217
335
354
  table `CURRENCY_MINOR_UNIT_DIGITS` (a code outside it gets `Intl`'s digits), and always groups thousands.
336
355
  Zero never carries a sign: a negative zero (`Math.round(-0.4)`) or an amount that rounds to zero shows as `PLN 0.00`,
337
356
  and a percentage that rounds to zero as `0%`. `signed` puts a `+` before a positive amount or percentage; the minus is
338
- the one `Intl` writes (a hyphen-minus in en and pl). `tight` drops any space a locale's pattern puts before `%`.
357
+ the one `Intl` writes (a hyphen-minus in en). `tight` drops any space a locale's pattern puts before `%`.
358
+ `fractionDigits` fixes the number of fraction digits (an integer from 0 to 20) so a column reads equally precise; a
359
+ value that rounds to zero at that precision is still unsigned (`0.0%`).
339
360
 
340
361
  `formatCalendarMonth(month, locale, style)` takes `YYYY-MM` (`isCalendarMonth`) or a `YYYY-MM-DD` day. `long` is the
341
- month as written alone (the nominative in pl), `genitive` the form a full date uses (pl: "4 [month] 2026" inflects
342
- it), `roman` the month in Roman numerals for compact tables. `formatDuration(months, locale)` writes years and months
343
- with the locale's plural forms (copy in `coreMessages.<locale>.duration`); a part that is zero is left out, and zero
344
- itself is "0 months".
362
+ month as written alone, `roman` the month in Roman numerals for compact tables.
363
+
364
+ `formatDuration(months, { locale, messages })` writes years and months; a part that is zero is left out, and zero
365
+ itself is "0 months". Its only built-in words are English (`DEFAULT_DURATION_MESSAGES`). An app that shows another
366
+ language passes its own `DurationMessages` and the locale whose plural rules pick among them; a locale alone keeps the
367
+ English words:
368
+
369
+ ```ts
370
+ const durationDe: DurationMessages = {
371
+ years: { one: "{count} Jahr", other: "{count} Jahre" },
372
+ months: { one: "{count} Monat", other: "{count} Monate" },
373
+ both: "{years} und {months}",
374
+ };
375
+ formatDuration(13, { locale: "de", messages: durationDe }); // "1 Jahr und 1 Monat"
376
+ ```
377
+
378
+ Give every plural category the locale uses (`Intl.PluralRules`: `one`, `few`, `many`, `other`, ...); a missing one
379
+ falls back to `other`.
345
380
 
346
381
  **Memoization.** `memoizeByContent(fn, { max })` caches a pure function of plain data by the content of its arguments:
347
382
  two equal objects built apart (key order aside) hit the same entry. The least recently used entry goes beyond `max`;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAGnD;;;GAGG;AAEH,MAAM,WAAW,0BAA0B;CAAG;AAE9C,iHAAiH;AACjH,MAAM,MAAM,qBAAqB,GAAG,0BAA0B,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5H,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,YAAY,CAAC,qBAAqB,CAAC,CAAC;CAC7D;AAED,qHAAqH;AACrH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AAED,kDAAkD;AAClD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AA+BD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAoB5E;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEzF;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GACnC,MAAM,CAAC,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAGrD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,EAAY,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAClD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,EAAW,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AAGnD;;;GAGG;AAEH,MAAM,WAAW,0BAA0B;CAAG;AAE9C,iHAAiH;AACjH,MAAM,MAAM,qBAAqB,GAAG,0BAA0B,SAAS;IAAE,QAAQ,CAAC,MAAM,EAAE,MAAM,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5H,KAAK,YAAY,CAAC,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;AAEtC,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB;;;;;;;OAOG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,YAAY,CAAC,qBAAqB,CAAC,CAAC;CAC7D;AAED,qHAAqH;AACrH,MAAM,WAAW,cAAc;IAC7B;;;OAGG;IACH,QAAQ,CAAC,cAAc,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3C;;;OAGG;IACH,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;CACpC;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC5C,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,MAAM,WAAW,aAAa;IAC5B;;;OAGG;IACH,QAAQ,CAAC,QAAQ,EAAE,qBAAqB,GAAG,IAAI,CAAC;IAChD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,6EAA6E;IAC7E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,mFAAmF;IACnF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;OAGG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,4FAA4F;IAC5F,QAAQ,CAAC,OAAO,EAAE,cAAc,CAAC;IACjC,yDAAyD;IACzD,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AAED,kDAAkD;AAClD,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,QAAQ,CAAC,EAAE,qBAAqB,GAAG,IAAI,CAAC;IACjD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAChC,QAAQ,CAAC,OAAO,CAAC,EAAE,mBAAmB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;CAC/C;AA+BD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,kBAAkB,GAAG,aAAa,CAoB5E;AAED,oFAAoF;AACpF,wBAAgB,SAAS,CAAC,MAAM,EAAE,aAAa,EAAE,EAAE,EAAE,MAAM,GAAG,gBAAgB,GAAG,SAAS,CAEzF;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,SAAS,gBAAgB,EAAE,GACnC,MAAM,CAAC,gBAAgB,EAAE,EAAE,uBAAuB,CAAC,CAGrD"}
package/dist/config.js CHANGED
@@ -3,7 +3,7 @@
3
3
  import { z } from "zod";
4
4
  import { formatIssues, SoftureConfigError } from "./config-error.js";
5
5
  import { isDatabaseOptional } from "./database-requirement.js";
6
- import { LOCALES } from "./i18n.js";
6
+ import { isLocale } from "./i18n.js";
7
7
  import { err, ok } from "./result.js";
8
8
  import { parseVersionRange, satisfiesRange } from "./version-range.js";
9
9
  const CONFIG_SUBJECT = "softure.config (defineSoftureConfig)";
@@ -20,7 +20,7 @@ const configSchema = z.object({
20
20
  })
21
21
  .nullable()
22
22
  .default(null),
23
- locale: z.enum(LOCALES),
23
+ locale: z.string().refine(isLocale, "must be a BCP 47 language tag, e.g. en or de-AT"),
24
24
  timezone: z.string().refine(isTimeZone, "must be an IANA time zone, e.g. Europe/Warsaw"),
25
25
  appOrigin: z.string().refine(isOrigin, ORIGIN_MESSAGE),
26
26
  landingOrigin: z.string().refine(isOrigin, ORIGIN_MESSAGE).optional(),
@@ -1 +1 @@
1
- {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
package/dist/format.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { type Locale } from "./i18n.js";
2
- /** `long`: October 4, 2026 (pl names the month in the genitive); `medium`: Oct 4, 2026; `numeric`: 10/04/2026 (pl: 04.10.2026). */
1
+ import { type PluralForms } from "./i18n.js";
2
+ /** `long`: October 4, 2026; `medium`: Oct 4, 2026; `numeric`: 10/04/2026 (each locale writes its own order and separators). */
3
3
  export type CalendarDayStyle = "long" | "medium" | "numeric";
4
4
  export interface FormatMoneyOptions {
5
5
  /** Whole units only, rounded half away from zero: PLN 1,235 for 1234.56. */
@@ -9,39 +9,60 @@ export interface FormatMoneyOptions {
9
9
  /** The locale's short notation for large amounts: PLN 1.2M. */
10
10
  readonly compact?: boolean;
11
11
  }
12
- /** `long`: October 2026 (pl: the month in the nominative); `genitive`: pl's genitive month, as inside a date; `roman`: X 2026. */
13
- export type CalendarMonthStyle = "long" | "genitive" | "roman";
12
+ /** `long`: October 2026; `roman`: X 2026. */
13
+ export type CalendarMonthStyle = "long" | "roman";
14
+ /** The words `formatDuration` writes: plural forms with `{count}`, and `both` joining `{years}` and `{months}`. */
15
+ export interface DurationMessages {
16
+ readonly years: PluralForms;
17
+ readonly months: PluralForms;
18
+ readonly both: string;
19
+ }
20
+ export interface FormatDurationOptions {
21
+ /** The BCP 47 tag whose plural rules pick a form (default `"en"`). */
22
+ readonly locale?: string;
23
+ /** The app's own words (default `DEFAULT_DURATION_MESSAGES`, English). */
24
+ readonly messages?: DurationMessages;
25
+ }
14
26
  export interface FormatPercentOptions {
15
27
  /** A `+` before a positive value (a `-` before a negative one is always there). Zero has neither in any mode. */
16
28
  readonly signed?: boolean;
17
29
  /** No space between the number and `%`, whatever the locale's pattern: `12,5%`, not `12,5 %`. */
18
30
  readonly tight?: boolean;
31
+ /**
32
+ * Exactly this many fraction digits, an integer from 0 to 20: `{ fractionDigits: 2 }` writes 1900 as `19.00%`.
33
+ * Without it a value has at most two fraction digits and no trailing zeros.
34
+ */
35
+ readonly fractionDigits?: number;
19
36
  }
37
+ /** The built-in, English words of `formatDuration`; any other category falls back to `other`. */
38
+ export declare const DEFAULT_DURATION_MESSAGES: DurationMessages;
20
39
  /** A `YYYY-MM-DD` day as `locale` writes it. Throws a `RangeError` for a malformed day. */
21
- export declare function formatCalendarDay(day: string, locale: Locale, style?: CalendarDayStyle): string;
40
+ export declare function formatCalendarDay(day: string, locale: string, style?: CalendarDayStyle): string;
22
41
  /** Whether `value` is a real calendar month written `YYYY-MM` (`2026-13` and `2026-1` are not). */
23
42
  export declare function isCalendarMonth(value: unknown): value is string;
24
43
  /**
25
44
  * The month of a `YYYY-MM` month or a `YYYY-MM-DD` day, with its year, as `locale` writes it alone. Throws a
26
45
  * `RangeError` for anything else.
27
46
  */
28
- export declare function formatCalendarMonth(month: string, locale: Locale, style?: CalendarMonthStyle): string;
47
+ export declare function formatCalendarMonth(month: string, locale: string, style?: CalendarMonthStyle): string;
29
48
  /**
30
- * Whole months as years and months in `locale`, with its plural forms: 38 is "3 years and 2 months"; a part that is
31
- * zero is left out, and zero itself is "0 months". Throws a `RangeError` for a negative or fractional count.
49
+ * Whole months as years and months: 38 is "3 years and 2 months"; a part that is zero is left out, and zero itself is
50
+ * "0 months". English by default; an app passes its own `messages` and the `locale` whose plural rules pick among
51
+ * them. Throws a `RangeError` for a negative or fractional count, or an invalid locale tag.
32
52
  */
33
- export declare function formatDuration(months: number, locale: Locale): string;
53
+ export declare function formatDuration(months: number, options?: FormatDurationOptions): string;
34
54
  /**
35
55
  * An amount in the currency's minor unit (2900 PLN = PLN 29.00) in `locale`'s notation, thousands always
36
- * grouped (pl writes 1 234,56 where its default would leave four digits together), and zero is never signed (`-0`
56
+ * grouped (even where a locale's default would leave four digits together), and zero is never signed (`-0`
37
57
  * and an amount that rounds to zero show as zero). The minor unit is the pinned ISO
38
58
  * 4217 one (`CURRENCY_MINOR_UNIT_DIGITS`); a code outside the table gets `Intl`'s digits. Throws a
39
59
  * `RangeError` for an amount that is not a whole number of minor units, or a code `Intl` refuses.
40
60
  */
41
- export declare function formatMoney(minor: number, currency: string, locale: Locale, options?: FormatMoneyOptions): string;
61
+ export declare function formatMoney(minor: number, currency: string, locale: string, options?: FormatMoneyOptions): string;
42
62
  /**
43
- * Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits, and zero is never signed (`-0`
44
- * and a value that rounds to zero show as `0%`). Throws a `RangeError` for a non-finite value.
63
+ * Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits unless `fractionDigits` fixes
64
+ * them, and zero is never signed (`-0` and a value that rounds to zero show as `0%`, or `0.0%` at one digit). Throws a
65
+ * `RangeError` for a non-finite value or an invalid `fractionDigits`.
45
66
  */
46
- export declare function formatPercent(basisPoints: number, locale: Locale, options?: FormatPercentOptions): string;
67
+ export declare function formatPercent(basisPoints: number, locale: string, options?: FormatPercentOptions): string;
47
68
  //# sourceMappingURL=format.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAIA,OAAO,EAA+B,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAGrE,mIAAmI;AACnI,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,yKAAyK;IACzK,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,kIAAkI;AAClI,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACnC,iHAAiH;IACjH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,iGAAiG;IACjG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;CAC1B;AAiBD,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,gBAAyB,GAAG,MAAM,CAGvG;AAED,mGAAmG;AACnG,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAM/D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,kBAA2B,GAAG,MAAM,CAW7G;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAYrE;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,CAarH;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAc7G"}
1
+ {"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAMA,OAAO,EAA+B,KAAK,WAAW,EAAE,MAAM,WAAW,CAAC;AAE1E,+HAA+H;AAC/H,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE7D,MAAM,WAAW,kBAAkB;IACjC,4EAA4E;IAC5E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,yKAAyK;IACzK,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,6CAA6C;AAC7C,MAAM,MAAM,kBAAkB,GAAG,MAAM,GAAG,OAAO,CAAC;AAElD,mHAAmH;AACnH,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,QAAQ,CAAC,EAAE,gBAAgB,CAAC;CACtC;AAED,MAAM,WAAW,oBAAoB;IACnC,iHAAiH;IACjH,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAC1B,iGAAiG;IACjG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;CAClC;AAMD,iGAAiG;AACjG,eAAO,MAAM,yBAAyB,EAAE,gBAIvC,CAAC;AAeF,2FAA2F;AAC3F,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,gBAAyB,GAAG,MAAM,CAGvG;AAED,mGAAmG;AACnG,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAM/D;AAED;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,GAAE,kBAA2B,GAAG,MAAM,CAO7G;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAa1F;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,kBAAuB,GAAG,MAAM,CAarH;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAE,oBAAyB,GAAG,MAAM,CAc7G"}
package/dist/format.js CHANGED
@@ -1,14 +1,22 @@
1
1
  // Days, money and percentages as people read them, one way for every module and app. A calendar day
2
- // is formatted at UTC midnight in UTC, so neither the server's nor the reader's zone moves it.
2
+ // is formatted at UTC midnight in UTC, so neither the server's nor the reader's zone moves it. Every
3
+ // formatter takes any BCP 47 locale tag (an invalid one throws Intl's RangeError); the only built-in
4
+ // copy is English, and an app passes its own words where a formatter writes any.
3
5
  import { isCalendarDay } from "./calendar-day.js";
4
6
  import { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
5
7
  import { formatMessage, selectPlural } from "./i18n.js";
6
- import { coreMessages } from "./messages/index.js";
7
8
  const MONTHS_PER_YEAR = 12;
8
9
  const MONTH_PATTERN = /^(\d{4})-(\d{2})$/;
9
10
  const ROMAN_MONTHS = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"];
11
+ /** The built-in, English words of `formatDuration`; any other category falls back to `other`. */
12
+ export const DEFAULT_DURATION_MESSAGES = {
13
+ years: { one: "{count} year", other: "{count} years" },
14
+ months: { one: "{count} month", other: "{count} months" },
15
+ both: "{years} and {months}",
16
+ };
10
17
  const BASIS_POINTS_PER_UNIT = 10_000;
11
18
  const MAX_PERCENT_FRACTION_DIGITS = 2;
19
+ const FRACTION_DIGITS_LIMIT = 20;
12
20
  const dayFormatters = new Map();
13
21
  const numberFormatters = new Map();
14
22
  const DAY_STYLE_OPTIONS = {
@@ -44,22 +52,19 @@ export function formatCalendarMonth(month, locale, style = "long") {
44
52
  const year = firstDay.slice(0, 4);
45
53
  if (style === "roman")
46
54
  return `${ROMAN_MONTHS[date.getUTCMonth()] ?? ""} ${year}`;
47
- if (style === "long")
48
- return getDayFormatter(`${locale}:month`, locale, { month: "long", year: "numeric" }).format(date);
49
- // The genitive is the form a full date uses, so it is read from a day-month-year date's month part.
50
- const parts = getDayFormatter(`${locale}:day-month`, locale, { day: "numeric", month: "long", year: "numeric" }).formatToParts(date);
51
- const name = parts.find((part) => part.type === "month")?.value ?? "";
52
- return `${name} ${year}`;
55
+ return getDayFormatter(`${locale}:month`, locale, { month: "long", year: "numeric" }).format(date);
53
56
  }
54
57
  /**
55
- * Whole months as years and months in `locale`, with its plural forms: 38 is "3 years and 2 months"; a part that is
56
- * zero is left out, and zero itself is "0 months". Throws a `RangeError` for a negative or fractional count.
58
+ * Whole months as years and months: 38 is "3 years and 2 months"; a part that is zero is left out, and zero itself is
59
+ * "0 months". English by default; an app passes its own `messages` and the `locale` whose plural rules pick among
60
+ * them. Throws a `RangeError` for a negative or fractional count, or an invalid locale tag.
57
61
  */
58
- export function formatDuration(months, locale) {
62
+ export function formatDuration(months, options = {}) {
59
63
  if (!Number.isSafeInteger(months) || months < 0) {
60
64
  throw new RangeError(`formatDuration: ${String(months)} is not a whole, non-negative number of months`);
61
65
  }
62
- const copy = coreMessages[locale].duration;
66
+ const locale = options.locale ?? "en";
67
+ const copy = options.messages ?? DEFAULT_DURATION_MESSAGES;
63
68
  const years = Math.floor(months / MONTHS_PER_YEAR);
64
69
  const rest = months % MONTHS_PER_YEAR;
65
70
  const yearsText = formatMessage(selectPlural(locale, years, copy.years), { count: years });
@@ -72,7 +77,7 @@ export function formatDuration(months, locale) {
72
77
  }
73
78
  /**
74
79
  * An amount in the currency's minor unit (2900 PLN = PLN 29.00) in `locale`'s notation, thousands always
75
- * grouped (pl writes 1 234,56 where its default would leave four digits together), and zero is never signed (`-0`
80
+ * grouped (even where a locale's default would leave four digits together), and zero is never signed (`-0`
76
81
  * and an amount that rounds to zero show as zero). The minor unit is the pinned ISO
77
82
  * 4217 one (`CURRENCY_MINOR_UNIT_DIGITS`); a code outside the table gets `Intl`'s digits. Throws a
78
83
  * `RangeError` for an amount that is not a whole number of minor units, or a code `Intl` refuses.
@@ -93,15 +98,16 @@ export function formatMoney(minor, currency, locale, options = {}) {
93
98
  return format.format(minor / 10 ** digits);
94
99
  }
95
100
  /**
96
- * Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits, and zero is never signed (`-0`
97
- * and a value that rounds to zero show as `0%`). Throws a `RangeError` for a non-finite value.
101
+ * Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits unless `fractionDigits` fixes
102
+ * them, and zero is never signed (`-0` and a value that rounds to zero show as `0%`, or `0.0%` at one digit). Throws a
103
+ * `RangeError` for a non-finite value or an invalid `fractionDigits`.
98
104
  */
99
105
  export function formatPercent(basisPoints, locale, options = {}) {
100
106
  if (!Number.isFinite(basisPoints))
101
107
  throw new RangeError(`formatPercent: ${String(basisPoints)} is not a finite number of basis points`);
102
108
  const format = getNumberFormatter(locale, {
103
109
  style: "percent",
104
- maximumFractionDigits: MAX_PERCENT_FRACTION_DIGITS,
110
+ ...getPercentFractionDigits(options.fractionDigits),
105
111
  signDisplay: options.signed === true ? "exceptZero" : "negative",
106
112
  });
107
113
  const value = basisPoints / BASIS_POINTS_PER_UNIT;
@@ -113,6 +119,14 @@ export function formatPercent(basisPoints, locale, options = {}) {
113
119
  .map((part) => part.value)
114
120
  .join("");
115
121
  }
122
+ function getPercentFractionDigits(fractionDigits) {
123
+ if (fractionDigits === undefined)
124
+ return { maximumFractionDigits: MAX_PERCENT_FRACTION_DIGITS };
125
+ if (!Number.isInteger(fractionDigits) || fractionDigits < 0 || fractionDigits > FRACTION_DIGITS_LIMIT) {
126
+ throw new RangeError(`formatPercent: fractionDigits ${String(fractionDigits)} is not an integer from 0 to ${String(FRACTION_DIGITS_LIMIT)}`);
127
+ }
128
+ return { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits };
129
+ }
116
130
  function isNextToPercentSign(parts, index) {
117
131
  return parts[index - 1]?.type === "percentSign" || parts[index + 1]?.type === "percentSign";
118
132
  }
@@ -1 +1 @@
1
- {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
package/dist/i18n.d.ts CHANGED
@@ -1,5 +1,7 @@
1
- export declare const LOCALES: readonly ["en", "pl"];
2
- export type Locale = (typeof LOCALES)[number];
1
+ /** A BCP 47 language tag (`en`, `de-AT`). */
2
+ export type Locale = string;
3
+ /** The language of every built-in dictionary and the fallback for any other locale. */
4
+ export declare const DEFAULT_LOCALE = "en";
3
5
  /** A dictionary: nested groups of message strings. */
4
6
  export interface MessageTree {
5
7
  readonly [key: string]: string | MessageTree;
@@ -8,10 +10,20 @@ export interface MessageTree {
8
10
  export type DeepPartial<T> = {
9
11
  [K in keyof T]?: T[K] extends string ? string : DeepPartial<T[K]>;
10
12
  };
11
- /** One complete dictionary per locale. */
12
- export type Dictionaries<T extends MessageTree> = Readonly<Record<Locale, T>>;
13
- /** Partial overrides an app passes for some locales. */
14
- export type MessageOverrides<T extends MessageTree> = Partial<Record<Locale, DeepPartial<T>>>;
13
+ /** A complete `en` dictionary, plus complete dictionaries for any other locales. */
14
+ export type Dictionaries<T extends MessageTree> = {
15
+ readonly en: T;
16
+ } & {
17
+ readonly [locale: string]: T | undefined;
18
+ };
19
+ /** Partial overrides an app passes per locale: a translation for its own locale, or changed English copy. */
20
+ export type MessageOverrides<T extends MessageTree> = {
21
+ readonly [locale: string]: DeepPartial<T> | undefined;
22
+ };
23
+ /** A text an app writes per locale in its options (a plan name, a label); `en` is the fallback. */
24
+ export type LocalizedText = {
25
+ readonly [locale: string]: string | undefined;
26
+ };
15
27
  /** Plural forms named after the `Intl.PluralRules` categories; `other` is the fallback. */
16
28
  export interface PluralForms {
17
29
  readonly zero?: string;
@@ -21,17 +33,28 @@ export interface PluralForms {
21
33
  readonly many?: string;
22
34
  readonly other: string;
23
35
  }
36
+ /** Whether `value` is a well-formed BCP 47 language tag. */
24
37
  export declare function isLocale(value: unknown): value is Locale;
25
38
  /**
26
- * The defaults with the overrides applied, per locale. Only keys the defaults have are taken,
27
- * and a group is never replaced by a string, so an override cannot break the dictionary shape.
28
- * Inputs are not mutated.
39
+ * The defaults with the overrides applied, per locale: `en` and every locale the defaults or the
40
+ * overrides name. A locale without its own defaults is built on `en`, so a key its override leaves
41
+ * out reads English. Only keys the defaults have are taken, and a group is never replaced by a
42
+ * string, so an override cannot break the dictionary shape. Inputs are not mutated.
29
43
  */
30
44
  export declare function mergeMessages<T extends MessageTree>(defaults: Dictionaries<T>, overrides?: MessageOverrides<T>): Dictionaries<T>;
45
+ /**
46
+ * The dictionary for `locale`: its own, else its language's (`pt-BR` reads `pt`), else `en`.
47
+ */
48
+ export declare function selectMessages<T extends MessageTree>(dictionaries: Dictionaries<T>, locale: Locale): T;
49
+ /** The text for `locale` with `selectMessages`' fallback order, or `fallback` when no locale has one. */
50
+ export declare function pickLocalizedText(text: LocalizedText | undefined, locale: Locale, fallback?: string): string;
31
51
  /** Fills `{name}` placeholders. A placeholder without a value stays as written. */
32
52
  export declare function formatMessage(template: string, params?: Readonly<Record<string, string | number>>): string;
33
- /** The form for `count` under the plural rules of `locale` ("1 file", "2 files"). */
34
- export declare function selectPlural(locale: Locale, count: number, forms: PluralForms): string;
53
+ /**
54
+ * The form for `count` under the plural rules of `locale`, any BCP 47 tag ("1 file", "2 files"). A category the
55
+ * forms leave out falls back to `other`.
56
+ */
57
+ export declare function selectPlural(locale: string, count: number, forms: PluralForms): string;
35
58
  /** The message at a dotted path (`login.title`), or `undefined` when there is no string there. */
36
59
  export declare function getMessage(tree: MessageTree, path: string): string | undefined;
37
60
  //# sourceMappingURL=i18n.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,OAAO,uBAAwB,CAAC;AAE7C,MAAM,MAAM,MAAM,GAAG,CAAC,OAAO,OAAO,CAAC,CAAC,MAAM,CAAC,CAAC;AAE9C,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC;CAC9C;AAED,8DAA8D;AAC9D,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEnG,0CAA0C;AAC1C,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,WAAW,IAAI,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC;AAE9E,wDAAwD;AACxD,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9F,2FAA2F;AAC3F,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAExD;AAED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EACjD,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EACzB,SAAS,GAAE,gBAAgB,CAAC,CAAC,CAAM,GAClC,YAAY,CAAC,CAAC,CAAC,CAMjB;AAED,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAM,GAAG,MAAM,CAK9G;AAID,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAOtF;AAED,kGAAkG;AAClG,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS9E"}
1
+ {"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"AAIA,6CAA6C;AAC7C,MAAM,MAAM,MAAM,GAAG,MAAM,CAAC;AAE5B,uFAAuF;AACvF,eAAO,MAAM,cAAc,OAAO,CAAC;AAEnC,sDAAsD;AACtD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC;CAC9C;AAED,8DAA8D;AAC9D,MAAM,MAAM,WAAW,CAAC,CAAC,IAAI;KAAG,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,MAAM,GAAG,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;CAAE,CAAC;AAEnG,oFAAoF;AACpF,MAAM,MAAM,YAAY,CAAC,CAAC,SAAS,WAAW,IAAI;IAAE,QAAQ,CAAC,EAAE,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAA;CAAE,CAAC;AAEpH,6GAA6G;AAC7G,MAAM,MAAM,gBAAgB,CAAC,CAAC,SAAS,WAAW,IAAI;IAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,WAAW,CAAC,CAAC,CAAC,GAAG,SAAS,CAAA;CAAE,CAAC;AAEhH,mGAAmG;AACnG,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAA;CAAE,CAAC;AAE9E,2FAA2F;AAC3F,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;CACxB;AAED,4DAA4D;AAC5D,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,MAAM,CAQxD;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,CAAC,SAAS,WAAW,EACjD,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,EACzB,SAAS,GAAE,gBAAgB,CAAC,CAAC,CAAM,GAClC,YAAY,CAAC,CAAC,CAAC,CAMjB;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,WAAW,EAAE,YAAY,EAAE,YAAY,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,CAAC,CAEtG;AAED,yGAAyG;AACzG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,SAAK,GAAG,MAAM,CAGxG;AAMD,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,CAAM,GAAG,MAAM,CAK9G;AAID;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,MAAM,CAOtF;AAED,kGAAkG;AAClG,wBAAgB,UAAU,CAAC,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAS9E"}
package/dist/i18n.js CHANGED
@@ -1,21 +1,48 @@
1
- // Copy and localization (docs/02-module-standard.md §6): every module ships complete `en` and
2
- // `pl` dictionaries, and the app passes partial overrides per locale.
3
- export const LOCALES = ["en", "pl"];
1
+ // Copy and localization (docs/02-module-standard.md §6): every module ships a complete `en`
2
+ // dictionary, the only built-in language. An app that shows another language passes its own
3
+ // translation as overrides for its locale; a key it leaves out reads English.
4
+ /** The language of every built-in dictionary and the fallback for any other locale. */
5
+ export const DEFAULT_LOCALE = "en";
6
+ /** Whether `value` is a well-formed BCP 47 language tag. */
4
7
  export function isLocale(value) {
5
- return LOCALES.some((locale) => locale === value);
8
+ if (typeof value !== "string" || value === "")
9
+ return false;
10
+ try {
11
+ return Intl.getCanonicalLocales(value).length === 1;
12
+ }
13
+ catch {
14
+ // RangeError: not a well-formed tag.
15
+ return false;
16
+ }
6
17
  }
7
18
  /**
8
- * The defaults with the overrides applied, per locale. Only keys the defaults have are taken,
9
- * and a group is never replaced by a string, so an override cannot break the dictionary shape.
10
- * Inputs are not mutated.
19
+ * The defaults with the overrides applied, per locale: `en` and every locale the defaults or the
20
+ * overrides name. A locale without its own defaults is built on `en`, so a key its override leaves
21
+ * out reads English. Only keys the defaults have are taken, and a group is never replaced by a
22
+ * string, so an override cannot break the dictionary shape. Inputs are not mutated.
11
23
  */
12
24
  export function mergeMessages(defaults, overrides = {}) {
13
25
  const merged = {};
14
- for (const locale of LOCALES) {
15
- merged[locale] = mergeTree(defaults[locale], overrides[locale]);
26
+ for (const locale of new Set([DEFAULT_LOCALE, ...Object.keys(defaults), ...Object.keys(overrides)])) {
27
+ merged[locale] = mergeTree(defaults[locale] ?? defaults.en, overrides[locale]);
16
28
  }
17
29
  return merged;
18
30
  }
31
+ /**
32
+ * The dictionary for `locale`: its own, else its language's (`pt-BR` reads `pt`), else `en`.
33
+ */
34
+ export function selectMessages(dictionaries, locale) {
35
+ return dictionaries[locale] ?? dictionaries[getLanguage(locale)] ?? dictionaries.en;
36
+ }
37
+ /** The text for `locale` with `selectMessages`' fallback order, or `fallback` when no locale has one. */
38
+ export function pickLocalizedText(text, locale, fallback = "") {
39
+ if (text === undefined)
40
+ return fallback;
41
+ return text[locale] ?? text[getLanguage(locale)] ?? text.en ?? fallback;
42
+ }
43
+ function getLanguage(locale) {
44
+ return locale.split("-")[0] ?? locale;
45
+ }
19
46
  /** Fills `{name}` placeholders. A placeholder without a value stays as written. */
20
47
  export function formatMessage(template, params = {}) {
21
48
  return template.replace(/\{(\w+)\}/g, (placeholder, name) => {
@@ -24,7 +51,10 @@ export function formatMessage(template, params = {}) {
24
51
  });
25
52
  }
26
53
  const pluralRulesByLocale = new Map();
27
- /** The form for `count` under the plural rules of `locale` ("1 file", "2 files"). */
54
+ /**
55
+ * The form for `count` under the plural rules of `locale`, any BCP 47 tag ("1 file", "2 files"). A category the
56
+ * forms leave out falls back to `other`.
57
+ */
28
58
  export function selectPlural(locale, count, forms) {
29
59
  let rules = pluralRulesByLocale.get(locale);
30
60
  if (rules === undefined) {
package/dist/i18n.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"i18n.js","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"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"}
package/dist/index.d.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  export { addCalendarDays, addCalendarMonths, calendarDaysBetween, calendarDaySchema, getCalendarDay, isCalendarDay, toCalendarDay, wholeMonthsBetween, type EndOfMonth, } from "./calendar-day.js";
2
2
  export { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
3
- export { formatCalendarDay, formatCalendarMonth, formatDuration, formatMoney, formatPercent, isCalendarMonth, type CalendarDayStyle, type CalendarMonthStyle, type FormatMoneyOptions, type FormatPercentOptions, } from "./format.js";
3
+ export { DEFAULT_DURATION_MESSAGES, formatCalendarDay, formatCalendarMonth, formatDuration, formatMoney, formatPercent, isCalendarMonth, type CalendarDayStyle, type CalendarMonthStyle, type DurationMessages, type FormatDurationOptions, type FormatMoneyOptions, type FormatPercentOptions, } from "./format.js";
4
4
  export { memoizeByContent, type MemoizeByContentOptions, type MemoizedFunction } from "./memoize.js";
5
5
  export { createTestClock, systemClock, type Clock, type TestClock } from "./clock.js";
6
- export { formatMessage, getMessage, isLocale, LOCALES, mergeMessages, selectPlural, type DeepPartial, type Dictionaries, type Locale, type MessageOverrides, type MessageTree, type PluralForms, } from "./i18n.js";
6
+ export { formatMessage, getMessage, DEFAULT_LOCALE, isLocale, mergeMessages, pickLocalizedText, selectMessages, selectPlural, type DeepPartial, type Dictionaries, type Locale, type LocalizedText, type MessageOverrides, type MessageTree, type PluralForms, } from "./i18n.js";
7
7
  export { coreMessages, type CoreMessages } from "./messages/index.js";
8
8
  export { err, ok, type Err, type ErrorCode, type Ok, type Result } from "./result.js";
9
9
  export { errorLogLabel, getPublicMessage, isPublicError, PublicError, safeError, type CoreErrorCode } from "./safe-error.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,EAClB,KAAK,UAAU,GAChB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,WAAW,EACX,aAAa,EACb,eAAe,EACf,KAAK,gBAAgB,EACrB,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACvB,KAAK,oBAAoB,GAC1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,WAAW,EAAE,KAAK,KAAK,EAAE,KAAK,SAAS,EAAE,MAAM,YAAY,CAAC;AACtF,OAAO,EACL,aAAa,EACb,UAAU,EACV,QAAQ,EACR,OAAO,EACP,aAAa,EACb,YAAY,EACZ,KAAK,WAAW,EAChB,KAAK,YAAY,EACjB,KAAK,MAAM,EACX,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,WAAW,GACjB,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,KAAK,GAAG,EAAE,KAAK,SAAS,EAAE,KAAK,EAAE,EAAE,KAAK,MAAM,EAAE,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAE,KAAK,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,yBAAyB,EACzB,KAAK,aAAa,EAClB,KAAK,kBAAkB,EACvB,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC1B,KAAK,0BAA0B,EAC/B,KAAK,cAAc,EACnB,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAE,KAAK,cAAc,EAAE,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,EACZ,KAAK,gBAAgB,EACrB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,UAAU,EACf,KAAK,kBAAkB,EACvB,KAAK,aAAa,GACnB,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,EAChB,KAAK,aAAa,EAClB,KAAK,iBAAiB,EACtB,KAAK,uBAAuB,GAC7B,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAE,KAAK,eAAe,EAAE,KAAK,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/dist/index.js CHANGED
@@ -2,10 +2,10 @@
2
2
  // (docs/02-module-standard.md). The Next.js registry is a separate entry: `@softure-ai/core/next`.
3
3
  export { addCalendarDays, addCalendarMonths, calendarDaysBetween, calendarDaySchema, getCalendarDay, isCalendarDay, toCalendarDay, wholeMonthsBetween, } from "./calendar-day.js";
4
4
  export { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
5
- export { formatCalendarDay, formatCalendarMonth, formatDuration, formatMoney, formatPercent, isCalendarMonth, } from "./format.js";
5
+ export { DEFAULT_DURATION_MESSAGES, formatCalendarDay, formatCalendarMonth, formatDuration, formatMoney, formatPercent, isCalendarMonth, } from "./format.js";
6
6
  export { memoizeByContent } from "./memoize.js";
7
7
  export { createTestClock, systemClock } from "./clock.js";
8
- export { formatMessage, getMessage, isLocale, LOCALES, mergeMessages, selectPlural, } from "./i18n.js";
8
+ export { formatMessage, getMessage, DEFAULT_LOCALE, isLocale, mergeMessages, pickLocalizedText, selectMessages, selectPlural, } from "./i18n.js";
9
9
  export { coreMessages } from "./messages/index.js";
10
10
  export { err, ok } from "./result.js";
11
11
  export { errorLogLabel, getPublicMessage, isPublicError, PublicError, safeError } from "./safe-error.js";
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mGAAmG;AACnG,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,WAAW,EACX,aAAa,EACb,eAAe,GAKhB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAuD,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,WAAW,EAA8B,MAAM,YAAY,CAAC;AACtF,OAAO,EACL,aAAa,EACb,UAAU,EACV,QAAQ,EACR,OAAO,EACP,aAAa,EACb,YAAY,GAOb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAkD,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAsB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,yBAAyB,GAQ1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,GAUb,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GAIjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAuC,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAyC,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAmC,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,8EAA8E;AAC9E,mGAAmG;AACnG,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,EACnB,iBAAiB,EACjB,cAAc,EACd,aAAa,EACb,aAAa,EACb,kBAAkB,GAEnB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAClE,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,WAAW,EACX,aAAa,EACb,eAAe,GAOhB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,gBAAgB,EAAuD,MAAM,cAAc,CAAC;AACrG,OAAO,EAAE,eAAe,EAAE,WAAW,EAA8B,MAAM,YAAY,CAAC;AACtF,OAAO,EACL,aAAa,EACb,UAAU,EACV,cAAc,EACd,QAAQ,EACR,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,YAAY,GAQb,MAAM,WAAW,CAAC;AACnB,OAAO,EAAE,YAAY,EAAqB,MAAM,qBAAqB,CAAC;AACtE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAkD,MAAM,aAAa,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,gBAAgB,EAAE,aAAa,EAAE,WAAW,EAAE,SAAS,EAAsB,MAAM,iBAAiB,CAAC;AAC7H,OAAO,EACL,mBAAmB,EACnB,SAAS,EACT,yBAAyB,GAQ1B,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACjE,OAAO,EAAE,oBAAoB,EAAuB,MAAM,eAAe,CAAC;AAC1E,OAAO,EACL,YAAY,EACZ,oBAAoB,EACpB,YAAY,GAUb,MAAM,aAAa,CAAC;AACrB,OAAO,EACL,iBAAiB,EACjB,WAAW,EACX,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,gBAAgB,GAIjB,MAAM,cAAc,CAAC;AACtB,OAAO,EAAE,mBAAmB,EAAE,WAAW,EAAuC,MAAM,gBAAgB,CAAC;AACvG,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAyC,MAAM,eAAe,CAAC;AACpG,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,cAAc,EAAmC,MAAM,oBAAoB,CAAC"}
@@ -3,20 +3,5 @@ export declare const en: {
3
3
  database_failed: string;
4
4
  unexpected: string;
5
5
  };
6
- duration: {
7
- years: {
8
- one: string;
9
- few: string;
10
- many: string;
11
- other: string;
12
- };
13
- months: {
14
- one: string;
15
- few: string;
16
- many: string;
17
- other: string;
18
- };
19
- both: string;
20
- };
21
6
  };
22
7
  //# sourceMappingURL=en.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;;;;;;;;;;;;;;;;CAUd,CAAC"}
1
+ {"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;CAKd,CAAC"}
@@ -3,10 +3,5 @@ export const en = {
3
3
  database_failed: "Something went wrong while saving or loading data. Please try again.",
4
4
  unexpected: "Something went wrong. Please try again.",
5
5
  },
6
- duration: {
7
- years: { one: "{count} year", few: "{count} years", many: "{count} years", other: "{count} years" },
8
- months: { one: "{count} month", few: "{count} months", many: "{count} months", other: "{count} months" },
9
- both: "{years} and {months}",
10
- },
11
6
  };
12
7
  //# sourceMappingURL=en.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"en.js","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,MAAM,EAAE;QACN,eAAe,EAAE,sEAAsE;QACvF,UAAU,EAAE,yCAAyC;KACtD;IACD,QAAQ,EAAE;QACR,KAAK,EAAE,EAAE,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,eAAe,EAAE;QACnG,MAAM,EAAE,EAAE,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,gBAAgB,EAAE,IAAI,EAAE,gBAAgB,EAAE,KAAK,EAAE,gBAAgB,EAAE;QACxG,IAAI,EAAE,sBAAsB;KAC7B;CACF,CAAC"}
1
+ {"version":3,"file":"en.js","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,EAAE,GAAG;IAChB,MAAM,EAAE;QACN,eAAe,EAAE,sEAAsE;QACvF,UAAU,EAAE,yCAAyC;KACtD;CACF,CAAC"}
@@ -1,6 +1,6 @@
1
1
  import type { Dictionaries } from "../i18n.js";
2
2
  import { en } from "./en.js";
3
3
  export type CoreMessages = typeof en;
4
- /** Copy for core's own error codes (`core.<key>` → `errors.<key>`) and formatters (`duration`). */
4
+ /** Copy for core's own error codes (`core.<key>` → `errors.<key>`). */
5
5
  export declare const coreMessages: Dictionaries<CoreMessages>;
6
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAG7B,MAAM,MAAM,YAAY,GAAG,OAAO,EAAE,CAAC;AAErC,mGAAmG;AACnG,eAAO,MAAM,YAAY,EAAE,YAAY,CAAC,YAAY,CAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAE7B,MAAM,MAAM,YAAY,GAAG,OAAO,EAAE,CAAC;AAErC,uEAAuE;AACvE,eAAO,MAAM,YAAY,EAAE,YAAY,CAAC,YAAY,CAAU,CAAC"}
@@ -1,5 +1,4 @@
1
1
  import { en } from "./en.js";
2
- import { pl } from "./pl.js";
3
- /** Copy for core's own error codes (`core.<key>` → `errors.<key>`) and formatters (`duration`). */
4
- export const coreMessages = { en, pl };
2
+ /** Copy for core's own error codes (`core.<key>` → `errors.<key>`). */
3
+ export const coreMessages = { en };
5
4
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAC7B,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAI7B,mGAAmG;AACnG,MAAM,CAAC,MAAM,YAAY,GAA+B,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/messages/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAI7B,uEAAuE;AACvE,MAAM,CAAC,MAAM,YAAY,GAA+B,EAAE,EAAE,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@softure-ai/core",
3
- "version": "0.1.11",
3
+ "version": "0.2.0",
4
4
  "description": "The SOFTURE AI module contract: app config, module definitions, Result, Clock and messages.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/config.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import { z } from "zod";
4
4
  import { formatIssues, SoftureConfigError } from "./config-error.js";
5
5
  import { isDatabaseOptional } from "./database-requirement.js";
6
- import { LOCALES, type Locale } from "./i18n.js";
6
+ import { isLocale, type Locale } from "./i18n.js";
7
7
  import type { AnySoftureModule } from "./module.js";
8
8
  import { err, ok, type Result } from "./result.js";
9
9
  import { parseVersionRange, satisfiesRange } from "./version-range.js";
@@ -102,7 +102,7 @@ const configSchema = z.object({
102
102
  })
103
103
  .nullable()
104
104
  .default(null),
105
- locale: z.enum(LOCALES),
105
+ locale: z.string().refine(isLocale, "must be a BCP 47 language tag, e.g. en or de-AT"),
106
106
  timezone: z.string().refine(isTimeZone, "must be an IANA time zone, e.g. Europe/Warsaw"),
107
107
  appOrigin: z.string().refine(isOrigin, ORIGIN_MESSAGE),
108
108
  landingOrigin: z.string().refine(isOrigin, ORIGIN_MESSAGE).optional(),
package/src/format.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  // Days, money and percentages as people read them, one way for every module and app. A calendar day
2
- // is formatted at UTC midnight in UTC, so neither the server's nor the reader's zone moves it.
2
+ // is formatted at UTC midnight in UTC, so neither the server's nor the reader's zone moves it. Every
3
+ // formatter takes any BCP 47 locale tag (an invalid one throws Intl's RangeError); the only built-in
4
+ // copy is English, and an app passes its own words where a formatter writes any.
3
5
  import { isCalendarDay } from "./calendar-day.js";
4
6
  import { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
5
- import { formatMessage, selectPlural, type Locale } from "./i18n.js";
6
- import { coreMessages } from "./messages/index.js";
7
+ import { formatMessage, selectPlural, type PluralForms } from "./i18n.js";
7
8
 
8
- /** `long`: October 4, 2026 (pl names the month in the genitive); `medium`: Oct 4, 2026; `numeric`: 10/04/2026 (pl: 04.10.2026). */
9
+ /** `long`: October 4, 2026; `medium`: Oct 4, 2026; `numeric`: 10/04/2026 (each locale writes its own order and separators). */
9
10
  export type CalendarDayStyle = "long" | "medium" | "numeric";
10
11
 
11
12
  export interface FormatMoneyOptions {
@@ -17,21 +18,49 @@ export interface FormatMoneyOptions {
17
18
  readonly compact?: boolean;
18
19
  }
19
20
 
20
- /** `long`: October 2026 (pl: the month in the nominative); `genitive`: pl's genitive month, as inside a date; `roman`: X 2026. */
21
- export type CalendarMonthStyle = "long" | "genitive" | "roman";
21
+ /** `long`: October 2026; `roman`: X 2026. */
22
+ export type CalendarMonthStyle = "long" | "roman";
23
+
24
+ /** The words `formatDuration` writes: plural forms with `{count}`, and `both` joining `{years}` and `{months}`. */
25
+ export interface DurationMessages {
26
+ readonly years: PluralForms;
27
+ readonly months: PluralForms;
28
+ readonly both: string;
29
+ }
30
+
31
+ export interface FormatDurationOptions {
32
+ /** The BCP 47 tag whose plural rules pick a form (default `"en"`). */
33
+ readonly locale?: string;
34
+ /** The app's own words (default `DEFAULT_DURATION_MESSAGES`, English). */
35
+ readonly messages?: DurationMessages;
36
+ }
22
37
 
23
38
  export interface FormatPercentOptions {
24
39
  /** A `+` before a positive value (a `-` before a negative one is always there). Zero has neither in any mode. */
25
40
  readonly signed?: boolean;
26
41
  /** No space between the number and `%`, whatever the locale's pattern: `12,5%`, not `12,5 %`. */
27
42
  readonly tight?: boolean;
43
+ /**
44
+ * Exactly this many fraction digits, an integer from 0 to 20: `{ fractionDigits: 2 }` writes 1900 as `19.00%`.
45
+ * Without it a value has at most two fraction digits and no trailing zeros.
46
+ */
47
+ readonly fractionDigits?: number;
28
48
  }
29
49
 
30
50
  const MONTHS_PER_YEAR = 12;
31
51
  const MONTH_PATTERN = /^(\d{4})-(\d{2})$/;
32
52
  const ROMAN_MONTHS = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X", "XI", "XII"] as const;
53
+
54
+ /** The built-in, English words of `formatDuration`; any other category falls back to `other`. */
55
+ export const DEFAULT_DURATION_MESSAGES: DurationMessages = {
56
+ years: { one: "{count} year", other: "{count} years" },
57
+ months: { one: "{count} month", other: "{count} months" },
58
+ both: "{years} and {months}",
59
+ };
60
+
33
61
  const BASIS_POINTS_PER_UNIT = 10_000;
34
62
  const MAX_PERCENT_FRACTION_DIGITS = 2;
63
+ const FRACTION_DIGITS_LIMIT = 20;
35
64
 
36
65
  const dayFormatters = new Map<string, Intl.DateTimeFormat>();
37
66
  const numberFormatters = new Map<string, Intl.NumberFormat>();
@@ -43,7 +72,7 @@ const DAY_STYLE_OPTIONS: Readonly<Record<CalendarDayStyle, Intl.DateTimeFormatOp
43
72
  };
44
73
 
45
74
  /** A `YYYY-MM-DD` day as `locale` writes it. Throws a `RangeError` for a malformed day. */
46
- export function formatCalendarDay(day: string, locale: Locale, style: CalendarDayStyle = "long"): string {
75
+ export function formatCalendarDay(day: string, locale: string, style: CalendarDayStyle = "long"): string {
47
76
  if (!isCalendarDay(day)) throw new RangeError(`formatCalendarDay: "${String(day)}" is not a YYYY-MM-DD calendar day`);
48
77
  return getDayFormatter(`${locale}:${style}`, locale, DAY_STYLE_OPTIONS[style]).format(new Date(`${day}T00:00:00Z`));
49
78
  }
@@ -61,28 +90,26 @@ export function isCalendarMonth(value: unknown): value is string {
61
90
  * The month of a `YYYY-MM` month or a `YYYY-MM-DD` day, with its year, as `locale` writes it alone. Throws a
62
91
  * `RangeError` for anything else.
63
92
  */
64
- export function formatCalendarMonth(month: string, locale: Locale, style: CalendarMonthStyle = "long"): string {
93
+ export function formatCalendarMonth(month: string, locale: string, style: CalendarMonthStyle = "long"): string {
65
94
  const firstDay = isCalendarMonth(month) ? `${month}-01` : month;
66
95
  if (!isCalendarDay(firstDay)) throw new RangeError(`formatCalendarMonth: "${String(month)}" is not a YYYY-MM month or a YYYY-MM-DD day`);
67
96
  const date = new Date(`${firstDay}T00:00:00Z`);
68
97
  const year = firstDay.slice(0, 4);
69
98
  if (style === "roman") return `${ROMAN_MONTHS[date.getUTCMonth()] ?? ""} ${year}`;
70
- if (style === "long") return getDayFormatter(`${locale}:month`, locale, { month: "long", year: "numeric" }).format(date);
71
- // The genitive is the form a full date uses, so it is read from a day-month-year date's month part.
72
- const parts = getDayFormatter(`${locale}:day-month`, locale, { day: "numeric", month: "long", year: "numeric" }).formatToParts(date);
73
- const name = parts.find((part) => part.type === "month")?.value ?? "";
74
- return `${name} ${year}`;
99
+ return getDayFormatter(`${locale}:month`, locale, { month: "long", year: "numeric" }).format(date);
75
100
  }
76
101
 
77
102
  /**
78
- * Whole months as years and months in `locale`, with its plural forms: 38 is "3 years and 2 months"; a part that is
79
- * zero is left out, and zero itself is "0 months". Throws a `RangeError` for a negative or fractional count.
103
+ * Whole months as years and months: 38 is "3 years and 2 months"; a part that is zero is left out, and zero itself is
104
+ * "0 months". English by default; an app passes its own `messages` and the `locale` whose plural rules pick among
105
+ * them. Throws a `RangeError` for a negative or fractional count, or an invalid locale tag.
80
106
  */
81
- export function formatDuration(months: number, locale: Locale): string {
107
+ export function formatDuration(months: number, options: FormatDurationOptions = {}): string {
82
108
  if (!Number.isSafeInteger(months) || months < 0) {
83
109
  throw new RangeError(`formatDuration: ${String(months)} is not a whole, non-negative number of months`);
84
110
  }
85
- const copy = coreMessages[locale].duration;
111
+ const locale = options.locale ?? "en";
112
+ const copy = options.messages ?? DEFAULT_DURATION_MESSAGES;
86
113
  const years = Math.floor(months / MONTHS_PER_YEAR);
87
114
  const rest = months % MONTHS_PER_YEAR;
88
115
  const yearsText = formatMessage(selectPlural(locale, years, copy.years), { count: years });
@@ -94,12 +121,12 @@ export function formatDuration(months: number, locale: Locale): string {
94
121
 
95
122
  /**
96
123
  * An amount in the currency's minor unit (2900 PLN = PLN 29.00) in `locale`'s notation, thousands always
97
- * grouped (pl writes 1 234,56 where its default would leave four digits together), and zero is never signed (`-0`
124
+ * grouped (even where a locale's default would leave four digits together), and zero is never signed (`-0`
98
125
  * and an amount that rounds to zero show as zero). The minor unit is the pinned ISO
99
126
  * 4217 one (`CURRENCY_MINOR_UNIT_DIGITS`); a code outside the table gets `Intl`'s digits. Throws a
100
127
  * `RangeError` for an amount that is not a whole number of minor units, or a code `Intl` refuses.
101
128
  */
102
- export function formatMoney(minor: number, currency: string, locale: Locale, options: FormatMoneyOptions = {}): string {
129
+ export function formatMoney(minor: number, currency: string, locale: string, options: FormatMoneyOptions = {}): string {
103
130
  if (!Number.isSafeInteger(minor)) throw new RangeError(`formatMoney: ${String(minor)} is not a whole number of minor units`);
104
131
  const pinned = Object.hasOwn(CURRENCY_MINOR_UNIT_DIGITS, currency) ? CURRENCY_MINOR_UNIT_DIGITS[currency] : undefined;
105
132
  const digits = pinned ?? getNumberFormatter(locale, { style: "currency", currency }).resolvedOptions().maximumFractionDigits ?? 2;
@@ -115,14 +142,15 @@ export function formatMoney(minor: number, currency: string, locale: Locale, opt
115
142
  }
116
143
 
117
144
  /**
118
- * Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits, and zero is never signed (`-0`
119
- * and a value that rounds to zero show as `0%`). Throws a `RangeError` for a non-finite value.
145
+ * Basis points as a percentage in `locale`: 1250 is 12.5%, at most two fraction digits unless `fractionDigits` fixes
146
+ * them, and zero is never signed (`-0` and a value that rounds to zero show as `0%`, or `0.0%` at one digit). Throws a
147
+ * `RangeError` for a non-finite value or an invalid `fractionDigits`.
120
148
  */
121
- export function formatPercent(basisPoints: number, locale: Locale, options: FormatPercentOptions = {}): string {
149
+ export function formatPercent(basisPoints: number, locale: string, options: FormatPercentOptions = {}): string {
122
150
  if (!Number.isFinite(basisPoints)) throw new RangeError(`formatPercent: ${String(basisPoints)} is not a finite number of basis points`);
123
151
  const format = getNumberFormatter(locale, {
124
152
  style: "percent",
125
- maximumFractionDigits: MAX_PERCENT_FRACTION_DIGITS,
153
+ ...getPercentFractionDigits(options.fractionDigits),
126
154
  signDisplay: options.signed === true ? "exceptZero" : "negative",
127
155
  });
128
156
  const value = basisPoints / BASIS_POINTS_PER_UNIT;
@@ -134,11 +162,19 @@ export function formatPercent(basisPoints: number, locale: Locale, options: Form
134
162
  .join("");
135
163
  }
136
164
 
165
+ function getPercentFractionDigits(fractionDigits: number | undefined): Intl.NumberFormatOptions {
166
+ if (fractionDigits === undefined) return { maximumFractionDigits: MAX_PERCENT_FRACTION_DIGITS };
167
+ if (!Number.isInteger(fractionDigits) || fractionDigits < 0 || fractionDigits > FRACTION_DIGITS_LIMIT) {
168
+ throw new RangeError(`formatPercent: fractionDigits ${String(fractionDigits)} is not an integer from 0 to ${String(FRACTION_DIGITS_LIMIT)}`);
169
+ }
170
+ return { minimumFractionDigits: fractionDigits, maximumFractionDigits: fractionDigits };
171
+ }
172
+
137
173
  function isNextToPercentSign(parts: readonly Intl.NumberFormatPart[], index: number): boolean {
138
174
  return parts[index - 1]?.type === "percentSign" || parts[index + 1]?.type === "percentSign";
139
175
  }
140
176
 
141
- function getDayFormatter(key: string, locale: Locale, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
177
+ function getDayFormatter(key: string, locale: string, options: Intl.DateTimeFormatOptions): Intl.DateTimeFormat {
142
178
  let format = dayFormatters.get(key);
143
179
  if (format === undefined) {
144
180
  format = new Intl.DateTimeFormat(locale, { ...options, timeZone: "UTC" });
@@ -147,7 +183,7 @@ function getDayFormatter(key: string, locale: Locale, options: Intl.DateTimeForm
147
183
  return format;
148
184
  }
149
185
 
150
- function getNumberFormatter(locale: Locale, options: Intl.NumberFormatOptions): Intl.NumberFormat {
186
+ function getNumberFormatter(locale: string, options: Intl.NumberFormatOptions): Intl.NumberFormat {
151
187
  const key = `${locale}:${JSON.stringify(options)}`;
152
188
  let format = numberFormatters.get(key);
153
189
  if (format === undefined) {
package/src/i18n.ts CHANGED
@@ -1,9 +1,12 @@
1
- // Copy and localization (docs/02-module-standard.md §6): every module ships complete `en` and
2
- // `pl` dictionaries, and the app passes partial overrides per locale.
1
+ // Copy and localization (docs/02-module-standard.md §6): every module ships a complete `en`
2
+ // dictionary, the only built-in language. An app that shows another language passes its own
3
+ // translation as overrides for its locale; a key it leaves out reads English.
3
4
 
4
- export const LOCALES = ["en", "pl"] as const;
5
+ /** A BCP 47 language tag (`en`, `de-AT`). */
6
+ export type Locale = string;
5
7
 
6
- export type Locale = (typeof LOCALES)[number];
8
+ /** The language of every built-in dictionary and the fallback for any other locale. */
9
+ export const DEFAULT_LOCALE = "en";
7
10
 
8
11
  /** A dictionary: nested groups of message strings. */
9
12
  export interface MessageTree {
@@ -13,11 +16,14 @@ export interface MessageTree {
13
16
  /** The same shape with every key optional, at every depth. */
14
17
  export type DeepPartial<T> = { [K in keyof T]?: T[K] extends string ? string : DeepPartial<T[K]> };
15
18
 
16
- /** One complete dictionary per locale. */
17
- export type Dictionaries<T extends MessageTree> = Readonly<Record<Locale, T>>;
19
+ /** A complete `en` dictionary, plus complete dictionaries for any other locales. */
20
+ export type Dictionaries<T extends MessageTree> = { readonly en: T } & { readonly [locale: string]: T | undefined };
18
21
 
19
- /** Partial overrides an app passes for some locales. */
20
- export type MessageOverrides<T extends MessageTree> = Partial<Record<Locale, DeepPartial<T>>>;
22
+ /** Partial overrides an app passes per locale: a translation for its own locale, or changed English copy. */
23
+ export type MessageOverrides<T extends MessageTree> = { readonly [locale: string]: DeepPartial<T> | undefined };
24
+
25
+ /** A text an app writes per locale in its options (a plan name, a label); `en` is the fallback. */
26
+ export type LocalizedText = { readonly [locale: string]: string | undefined };
21
27
 
22
28
  /** Plural forms named after the `Intl.PluralRules` categories; `other` is the fallback. */
23
29
  export interface PluralForms {
@@ -29,24 +35,49 @@ export interface PluralForms {
29
35
  readonly other: string;
30
36
  }
31
37
 
38
+ /** Whether `value` is a well-formed BCP 47 language tag. */
32
39
  export function isLocale(value: unknown): value is Locale {
33
- return LOCALES.some((locale) => locale === value);
40
+ if (typeof value !== "string" || value === "") return false;
41
+ try {
42
+ return Intl.getCanonicalLocales(value).length === 1;
43
+ } catch {
44
+ // RangeError: not a well-formed tag.
45
+ return false;
46
+ }
34
47
  }
35
48
 
36
49
  /**
37
- * The defaults with the overrides applied, per locale. Only keys the defaults have are taken,
38
- * and a group is never replaced by a string, so an override cannot break the dictionary shape.
39
- * Inputs are not mutated.
50
+ * The defaults with the overrides applied, per locale: `en` and every locale the defaults or the
51
+ * overrides name. A locale without its own defaults is built on `en`, so a key its override leaves
52
+ * out reads English. Only keys the defaults have are taken, and a group is never replaced by a
53
+ * string, so an override cannot break the dictionary shape. Inputs are not mutated.
40
54
  */
41
55
  export function mergeMessages<T extends MessageTree>(
42
56
  defaults: Dictionaries<T>,
43
57
  overrides: MessageOverrides<T> = {},
44
58
  ): Dictionaries<T> {
45
- const merged = {} as Record<Locale, T>;
46
- for (const locale of LOCALES) {
47
- merged[locale] = mergeTree(defaults[locale], overrides[locale]) as T;
59
+ const merged: Record<string, T> = {};
60
+ for (const locale of new Set([DEFAULT_LOCALE, ...Object.keys(defaults), ...Object.keys(overrides)])) {
61
+ merged[locale] = mergeTree(defaults[locale] ?? defaults.en, overrides[locale]) as T;
48
62
  }
49
- return merged;
63
+ return merged as Dictionaries<T>;
64
+ }
65
+
66
+ /**
67
+ * The dictionary for `locale`: its own, else its language's (`pt-BR` reads `pt`), else `en`.
68
+ */
69
+ export function selectMessages<T extends MessageTree>(dictionaries: Dictionaries<T>, locale: Locale): T {
70
+ return dictionaries[locale] ?? dictionaries[getLanguage(locale)] ?? dictionaries.en;
71
+ }
72
+
73
+ /** The text for `locale` with `selectMessages`' fallback order, or `fallback` when no locale has one. */
74
+ export function pickLocalizedText(text: LocalizedText | undefined, locale: Locale, fallback = ""): string {
75
+ if (text === undefined) return fallback;
76
+ return text[locale] ?? text[getLanguage(locale)] ?? text.en ?? fallback;
77
+ }
78
+
79
+ function getLanguage(locale: Locale): string {
80
+ return locale.split("-")[0] ?? locale;
50
81
  }
51
82
 
52
83
  /** Fills `{name}` placeholders. A placeholder without a value stays as written. */
@@ -57,10 +88,13 @@ export function formatMessage(template: string, params: Readonly<Record<string,
57
88
  });
58
89
  }
59
90
 
60
- const pluralRulesByLocale = new Map<Locale, Intl.PluralRules>();
91
+ const pluralRulesByLocale = new Map<string, Intl.PluralRules>();
61
92
 
62
- /** The form for `count` under the plural rules of `locale` ("1 file", "2 files"). */
63
- export function selectPlural(locale: Locale, count: number, forms: PluralForms): string {
93
+ /**
94
+ * The form for `count` under the plural rules of `locale`, any BCP 47 tag ("1 file", "2 files"). A category the
95
+ * forms leave out falls back to `other`.
96
+ */
97
+ export function selectPlural(locale: string, count: number, forms: PluralForms): string {
64
98
  let rules = pluralRulesByLocale.get(locale);
65
99
  if (rules === undefined) {
66
100
  rules = new Intl.PluralRules(locale);
package/src/index.ts CHANGED
@@ -13,6 +13,7 @@ export {
13
13
  } from "./calendar-day.js";
14
14
  export { CURRENCY_MINOR_UNIT_DIGITS } from "./currency-digits.js";
15
15
  export {
16
+ DEFAULT_DURATION_MESSAGES,
16
17
  formatCalendarDay,
17
18
  formatCalendarMonth,
18
19
  formatDuration,
@@ -21,6 +22,8 @@ export {
21
22
  isCalendarMonth,
22
23
  type CalendarDayStyle,
23
24
  type CalendarMonthStyle,
25
+ type DurationMessages,
26
+ type FormatDurationOptions,
24
27
  type FormatMoneyOptions,
25
28
  type FormatPercentOptions,
26
29
  } from "./format.js";
@@ -29,13 +32,16 @@ export { createTestClock, systemClock, type Clock, type TestClock } from "./cloc
29
32
  export {
30
33
  formatMessage,
31
34
  getMessage,
35
+ DEFAULT_LOCALE,
32
36
  isLocale,
33
- LOCALES,
34
37
  mergeMessages,
38
+ pickLocalizedText,
39
+ selectMessages,
35
40
  selectPlural,
36
41
  type DeepPartial,
37
42
  type Dictionaries,
38
43
  type Locale,
44
+ type LocalizedText,
39
45
  type MessageOverrides,
40
46
  type MessageTree,
41
47
  type PluralForms,
@@ -3,9 +3,4 @@ export const en = {
3
3
  database_failed: "Something went wrong while saving or loading data. Please try again.",
4
4
  unexpected: "Something went wrong. Please try again.",
5
5
  },
6
- duration: {
7
- years: { one: "{count} year", few: "{count} years", many: "{count} years", other: "{count} years" },
8
- months: { one: "{count} month", few: "{count} months", many: "{count} months", other: "{count} months" },
9
- both: "{years} and {months}",
10
- },
11
6
  };
@@ -1,8 +1,7 @@
1
1
  import type { Dictionaries } from "../i18n.js";
2
2
  import { en } from "./en.js";
3
- import { pl } from "./pl.js";
4
3
 
5
4
  export type CoreMessages = typeof en;
6
5
 
7
- /** Copy for core's own error codes (`core.<key>` → `errors.<key>`) and formatters (`duration`). */
8
- export const coreMessages: Dictionaries<CoreMessages> = { en, pl };
6
+ /** Copy for core's own error codes (`core.<key>` → `errors.<key>`). */
7
+ export const coreMessages: Dictionaries<CoreMessages> = { en };
@@ -1,3 +0,0 @@
1
- import type { en } from "./en.js";
2
- export declare const pl: typeof en;
3
- //# sourceMappingURL=pl.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pl.d.ts","sourceRoot":"","sources":["../../src/messages/pl.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,SAAS,CAAC;AAElC,eAAO,MAAM,EAAE,EAAE,OAAO,EAUvB,CAAC"}
@@ -1,12 +0,0 @@
1
- export const pl = {
2
- errors: {
3
- database_failed: "Coś poszło nie tak przy zapisie lub odczycie danych. Spróbuj ponownie.",
4
- unexpected: "Coś poszło nie tak. Spróbuj ponownie.",
5
- },
6
- duration: {
7
- years: { one: "{count} rok", few: "{count} lata", many: "{count} lat", other: "{count} roku" },
8
- months: { one: "{count} miesiąc", few: "{count} miesiące", many: "{count} miesięcy", other: "{count} miesiąca" },
9
- both: "{years} i {months}",
10
- },
11
- };
12
- //# sourceMappingURL=pl.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"pl.js","sourceRoot":"","sources":["../../src/messages/pl.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,EAAE,GAAc;IAC3B,MAAM,EAAE;QACN,eAAe,EAAE,wEAAwE;QACzF,UAAU,EAAE,uCAAuC;KACpD;IACD,QAAQ,EAAE;QACR,KAAK,EAAE,EAAE,GAAG,EAAE,aAAa,EAAE,GAAG,EAAE,cAAc,EAAE,IAAI,EAAE,aAAa,EAAE,KAAK,EAAE,cAAc,EAAE;QAC9F,MAAM,EAAE,EAAE,GAAG,EAAE,iBAAiB,EAAE,GAAG,EAAE,kBAAkB,EAAE,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,kBAAkB,EAAE;QAChH,IAAI,EAAE,oBAAoB;KAC3B;CACF,CAAC"}
@@ -1,13 +0,0 @@
1
- import type { en } from "./en.js";
2
-
3
- export const pl: typeof en = {
4
- errors: {
5
- database_failed: "Coś poszło nie tak przy zapisie lub odczycie danych. Spróbuj ponownie.",
6
- unexpected: "Coś poszło nie tak. Spróbuj ponownie.",
7
- },
8
- duration: {
9
- years: { one: "{count} rok", few: "{count} lata", many: "{count} lat", other: "{count} roku" },
10
- months: { one: "{count} miesiąc", few: "{count} miesiące", many: "{count} miesięcy", other: "{count} miesiąca" },
11
- both: "{years} i {months}",
12
- },
13
- };