@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 +25 -0
- package/README.md +53 -18
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -2
- package/dist/config.js.map +1 -1
- package/dist/format.d.ts +35 -14
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +30 -16
- package/dist/format.js.map +1 -1
- package/dist/i18n.d.ts +34 -11
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +40 -10
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/messages/en.d.ts +0 -15
- package/dist/messages/en.d.ts.map +1 -1
- package/dist/messages/en.js +0 -5
- package/dist/messages/en.js.map +1 -1
- package/dist/messages/index.d.ts +1 -1
- package/dist/messages/index.d.ts.map +1 -1
- package/dist/messages/index.js +2 -3
- package/dist/messages/index.js.map +1 -1
- package/package.json +1 -1
- package/src/config.ts +2 -2
- package/src/format.ts +61 -25
- package/src/i18n.ts +53 -19
- package/src/index.ts +7 -1
- package/src/messages/en.ts +0 -5
- package/src/messages/index.ts +2 -3
- package/dist/messages/pl.d.ts +0 -3
- package/dist/messages/pl.d.ts.map +0 -1
- package/dist/messages/pl.js +0 -12
- package/dist/messages/pl.js.map +0 -1
- package/src/messages/pl.ts +0 -13
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`,
|
|
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"
|
|
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: "
|
|
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
|
-
-
|
|
253
|
-
`
|
|
254
|
-
-
|
|
255
|
-
|
|
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";
|
|
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
|
-
|
|
330
|
-
|
|
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
|
-
|
|
334
|
-
|
|
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
|
|
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
|
|
342
|
-
|
|
343
|
-
|
|
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`;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAKA,OAAO,
|
|
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 {
|
|
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.
|
|
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(),
|
package/dist/config.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG,gEAAgE;AAChE,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AACrE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,
|
|
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
|
|
2
|
-
/** `long`: October 4, 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
|
|
13
|
-
export type CalendarMonthStyle = "long" | "
|
|
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:
|
|
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:
|
|
47
|
+
export declare function formatCalendarMonth(month: string, locale: string, style?: CalendarMonthStyle): string;
|
|
29
48
|
/**
|
|
30
|
-
* Whole months as years and months
|
|
31
|
-
*
|
|
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,
|
|
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 (
|
|
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:
|
|
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
|
|
44
|
-
* and a value that rounds to zero show as `0%`). Throws a
|
|
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:
|
|
67
|
+
export declare function formatPercent(basisPoints: number, locale: string, options?: FormatPercentOptions): string;
|
|
47
68
|
//# sourceMappingURL=format.d.ts.map
|
package/dist/format.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
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
|
|
56
|
-
*
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
|
97
|
-
* and a value that rounds to zero show as `0%`). Throws a
|
|
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
|
-
|
|
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
|
}
|
package/dist/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA,oGAAoG;AACpG
|
|
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
|
-
|
|
2
|
-
export type Locale =
|
|
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
|
-
/**
|
|
12
|
-
export type Dictionaries<T extends MessageTree> =
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
27
|
-
*
|
|
28
|
-
*
|
|
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
|
-
/**
|
|
34
|
-
|
|
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
|
package/dist/i18n.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"i18n.d.ts","sourceRoot":"","sources":["../src/i18n.ts"],"names":[],"mappings":"
|
|
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`
|
|
2
|
-
//
|
|
3
|
-
|
|
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
|
-
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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
|
-
/**
|
|
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":"AAAA,
|
|
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,
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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,
|
|
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,
|
|
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,
|
|
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"}
|
package/dist/messages/en.d.ts
CHANGED
|
@@ -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
|
|
1
|
+
{"version":3,"file":"en.d.ts","sourceRoot":"","sources":["../../src/messages/en.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,EAAE;;;;;CAKd,CAAC"}
|
package/dist/messages/en.js
CHANGED
|
@@ -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
|
package/dist/messages/en.js.map
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/messages/index.d.ts
CHANGED
|
@@ -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>`)
|
|
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;
|
|
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"}
|
package/dist/messages/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { en } from "./en.js";
|
|
2
|
-
|
|
3
|
-
|
|
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;
|
|
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
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 {
|
|
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.
|
|
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
|
|
6
|
-
import { coreMessages } from "./messages/index.js";
|
|
7
|
+
import { formatMessage, selectPlural, type PluralForms } from "./i18n.js";
|
|
7
8
|
|
|
8
|
-
/** `long`: October 4, 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
|
|
21
|
-
export type CalendarMonthStyle = "long" | "
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
79
|
-
*
|
|
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,
|
|
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
|
|
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 (
|
|
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:
|
|
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
|
|
119
|
-
* and a value that rounds to zero show as `0%`). Throws a
|
|
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:
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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`
|
|
2
|
-
//
|
|
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
|
-
|
|
5
|
+
/** A BCP 47 language tag (`en`, `de-AT`). */
|
|
6
|
+
export type Locale = string;
|
|
5
7
|
|
|
6
|
-
|
|
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
|
-
/**
|
|
17
|
-
export type Dictionaries<T extends MessageTree> =
|
|
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
|
|
20
|
-
export type MessageOverrides<T extends MessageTree> =
|
|
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
|
-
|
|
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
|
|
38
|
-
*
|
|
39
|
-
*
|
|
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
|
|
46
|
-
for (const locale of
|
|
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<
|
|
91
|
+
const pluralRulesByLocale = new Map<string, Intl.PluralRules>();
|
|
61
92
|
|
|
62
|
-
/**
|
|
63
|
-
|
|
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,
|
package/src/messages/en.ts
CHANGED
|
@@ -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
|
};
|
package/src/messages/index.ts
CHANGED
|
@@ -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>`)
|
|
8
|
-
export const coreMessages: Dictionaries<CoreMessages> = { en
|
|
6
|
+
/** Copy for core's own error codes (`core.<key>` → `errors.<key>`). */
|
|
7
|
+
export const coreMessages: Dictionaries<CoreMessages> = { en };
|
package/dist/messages/pl.d.ts
DELETED
|
@@ -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"}
|
package/dist/messages/pl.js
DELETED
|
@@ -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
|
package/dist/messages/pl.js.map
DELETED
|
@@ -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"}
|
package/src/messages/pl.ts
DELETED
|
@@ -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
|
-
};
|