@pithy-sh/i18n 0.1.2 → 0.1.3

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.
Files changed (62) hide show
  1. package/dist/adapters/adapters.d.ts +47 -0
  2. package/dist/adapters/adapters.d.ts.map +1 -0
  3. package/dist/adapters/adapters.js +87 -0
  4. package/dist/browser/document.d.ts +36 -0
  5. package/dist/browser/document.d.ts.map +1 -0
  6. package/dist/browser/document.js +85 -0
  7. package/dist/browser/signals.d.ts +36 -0
  8. package/dist/browser/signals.d.ts.map +1 -0
  9. package/dist/browser/signals.js +70 -0
  10. package/dist/capability.d.ts +53 -0
  11. package/dist/capability.d.ts.map +1 -0
  12. package/dist/capability.js +110 -0
  13. package/dist/catalogs/browser.d.ts +13 -0
  14. package/dist/catalogs/browser.d.ts.map +1 -0
  15. package/dist/catalogs/browser.js +33 -0
  16. package/dist/catalogs/es/errors.d.ts +30 -0
  17. package/dist/catalogs/es/errors.d.ts.map +1 -0
  18. package/dist/catalogs/es/errors.js +154 -0
  19. package/dist/catalogs/es/index.d.ts +23 -0
  20. package/dist/catalogs/es/index.d.ts.map +1 -0
  21. package/dist/catalogs/es/index.js +29 -0
  22. package/dist/catalogs/es/screens.d.ts +28 -0
  23. package/dist/catalogs/es/screens.d.ts.map +1 -0
  24. package/dist/catalogs/es/screens.js +105 -0
  25. package/dist/catalogs/kit.d.ts +21 -0
  26. package/dist/catalogs/kit.d.ts.map +1 -0
  27. package/dist/catalogs/kit.js +23 -0
  28. package/dist/client/projection.d.ts +47 -0
  29. package/dist/client/projection.d.ts.map +1 -0
  30. package/dist/client/projection.js +0 -0
  31. package/dist/config/config.d.ts +71 -0
  32. package/dist/config/config.d.ts.map +1 -0
  33. package/dist/config/config.js +103 -0
  34. package/dist/http/middleware.d.ts +36 -0
  35. package/dist/http/middleware.d.ts.map +1 -0
  36. package/dist/http/middleware.js +93 -0
  37. package/dist/index.d.ts +17 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +3 -0
  40. package/dist/react/translator.d.ts +73 -0
  41. package/dist/react/translator.d.ts.map +1 -0
  42. package/dist/react/translator.js +81 -0
  43. package/dist/react/useNegotiatedLocale.d.ts +67 -0
  44. package/dist/react/useNegotiatedLocale.d.ts.map +1 -0
  45. package/dist/react/useNegotiatedLocale.js +108 -0
  46. package/dist/resolve/browser.d.ts +69 -0
  47. package/dist/resolve/browser.d.ts.map +1 -0
  48. package/dist/resolve/browser.js +62 -0
  49. package/dist/resolve/chain.d.ts +52 -0
  50. package/dist/resolve/chain.d.ts.map +1 -0
  51. package/dist/resolve/chain.js +49 -0
  52. package/dist/resolve/server.d.ts +31 -0
  53. package/dist/resolve/server.d.ts.map +1 -0
  54. package/dist/resolve/server.js +32 -0
  55. package/dist/settings/coverage.d.ts +21 -0
  56. package/dist/settings/coverage.d.ts.map +1 -0
  57. package/dist/settings/coverage.js +48 -0
  58. package/dist/version.generated.d.ts +5 -0
  59. package/dist/version.generated.d.ts.map +1 -0
  60. package/dist/version.generated.js +7 -0
  61. package/package.json +18 -10
  62. package/src/version.generated.ts +1 -1
@@ -0,0 +1,69 @@
1
+ import type { BrowserResolver } from "../config/config";
2
+ import { type LocaleSet, type ResolvedLocale } from "./chain";
3
+ /**
4
+ * What the browser side of this package reads: the languages, the chain order, and the two names a page
5
+ * looks itself up by.
6
+ *
7
+ * **The shape `I18nClientProjection` and `I18nConfig` both already are.** The browser holds the
8
+ * projection — locale metadata and nothing else, no catalogs, no cookie name, no server chain — so
9
+ * asking a page for an `I18nConfig` asks it for three fields it has no business knowing, and every
10
+ * adopter widens one into the other the same five ways. Declaring what is actually read costs nothing
11
+ * on the server, where the resolved config satisfies it as it stands.
12
+ *
13
+ * `browserResolvers` is `readonly string[]` for the same reason the projection's is: the ambient
14
+ * declaration `pithy ui add react` copies into an adopter's Worker cannot name a type it does not
15
+ * import. The chain is walked by name — see {@link recognizedResolvers}.
16
+ */
17
+ export interface BrowserChain extends LocaleSet {
18
+ /** The query parameter an explicit choice arrives on — `?lang=es`. */
19
+ readonly queryParam: string;
20
+ /** The `localStorage` key this device's remembered locale is written under. */
21
+ readonly storageKey: string;
22
+ /** The browser chain, in the order it is asked. Names this build does not know contribute nothing. */
23
+ readonly browserResolvers: readonly string[];
24
+ }
25
+ /**
26
+ * The links of `names` this build recognizes, in the order given, dropping the rest.
27
+ *
28
+ * The browser walks its chain by name because that is how the projection carries it, so a link this
29
+ * build has never heard of — a project one release ahead of the SPA bundle it is serving — contributes
30
+ * nothing rather than throwing. The links around it are still asked, each in its own place.
31
+ */
32
+ export declare function recognizedResolvers(names: readonly string[]): BrowserResolver[];
33
+ /**
34
+ * What a browser knows about the reader's language.
35
+ *
36
+ * A separate chain from the server's, because half the server's links do not exist here and half of
37
+ * these do not exist there: `localStorage` is absent from a Worker, and `navigator.language` inside
38
+ * workerd is the constant `"en"`, carrying no request information at all.
39
+ */
40
+ export interface BrowserLocaleSignals {
41
+ /** An explicit choice on the URL — `?lang=es`. */
42
+ readonly query?: string | null;
43
+ /** The signed-in reader's own preference, as the app already holds it. */
44
+ readonly account?: string | null;
45
+ /** What this device remembers, from `localStorage`. */
46
+ readonly storage?: string | null;
47
+ /**
48
+ * The reader's own browser languages, most-wanted first, from `navigator.languages`.
49
+ *
50
+ * The browser's equivalent of `Accept-Language`, and the only link that answers for a first-time
51
+ * visitor who has chosen nothing and is signed in to nothing. A list rather than one tag, because
52
+ * `navigator.languages` is one and the whole of it is a preference order worth honoring.
53
+ */
54
+ readonly navigator?: readonly string[] | null;
55
+ /** What the server negotiated for the document, off `<html lang>`. */
56
+ readonly server?: string | null;
57
+ }
58
+ /**
59
+ * The reader's locale, by the configured browser chain.
60
+ *
61
+ * Default order: `?lang=`, the account, local storage, the server's answer, the project default.
62
+ *
63
+ * **The account outranks the device.** `pithy_auth_users.locale` is where a person's locale lives, so a
64
+ * signed-in reader who picks a language on one device must not silently be reading another language on
65
+ * the next. A `?lang=` choice by a signed-in reader is written through to their account rather than
66
+ * only to `localStorage`, which is what keeps the fact in one home; `docs/I18N.md` states it once.
67
+ */
68
+ export declare function resolveBrowserLocale(signals: BrowserLocaleSignals, config: BrowserChain): ResolvedLocale;
69
+ //# sourceMappingURL=browser.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/resolve/browser.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACxD,OAAO,EAAE,KAAK,SAAS,EAAE,KAAK,cAAc,EAA4C,MAAM,SAAS,CAAC;AAExG;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,sEAAsE;IACtE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,+EAA+E;IAC/E,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,sGAAsG;IACtG,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9C;AAuBD;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,GAAG,eAAe,EAAE,CAE/E;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,0EAA0E;IAC1E,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,uDAAuD;IACvD,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC;;;;;;OAMG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,GAAG,IAAI,CAAC;IAC9C,sEAAsE;IACtE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,oBAAoB,EAAE,MAAM,EAAE,YAAY,GAAG,cAAc,CAIxG"}
@@ -0,0 +1,62 @@
1
+ import { resolveChain, tagLink } from "./chain.js";
2
+ //#region src/resolve/browser.ts
3
+ /**
4
+ * Every link this build knows, as a record rather than a list.
5
+ *
6
+ * **A record so a new `BrowserResolver` cannot be forgotten here.** Adding a member to the enum leaves
7
+ * this object missing a property, which is a red build — the same guarantee the exhaustive switch in
8
+ * {@link linkFor} gives, in the one place a `string[]` has to be turned back into enum members.
9
+ *
10
+ * Written out rather than read off `BrowserResolver.options`, and that is a bundle decision, not a
11
+ * preference. `config/config.ts` is a Zod module, and everything under `src/browser/**` and
12
+ * `src/react/**` imports the config type-only precisely so that no scaffolded SPA ships Zod to walk a
13
+ * chain of six names. A value import here would put it in every adopter's main chunk.
14
+ */
15
+ const KNOWN_RESOLVERS = {
16
+ query: true,
17
+ account: true,
18
+ storage: true,
19
+ navigator: true,
20
+ server: true,
21
+ default: true
22
+ };
23
+ /**
24
+ * The links of `names` this build recognizes, in the order given, dropping the rest.
25
+ *
26
+ * The browser walks its chain by name because that is how the projection carries it, so a link this
27
+ * build has never heard of — a project one release ahead of the SPA bundle it is serving — contributes
28
+ * nothing rather than throwing. The links around it are still asked, each in its own place.
29
+ */
30
+ function recognizedResolvers(names) {
31
+ return names.filter((name) => Object.hasOwn(KNOWN_RESOLVERS, name));
32
+ }
33
+ /**
34
+ * The reader's locale, by the configured browser chain.
35
+ *
36
+ * Default order: `?lang=`, the account, local storage, the server's answer, the project default.
37
+ *
38
+ * **The account outranks the device.** `pithy_auth_users.locale` is where a person's locale lives, so a
39
+ * signed-in reader who picks a language on one device must not silently be reading another language on
40
+ * the next. A `?lang=` choice by a signed-in reader is written through to their account rather than
41
+ * only to `localStorage`, which is what keeps the fact in one home; `docs/I18N.md` states it once.
42
+ */
43
+ function resolveBrowserLocale(signals, config) {
44
+ const links = recognizedResolvers(config.browserResolvers).map((resolver) => linkFor(resolver, signals, config));
45
+ return resolveChain(links, config);
46
+ }
47
+ /** One link, by name. A standalone function so the switch stays exhaustive under `verbatimModuleSyntax`. */
48
+ function linkFor(resolver, signals, config) {
49
+ switch (resolver) {
50
+ case "query": return tagLink(resolver, signals.query);
51
+ case "account": return tagLink(resolver, signals.account);
52
+ case "storage": return tagLink(resolver, signals.storage);
53
+ case "navigator": return {
54
+ name: resolver,
55
+ ranges: (signals.navigator ?? []).filter((tag) => tag.trim().length > 0)
56
+ };
57
+ case "server": return tagLink(resolver, signals.server);
58
+ case "default": return tagLink(resolver, config.defaultLocale);
59
+ }
60
+ }
61
+ //#endregion
62
+ export { recognizedResolvers, resolveBrowserLocale };
@@ -0,0 +1,52 @@
1
+ import type { LocaleContext } from "@pithy-sh/core/src/i18n/locale";
2
+ import type { LocaleExceptions } from "@pithy-sh/core/src/i18n/match";
3
+ /**
4
+ * The languages a chain may land on: what it matches against, what it falls back to, and the pairs no
5
+ * truncation of a tag would reach.
6
+ *
7
+ * **Three fields rather than `I18nConfig`, because a browser holds neither the catalogs nor the cookie
8
+ * name nor the server chain.** `virtual:pithy/i18n` projects locale metadata and nothing else, and this
9
+ * is the part of it a match is made from — so the projection satisfies this shape as it stands and the
10
+ * resolved config satisfies it too. Structural on purpose: one type both sides already are beats a
11
+ * conversion every caller has to work out for itself.
12
+ */
13
+ export interface LocaleSet {
14
+ /** Every locale this project serves. A chain answers with one of these or with nothing. */
15
+ readonly supportedLocales: readonly string[];
16
+ /** The locale served when no link answers. Always one of `supportedLocales`. */
17
+ readonly defaultLocale: string;
18
+ /** Language ranges the matcher cannot derive, as range → supported locale. Usually empty. */
19
+ readonly exceptions: LocaleExceptions;
20
+ }
21
+ /** One link of a resolver chain: where the ranges came from, and what they were. */
22
+ export interface ResolverLink {
23
+ /** The link's name, as it appears in `serverResolvers` / `browserResolvers`. */
24
+ readonly name: string;
25
+ /** The language ranges this link offers, most-wanted first. Empty when the link has nothing to say. */
26
+ readonly ranges: readonly string[];
27
+ }
28
+ /** A resolved locale, plus which link answered — so `pithy doctor` and a log can say why. */
29
+ export interface ResolvedLocale extends LocaleContext {
30
+ /** The chain link that answered, or `"default"` when the chain fell through to the project default. */
31
+ readonly resolvedBy: string;
32
+ }
33
+ /**
34
+ * A single tag as a one-element range list, or nothing when there is no tag.
35
+ *
36
+ * The links that read a stored or supplied value all have this shape. A blank string is nothing, not a
37
+ * range: a cookie the browser cleared and a cookie that was never set mean the same thing here.
38
+ */
39
+ export declare function tagLink(name: string, tag: string | null | undefined): ResolverLink;
40
+ /**
41
+ * Walk `links` in order and take the first that matches a supported locale.
42
+ *
43
+ * **The two locales are decided here, and only one of them falls back.** The catalog locale is what
44
+ * matched — the words the kit actually has. The formatting locale is the *range the reader sent*, kept
45
+ * whole when it is a tag `Intl` accepts: an `es-AR` visitor reads the `es` catalog and formats as
46
+ * `es-AR`, which `Intl` supports natively whether or not anyone wrote a string for it.
47
+ *
48
+ * Total. Every construction on the way through is guarded, so a chain fed nothing but malformed input
49
+ * lands on the project default rather than throwing.
50
+ */
51
+ export declare function resolveChain(links: readonly ResolverLink[], locales: LocaleSet): ResolvedLocale;
52
+ //# sourceMappingURL=chain.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain.d.ts","sourceRoot":"","sources":["../../src/resolve/chain.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gCAAgC,CAAC;AAEpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AAGtE;;;;;;;;;GASG;AACH,MAAM,WAAW,SAAS;IACxB,2FAA2F;IAC3F,QAAQ,CAAC,gBAAgB,EAAE,SAAS,MAAM,EAAE,CAAC;IAC7C,gFAAgF;IAChF,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,6FAA6F;IAC7F,QAAQ,CAAC,UAAU,EAAE,gBAAgB,CAAC;CACvC;AAED,oFAAoF;AACpF,MAAM,WAAW,YAAY;IAC3B,gFAAgF;IAChF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,uGAAuG;IACvG,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,6FAA6F;AAC7F,MAAM,WAAW,cAAe,SAAQ,aAAa;IACnD,uGAAuG;IACvG,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,YAAY,CAGlF;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,KAAK,EAAE,SAAS,YAAY,EAAE,EAAE,OAAO,EAAE,SAAS,GAAG,cAAc,CAsB/F"}
@@ -0,0 +1,49 @@
1
+ import { formattingLocaleOf, localeDirection } from "@pithy-sh/core/src/i18n/locale";
2
+ import { matchLocale } from "@pithy-sh/core/src/i18n/match";
3
+ //#region src/resolve/chain.ts
4
+ /**
5
+ * A single tag as a one-element range list, or nothing when there is no tag.
6
+ *
7
+ * The links that read a stored or supplied value all have this shape. A blank string is nothing, not a
8
+ * range: a cookie the browser cleared and a cookie that was never set mean the same thing here.
9
+ */
10
+ function tagLink(name, tag) {
11
+ const trimmed = tag?.trim();
12
+ return {
13
+ name,
14
+ ranges: trimmed ? [trimmed] : []
15
+ };
16
+ }
17
+ /**
18
+ * Walk `links` in order and take the first that matches a supported locale.
19
+ *
20
+ * **The two locales are decided here, and only one of them falls back.** The catalog locale is what
21
+ * matched — the words the kit actually has. The formatting locale is the *range the reader sent*, kept
22
+ * whole when it is a tag `Intl` accepts: an `es-AR` visitor reads the `es` catalog and formats as
23
+ * `es-AR`, which `Intl` supports natively whether or not anyone wrote a string for it.
24
+ *
25
+ * Total. Every construction on the way through is guarded, so a chain fed nothing but malformed input
26
+ * lands on the project default rather than throwing.
27
+ */
28
+ function resolveChain(links, locales) {
29
+ for (const link of links) {
30
+ if (link.ranges.length === 0) continue;
31
+ const match = matchLocale(link.ranges, locales.supportedLocales, locales.exceptions);
32
+ if (!match) continue;
33
+ const requested = formattingLocaleOf(match.range);
34
+ return {
35
+ catalogLocale: match.locale,
36
+ formattingLocale: requested ?? match.locale,
37
+ direction: localeDirection(match.locale),
38
+ resolvedBy: link.name
39
+ };
40
+ }
41
+ return {
42
+ catalogLocale: locales.defaultLocale,
43
+ formattingLocale: locales.defaultLocale,
44
+ direction: localeDirection(locales.defaultLocale),
45
+ resolvedBy: "default"
46
+ };
47
+ }
48
+ //#endregion
49
+ export { resolveChain, tagLink };
@@ -0,0 +1,31 @@
1
+ import type { I18nConfig } from "../config/config";
2
+ import { type ResolvedLocale } from "./chain";
3
+ /**
4
+ * What a Worker knows about a request's language, before any of it is trusted.
5
+ *
6
+ * Every field is caller-supplied and every one of them is guarded downstream: a `*`, an `en_US`, an
7
+ * empty token or a fragment still carrying `;q=0.9` falls out of the match instead of reaching
8
+ * `Intl.Locale` and raising a `RangeError`.
9
+ */
10
+ export interface ServerLocaleSignals {
11
+ /** An explicit choice on the query string — `?lang=es`. */
12
+ readonly param?: string | null;
13
+ /** The signed-in reader's own preference, from `pithy_auth_users.locale`. */
14
+ readonly user?: string | null;
15
+ /** The locale cookie, for a reader who chose one and is not signed in. */
16
+ readonly cookie?: string | null;
17
+ /** The raw `Accept-Language` header, q-weights and all. */
18
+ readonly header?: string | null;
19
+ }
20
+ /**
21
+ * The request's locale, by the configured server chain.
22
+ *
23
+ * Default order: explicit param, the account, the cookie, `Accept-Language`, the project default. An
24
+ * adopter reorders or shortens it in config; `default` is the last resort whether or not it is listed.
25
+ *
26
+ * `Accept-Language` is honored as the **full q-weighted list**, not as its first entry —
27
+ * `pt-PT;q=1.0, es;q=0.8, en;q=0.5` from a reader with no Portuguese is a request for Spanish, and
28
+ * reading only the head answers English.
29
+ */
30
+ export declare function resolveServerLocale(signals: ServerLocaleSignals, config: I18nConfig): ResolvedLocale;
31
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../src/resolve/server.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,kBAAkB,CAAC;AACnE,OAAO,EAAE,KAAK,cAAc,EAA4C,MAAM,SAAS,CAAC;AAExF;;;;;;GAMG;AACH,MAAM,WAAW,mBAAmB;IAClC,2DAA2D;IAC3D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,0EAA0E;IAC1E,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,2DAA2D;IAC3D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACjC;AAED;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,mBAAmB,EAAE,MAAM,EAAE,UAAU,GAAG,cAAc,CAGpG"}
@@ -0,0 +1,32 @@
1
+ import { resolveChain, tagLink } from "./chain.js";
2
+ import { parseAcceptLanguage } from "@pithy-sh/core/src/i18n/acceptLanguage";
3
+ //#region src/resolve/server.ts
4
+ /**
5
+ * The request's locale, by the configured server chain.
6
+ *
7
+ * Default order: explicit param, the account, the cookie, `Accept-Language`, the project default. An
8
+ * adopter reorders or shortens it in config; `default` is the last resort whether or not it is listed.
9
+ *
10
+ * `Accept-Language` is honored as the **full q-weighted list**, not as its first entry —
11
+ * `pt-PT;q=1.0, es;q=0.8, en;q=0.5` from a reader with no Portuguese is a request for Spanish, and
12
+ * reading only the head answers English.
13
+ */
14
+ function resolveServerLocale(signals, config) {
15
+ const links = config.serverResolvers.map((resolver) => linkFor(resolver, signals, config));
16
+ return resolveChain(links, config);
17
+ }
18
+ /** One link, by name. A standalone function so the switch stays exhaustive under `verbatimModuleSyntax`. */
19
+ function linkFor(resolver, signals, config) {
20
+ switch (resolver) {
21
+ case "param": return tagLink(resolver, signals.param);
22
+ case "user": return tagLink(resolver, signals.user);
23
+ case "cookie": return tagLink(resolver, signals.cookie);
24
+ case "header": return {
25
+ name: resolver,
26
+ ranges: parseAcceptLanguage(signals.header).map((entry) => entry.range)
27
+ };
28
+ case "default": return tagLink(resolver, config.defaultLocale);
29
+ }
30
+ }
31
+ //#endregion
32
+ export { resolveServerLocale };
@@ -0,0 +1,21 @@
1
+ import type { CapabilitySettings } from "@pithy-sh/core/src/capability/settings";
2
+ import type { LocaleCatalogs } from "@pithy-sh/core/src/i18n/catalog";
3
+ import type { I18nConfig } from "../config/config";
4
+ /**
5
+ * Catalog coverage as `pithy doctor`'s **local** tier — offline, project-file-only, and already fatal
6
+ * to `doctor`'s exit code.
7
+ *
8
+ * This is the whole of what a `pithy i18n check` command would have been, and it costs no new command,
9
+ * no `docs/commands/` page, and no edit to the five exact-count or byte-pinned CLI gates a new command
10
+ * moves (`dispatch.test.ts`'s `DECLARED`/`GROUPS`, `binDocs.test.ts`'s byte comparison of `docs/CLI.md`
11
+ * §4.1 against real `pithy --help`, `doctorDocs.test.ts`'s mandated page sections and `--json` key
12
+ * register). The seam already exists and this is textbook for it.
13
+ *
14
+ * It answers one question: **would a reader in a supported locale meet a sentence nobody wrote?** For
15
+ * each locale the project serves, every key reachable in the default locale must be reachable in that
16
+ * one too, through the adopter's own catalog or the kit's translation.
17
+ *
18
+ * There is no `account` tier. Nothing about language is a question for the Cloudflare API.
19
+ */
20
+ export declare function i18nSettings(config: I18nConfig, composed: () => LocaleCatalogs): CapabilitySettings;
21
+ //# sourceMappingURL=coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../../src/settings/coverage.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,kBAAkB,EAAmB,MAAM,wCAAwC,CAAC;AAClG,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,cAAc,GAAG,kBAAkB,CA8BnG"}
@@ -0,0 +1,48 @@
1
+ import { KIT_CATALOGS } from "../catalogs/kit.js";
2
+ //#region src/settings/coverage.ts
3
+ /** How many missing keys a finding names before it stops counting out loud. */
4
+ const LISTED_KEYS = 5;
5
+ /**
6
+ * Catalog coverage as `pithy doctor`'s **local** tier — offline, project-file-only, and already fatal
7
+ * to `doctor`'s exit code.
8
+ *
9
+ * This is the whole of what a `pithy i18n check` command would have been, and it costs no new command,
10
+ * no `docs/commands/` page, and no edit to the five exact-count or byte-pinned CLI gates a new command
11
+ * moves (`dispatch.test.ts`'s `DECLARED`/`GROUPS`, `binDocs.test.ts`'s byte comparison of `docs/CLI.md`
12
+ * §4.1 against real `pithy --help`, `doctorDocs.test.ts`'s mandated page sections and `--json` key
13
+ * register). The seam already exists and this is textbook for it.
14
+ *
15
+ * It answers one question: **would a reader in a supported locale meet a sentence nobody wrote?** For
16
+ * each locale the project serves, every key reachable in the default locale must be reachable in that
17
+ * one too, through the adopter's own catalog or the kit's translation.
18
+ *
19
+ * There is no `account` tier. Nothing about language is a question for the Cloudflare API.
20
+ */
21
+ function i18nSettings(config, composed) {
22
+ return { local: () => {
23
+ const findings = [];
24
+ const capabilityMessages = composed();
25
+ const baseline = /* @__PURE__ */ new Set([...Object.keys(capabilityMessages[config.defaultLocale] ?? {}), ...Object.keys(config.messages[config.defaultLocale] ?? {})]);
26
+ for (const locale of config.supportedLocales) {
27
+ if (locale === config.defaultLocale) continue;
28
+ const covered = /* @__PURE__ */ new Set([
29
+ ...Object.keys(KIT_CATALOGS[locale] ?? {}),
30
+ ...Object.keys(capabilityMessages[locale] ?? {}),
31
+ ...Object.keys(config.messages[locale] ?? {})
32
+ ]);
33
+ const missing = [...baseline].filter((key) => !covered.has(key)).sort();
34
+ if (missing.length === 0) continue;
35
+ const named = missing.slice(0, LISTED_KEYS).join(", ");
36
+ const rest = missing.length > LISTED_KEYS ? `, and ${missing.length - LISTED_KEYS} more` : "";
37
+ findings.push({
38
+ setting: `i18n.supportedLocales.${locale}`,
39
+ environment: null,
40
+ problem: `${missing.length} ${missing.length === 1 ? "message has" : "messages have"} no \`${locale}\` translation, so a reader in that language meets ${config.defaultLocale} instead.`,
41
+ action: `Add them to \`i18n({ messages: { ${locale}: … } })\` in your \`pithy.config.ts\`: ${named}${rest}.`
42
+ });
43
+ }
44
+ return findings;
45
+ } };
46
+ }
47
+ //#endregion
48
+ export { i18nSettings };
@@ -0,0 +1,5 @@
1
+ /** This package's npm name — the join key against a release feed. */
2
+ export declare const PACKAGE_NAME = "@pithy-sh/i18n";
3
+ /** This package's version, stamped from its own package.json at generation time. */
4
+ export declare const PACKAGE_VERSION = "0.1.3";
5
+ //# sourceMappingURL=version.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"version.generated.d.ts","sourceRoot":"","sources":["../src/version.generated.ts"],"names":[],"mappings":"AAWA,qEAAqE;AACrE,eAAO,MAAM,YAAY,mBAAmB,CAAC;AAE7C,oFAAoF;AACpF,eAAO,MAAM,eAAe,UAAU,CAAC"}
@@ -0,0 +1,7 @@
1
+ //#region src/version.generated.ts
2
+ /** This package's npm name — the join key against a release feed. */
3
+ const PACKAGE_NAME = "@pithy-sh/i18n";
4
+ /** This package's version, stamped from its own package.json at generation time. */
5
+ const PACKAGE_VERSION = "0.1.3";
6
+ //#endregion
7
+ export { PACKAGE_NAME, PACKAGE_VERSION };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pithy-sh/i18n",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -8,20 +8,25 @@
8
8
  "directory": "packages/i18n"
9
9
  },
10
10
  "files": [
11
+ "dist",
11
12
  "src",
12
13
  "pithy.manifest.json",
13
- "!src/**/*.test.*"
14
+ "!src/**/*.test.*",
15
+ "!dist/**/*.test.*"
14
16
  ],
15
17
  "type": "module",
16
18
  "engines": {
17
19
  "node": ">=22"
18
20
  },
19
21
  "exports": {
20
- "./src/react/translator": "./src/react/translator.tsx",
21
- "./src/*": "./src/*.ts"
22
+ "./src/*": {
23
+ "types": "./dist/*.d.ts",
24
+ "default": "./dist/*.js"
25
+ },
26
+ "./src/react/translator": "./src/react/translator.tsx"
22
27
  },
23
28
  "scripts": {
24
- "build": "tsc -p tsconfig.json --noEmit false --outDir dist",
29
+ "build": "tsdown && tsc -p tsconfig.build.json",
25
30
  "typecheck": "tsc -p tsconfig.json && tsc -p tsconfig.browser.json",
26
31
  "test": "vitest run",
27
32
  "test:node": "vitest run --project=node --project=dom",
@@ -30,12 +35,12 @@
30
35
  "reset": "bun run clean && rm -rf node_modules"
31
36
  },
32
37
  "dependencies": {
33
- "@pithy-sh/core": "^0.1.2",
34
- "hono": "^4.13.2",
35
- "zod": "^4.0.0"
38
+ "@pithy-sh/core": "^0.1.3"
36
39
  },
37
40
  "peerDependencies": {
38
- "react": "^19.0.0"
41
+ "hono": "^4.13.2",
42
+ "react": "^19.0.0",
43
+ "zod": "^4.4.0"
39
44
  },
40
45
  "peerDependenciesMeta": {
41
46
  "react": {
@@ -52,10 +57,13 @@
52
57
  "@types/react-dom": "^19.2.3",
53
58
  "@vitest/coverage-v8": "^4.1.0",
54
59
  "happy-dom": "^20.11.1",
60
+ "hono": "^4.13.2",
55
61
  "react": "^19.2.8",
56
62
  "react-dom": "^19.2.8",
63
+ "tsdown": "^0.23.0",
57
64
  "typescript": "^7.0.2",
58
65
  "vitest": "^4.1.0",
59
- "wrangler": "^4.115.0"
66
+ "wrangler": "^4.115.0",
67
+ "zod": "^4.4.0"
60
68
  }
61
69
  }
@@ -13,4 +13,4 @@
13
13
  export const PACKAGE_NAME = "@pithy-sh/i18n";
14
14
 
15
15
  /** This package's version, stamped from its own package.json at generation time. */
16
- export const PACKAGE_VERSION = "0.1.2";
16
+ export const PACKAGE_VERSION = "0.1.3";