@pithy-sh/i18n 0.1.1 → 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.
- package/dist/adapters/adapters.d.ts +47 -0
- package/dist/adapters/adapters.d.ts.map +1 -0
- package/dist/adapters/adapters.js +87 -0
- package/dist/browser/document.d.ts +36 -0
- package/dist/browser/document.d.ts.map +1 -0
- package/dist/browser/document.js +85 -0
- package/dist/browser/signals.d.ts +36 -0
- package/dist/browser/signals.d.ts.map +1 -0
- package/dist/browser/signals.js +70 -0
- package/dist/capability.d.ts +53 -0
- package/dist/capability.d.ts.map +1 -0
- package/dist/capability.js +110 -0
- package/dist/catalogs/browser.d.ts +13 -0
- package/dist/catalogs/browser.d.ts.map +1 -0
- package/dist/catalogs/browser.js +33 -0
- package/dist/catalogs/es/errors.d.ts +30 -0
- package/dist/catalogs/es/errors.d.ts.map +1 -0
- package/dist/catalogs/es/errors.js +154 -0
- package/dist/catalogs/es/index.d.ts +23 -0
- package/dist/catalogs/es/index.d.ts.map +1 -0
- package/dist/catalogs/es/index.js +29 -0
- package/dist/catalogs/es/screens.d.ts +28 -0
- package/dist/catalogs/es/screens.d.ts.map +1 -0
- package/dist/catalogs/es/screens.js +105 -0
- package/dist/catalogs/kit.d.ts +21 -0
- package/dist/catalogs/kit.d.ts.map +1 -0
- package/dist/catalogs/kit.js +23 -0
- package/dist/client/projection.d.ts +47 -0
- package/dist/client/projection.d.ts.map +1 -0
- package/dist/client/projection.js +0 -0
- package/dist/config/config.d.ts +71 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +103 -0
- package/dist/http/middleware.d.ts +36 -0
- package/dist/http/middleware.d.ts.map +1 -0
- package/dist/http/middleware.js +93 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/react/translator.d.ts +73 -0
- package/dist/react/translator.d.ts.map +1 -0
- package/dist/react/translator.js +81 -0
- package/dist/react/useNegotiatedLocale.d.ts +67 -0
- package/dist/react/useNegotiatedLocale.d.ts.map +1 -0
- package/dist/react/useNegotiatedLocale.js +108 -0
- package/dist/resolve/browser.d.ts +69 -0
- package/dist/resolve/browser.d.ts.map +1 -0
- package/dist/resolve/browser.js +62 -0
- package/dist/resolve/chain.d.ts +52 -0
- package/dist/resolve/chain.d.ts.map +1 -0
- package/dist/resolve/chain.js +49 -0
- package/dist/resolve/server.d.ts +31 -0
- package/dist/resolve/server.d.ts.map +1 -0
- package/dist/resolve/server.js +32 -0
- package/dist/settings/coverage.d.ts +21 -0
- package/dist/settings/coverage.d.ts.map +1 -0
- package/dist/settings/coverage.js +48 -0
- package/dist/version.generated.d.ts +5 -0
- package/dist/version.generated.d.ts.map +1 -0
- package/dist/version.generated.js +7 -0
- package/package.json +18 -10
- package/src/version.generated.ts +1 -1
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Translator } from "@pithy-sh/core/src/i18n/translator";
|
|
2
|
+
/** The part of an i18next instance this adapter calls. */
|
|
3
|
+
export interface I18nextLike {
|
|
4
|
+
/** The active language tag. */
|
|
5
|
+
readonly language: string;
|
|
6
|
+
/** i18next's lookup. `count` in the options object is what drives its own plural selection. */
|
|
7
|
+
t(key: string, options?: Record<string, unknown>): string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* A `Translator` backed by an i18next instance.
|
|
11
|
+
*
|
|
12
|
+
* i18next selects plurals from a `count` option rather than from a key suffix, so `plural` hands it
|
|
13
|
+
* `count` and lets it choose — the kit's `<key>.<category>` convention is not imposed on a catalog
|
|
14
|
+
* i18next already owns.
|
|
15
|
+
*/
|
|
16
|
+
export declare function fromI18next(instance: I18nextLike, formattingLocale?: string): Translator;
|
|
17
|
+
/** The part of a FormatJS / react-intl `IntlShape` this adapter calls. */
|
|
18
|
+
export interface IntlShapeLike {
|
|
19
|
+
/** The active locale tag. */
|
|
20
|
+
readonly locale: string;
|
|
21
|
+
/** FormatJS's lookup, keyed by message id. */
|
|
22
|
+
formatMessage(descriptor: {
|
|
23
|
+
id: string;
|
|
24
|
+
}, values?: Record<string, unknown>): string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* A `Translator` backed by a FormatJS / react-intl `IntlShape`.
|
|
28
|
+
*
|
|
29
|
+
* FormatJS resolves plurals inside the ICU message itself, so `plural` passes `count` as a value and
|
|
30
|
+
* lets the message's own `{count, plural, …}` arm decide.
|
|
31
|
+
*/
|
|
32
|
+
export declare function fromIntl(intl: IntlShapeLike, formattingLocale?: string): Translator;
|
|
33
|
+
/** The part of a Lingui `I18n` instance this adapter calls. */
|
|
34
|
+
export interface LinguiLike {
|
|
35
|
+
/** The active locale tag. */
|
|
36
|
+
readonly locale: string;
|
|
37
|
+
/** Lingui's lookup, keyed by message id. */
|
|
38
|
+
_(id: string, values?: Record<string, unknown>): string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* A `Translator` backed by a Lingui `I18n` instance.
|
|
42
|
+
*
|
|
43
|
+
* Lingui also resolves plurals inside the message, so this is the same shape as {@link fromIntl} — the
|
|
44
|
+
* two differ only in what the lookup is called.
|
|
45
|
+
*/
|
|
46
|
+
export declare function fromLingui(i18n: LinguiLike, formattingLocale?: string): Translator;
|
|
47
|
+
//# sourceMappingURL=adapters.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../../src/adapters/adapters.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,oCAAoC,CAAC;AAkErE,0DAA0D;AAC1D,MAAM,WAAW,WAAW;IAC1B,+BAA+B;IAC/B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,+FAA+F;IAC/F,CAAC,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;CAC3D;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,WAAW,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,UAAU,CAOxF;AAED,0EAA0E;AAC1E,MAAM,WAAW,aAAa;IAC5B,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,aAAa,CAAC,UAAU,EAAE;QAAE,EAAE,EAAE,MAAM,CAAA;KAAE,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;CACrF;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,aAAa,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,UAAU,CAMnF;AAED,+DAA+D;AAC/D,MAAM,WAAW,UAAU;IACzB,6BAA6B;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,4CAA4C;IAC5C,CAAC,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM,CAAC;CACzD;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,UAAU,EAAE,gBAAgB,CAAC,EAAE,MAAM,GAAG,UAAU,CAMlF"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createTranslator } from "@pithy-sh/core/src/i18n/translator";
|
|
2
|
+
import { localeDirection } from "@pithy-sh/core/src/i18n/locale";
|
|
3
|
+
//#region src/adapters/adapters.ts
|
|
4
|
+
/**
|
|
5
|
+
* A `Translator` whose words come from `translate` and whose formatting comes from `Intl`.
|
|
6
|
+
*
|
|
7
|
+
* The formatting half is never delegated. Every one of these libraries formats through `Intl` anyway,
|
|
8
|
+
* workerd embeds full ICU, and a `Translator` that formatted through a wrapper would answer a
|
|
9
|
+
* different date for the same locale depending on which adapter was in use.
|
|
10
|
+
*/
|
|
11
|
+
function wrap(source) {
|
|
12
|
+
const base = createTranslator({
|
|
13
|
+
catalogLocale: source.catalogLocale,
|
|
14
|
+
formattingLocale: source.formattingLocale,
|
|
15
|
+
layers: []
|
|
16
|
+
});
|
|
17
|
+
const translate = (key, params) => source.translate(key, params);
|
|
18
|
+
return {
|
|
19
|
+
...base,
|
|
20
|
+
direction: localeDirection(source.catalogLocale),
|
|
21
|
+
t: translate,
|
|
22
|
+
/**
|
|
23
|
+
* **A miss is the key coming back**, which is what every library this adapts already does:
|
|
24
|
+
* i18next returns the key, FormatJS returns the message id, Lingui returns the id.
|
|
25
|
+
*
|
|
26
|
+
* It has to be implemented here rather than inherited. `wrap` builds its base from
|
|
27
|
+
* `createTranslator` with **no layers**, so an inherited `maybe` answers `null` for everything —
|
|
28
|
+
* and `maybe` is the whole of the documented error contract, `t.maybe(code, params) ?? message`.
|
|
29
|
+
* Left inherited, every adapted translator silently rendered the English fallback for every code
|
|
30
|
+
* it could in fact translate, and nothing failed to say so.
|
|
31
|
+
*/
|
|
32
|
+
maybe: (key, params) => {
|
|
33
|
+
const answered = translate(key, params);
|
|
34
|
+
return answered === key ? null : answered;
|
|
35
|
+
},
|
|
36
|
+
plural: (key, count, params) => source.plural ? source.plural(key, count, params) : translate(key, {
|
|
37
|
+
count,
|
|
38
|
+
...params
|
|
39
|
+
})
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A `Translator` backed by an i18next instance.
|
|
44
|
+
*
|
|
45
|
+
* i18next selects plurals from a `count` option rather than from a key suffix, so `plural` hands it
|
|
46
|
+
* `count` and lets it choose — the kit's `<key>.<category>` convention is not imposed on a catalog
|
|
47
|
+
* i18next already owns.
|
|
48
|
+
*/
|
|
49
|
+
function fromI18next(instance, formattingLocale) {
|
|
50
|
+
return wrap({
|
|
51
|
+
catalogLocale: instance.language,
|
|
52
|
+
formattingLocale,
|
|
53
|
+
translate: (key, params) => instance.t(key, params),
|
|
54
|
+
plural: (key, count, params) => instance.t(key, {
|
|
55
|
+
count,
|
|
56
|
+
...params
|
|
57
|
+
})
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* A `Translator` backed by a FormatJS / react-intl `IntlShape`.
|
|
62
|
+
*
|
|
63
|
+
* FormatJS resolves plurals inside the ICU message itself, so `plural` passes `count` as a value and
|
|
64
|
+
* lets the message's own `{count, plural, …}` arm decide.
|
|
65
|
+
*/
|
|
66
|
+
function fromIntl(intl, formattingLocale) {
|
|
67
|
+
return wrap({
|
|
68
|
+
catalogLocale: intl.locale,
|
|
69
|
+
formattingLocale,
|
|
70
|
+
translate: (key, params) => intl.formatMessage({ id: key }, params)
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* A `Translator` backed by a Lingui `I18n` instance.
|
|
75
|
+
*
|
|
76
|
+
* Lingui also resolves plurals inside the message, so this is the same shape as {@link fromIntl} — the
|
|
77
|
+
* two differ only in what the lookup is called.
|
|
78
|
+
*/
|
|
79
|
+
function fromLingui(i18n, formattingLocale) {
|
|
80
|
+
return wrap({
|
|
81
|
+
catalogLocale: i18n.locale,
|
|
82
|
+
formattingLocale,
|
|
83
|
+
translate: (key, params) => i18n._(key, params)
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { fromI18next, fromIntl, fromLingui };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type LocaleContext } from "@pithy-sh/core/src/i18n/locale";
|
|
2
|
+
import type { I18nClientProjection } from "../client/projection";
|
|
3
|
+
/**
|
|
4
|
+
* The document's opening language, negotiated from what `virtual:pithy/i18n` projects.
|
|
5
|
+
*
|
|
6
|
+
* **The entry point a scaffolded `client.tsx` calls, and the reason it is one line there.** A template
|
|
7
|
+
* is copied into an adopter's repository and never rewritten, so anything it does by hand is frozen on
|
|
8
|
+
* the day they scaffold. Two things in particular must not be: the guarded storage reads — `localStorage`
|
|
9
|
+
* throws in a private window, behind a site-data block, and inside some embedded views — and the chain
|
|
10
|
+
* order itself, which is the project's configuration rather than the front end's assumption.
|
|
11
|
+
*
|
|
12
|
+
* **It takes the projection, not an `I18nConfig`.** The projection is browser-safe by construction and
|
|
13
|
+
* is what a client actually holds: `browserResolvers` arrives as `string[]` because the generated
|
|
14
|
+
* ambient declaration in an adopter's Worker cannot name a type it does not import, so the chain is
|
|
15
|
+
* walked by name and an unrecognized link contributes nothing rather than throwing. `recognizedResolvers`
|
|
16
|
+
* is where that happens, once, for this pass and for `useNegotiatedLocale` both.
|
|
17
|
+
*
|
|
18
|
+
* **This is the first pass, not the decision.** It reaches only what a page can see before it renders:
|
|
19
|
+
* the URL, this device's memory, and whatever the server put on `<html lang>`. A signed-in reader's
|
|
20
|
+
* stored preference needs a session, which needs a render — `useNegotiatedLocale` picks that up and
|
|
21
|
+
* calls {@link applyDocumentLocale} again if it lands somewhere else. Doing it here as well is what
|
|
22
|
+
* keeps a right-to-left reader from watching the page reflow after the first paint.
|
|
23
|
+
*
|
|
24
|
+
* Answers **both** locales it resolved, or `null` when the capability is not composed and there was
|
|
25
|
+
* nothing to negotiate — a project without it keeps the `lang` its `index.html` shipped.
|
|
26
|
+
*
|
|
27
|
+
* **Both, and not the one tag `lang` carries.** This returned a single string at first, and the
|
|
28
|
+
* scaffolded provider then passed it as `catalogLocale` *and* `formattingLocale` — which is exactly the
|
|
29
|
+
* collapse the two-locale design exists to prevent, reintroduced in the one place a reader actually
|
|
30
|
+
* looks at a page. An `es-AR` visitor got Argentine dates from the Worker and Spanish-from-Spain dates
|
|
31
|
+
* from the SPA, on the same account, in the same session. The region survives here for the same reason
|
|
32
|
+
* `resolveChain` keeps it on the server: the range the reader asked for is a fact the match already
|
|
33
|
+
* knows, and throwing it away is a decision, not an omission.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyProjectedLocale(projection: I18nClientProjection): LocaleContext | null;
|
|
36
|
+
//# sourceMappingURL=document.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"document.d.ts","sourceRoot":"","sources":["../../src/browser/document.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsB,KAAK,aAAa,EAAmB,MAAM,gCAAgC,CAAC;AAEzG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAKjE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,oBAAoB,GAAG,aAAa,GAAG,IAAI,CAoB3F"}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { recognizedResolvers } from "../resolve/browser.js";
|
|
2
|
+
import { applyDocumentLocale } from "./signals.js";
|
|
3
|
+
import { formattingLocaleOf, localeDirection } from "@pithy-sh/core/src/i18n/locale";
|
|
4
|
+
import { matchLocale } from "@pithy-sh/core/src/i18n/match";
|
|
5
|
+
//#region src/browser/document.ts
|
|
6
|
+
/**
|
|
7
|
+
* The document's opening language, negotiated from what `virtual:pithy/i18n` projects.
|
|
8
|
+
*
|
|
9
|
+
* **The entry point a scaffolded `client.tsx` calls, and the reason it is one line there.** A template
|
|
10
|
+
* is copied into an adopter's repository and never rewritten, so anything it does by hand is frozen on
|
|
11
|
+
* the day they scaffold. Two things in particular must not be: the guarded storage reads — `localStorage`
|
|
12
|
+
* throws in a private window, behind a site-data block, and inside some embedded views — and the chain
|
|
13
|
+
* order itself, which is the project's configuration rather than the front end's assumption.
|
|
14
|
+
*
|
|
15
|
+
* **It takes the projection, not an `I18nConfig`.** The projection is browser-safe by construction and
|
|
16
|
+
* is what a client actually holds: `browserResolvers` arrives as `string[]` because the generated
|
|
17
|
+
* ambient declaration in an adopter's Worker cannot name a type it does not import, so the chain is
|
|
18
|
+
* walked by name and an unrecognized link contributes nothing rather than throwing. `recognizedResolvers`
|
|
19
|
+
* is where that happens, once, for this pass and for `useNegotiatedLocale` both.
|
|
20
|
+
*
|
|
21
|
+
* **This is the first pass, not the decision.** It reaches only what a page can see before it renders:
|
|
22
|
+
* the URL, this device's memory, and whatever the server put on `<html lang>`. A signed-in reader's
|
|
23
|
+
* stored preference needs a session, which needs a render — `useNegotiatedLocale` picks that up and
|
|
24
|
+
* calls {@link applyDocumentLocale} again if it lands somewhere else. Doing it here as well is what
|
|
25
|
+
* keeps a right-to-left reader from watching the page reflow after the first paint.
|
|
26
|
+
*
|
|
27
|
+
* Answers **both** locales it resolved, or `null` when the capability is not composed and there was
|
|
28
|
+
* nothing to negotiate — a project without it keeps the `lang` its `index.html` shipped.
|
|
29
|
+
*
|
|
30
|
+
* **Both, and not the one tag `lang` carries.** This returned a single string at first, and the
|
|
31
|
+
* scaffolded provider then passed it as `catalogLocale` *and* `formattingLocale` — which is exactly the
|
|
32
|
+
* collapse the two-locale design exists to prevent, reintroduced in the one place a reader actually
|
|
33
|
+
* looks at a page. An `es-AR` visitor got Argentine dates from the Worker and Spanish-from-Spain dates
|
|
34
|
+
* from the SPA, on the same account, in the same session. The region survives here for the same reason
|
|
35
|
+
* `resolveChain` keeps it on the server: the range the reader asked for is a fact the match already
|
|
36
|
+
* knows, and throwing it away is a decision, not an omission.
|
|
37
|
+
*/
|
|
38
|
+
function applyProjectedLocale(projection) {
|
|
39
|
+
if (!projection.enabled) return null;
|
|
40
|
+
const wanted = [];
|
|
41
|
+
for (const resolver of recognizedResolvers(projection.browserResolvers)) {
|
|
42
|
+
const tag = signal(resolver, projection);
|
|
43
|
+
if (tag) wanted.push(tag);
|
|
44
|
+
}
|
|
45
|
+
const matched = matchLocale(wanted, projection.supportedLocales, projection.exceptions);
|
|
46
|
+
const catalogLocale = matched?.locale ?? projection.defaultLocale;
|
|
47
|
+
const resolved = {
|
|
48
|
+
catalogLocale,
|
|
49
|
+
formattingLocale: (matched ? formattingLocaleOf(matched.range) : null) ?? catalogLocale,
|
|
50
|
+
direction: localeDirection(catalogLocale)
|
|
51
|
+
};
|
|
52
|
+
applyDocumentLocale(resolved.catalogLocale, resolved.direction);
|
|
53
|
+
return resolved;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* One link of the browser chain, by name.
|
|
57
|
+
*
|
|
58
|
+
* `account` answers nothing: there is no session before a render, and inventing one here would be a
|
|
59
|
+
* second source of truth for a fact `pithy_auth_users.locale` owns. It stays in the list so the
|
|
60
|
+
* configured order is walked whole, and so the link that follows it is still asked in its own place.
|
|
61
|
+
*
|
|
62
|
+
* Takes a `BrowserResolver` rather than a `string`, so the switch is exhaustive: `recognizedResolvers`
|
|
63
|
+
* has already dropped anything this build does not know, and a link added to the enum tomorrow is a red
|
|
64
|
+
* build here rather than a silently inert first paint.
|
|
65
|
+
*/
|
|
66
|
+
function signal(resolver, projection) {
|
|
67
|
+
switch (resolver) {
|
|
68
|
+
case "query": return read(() => new URL(window.location.href).searchParams.get(projection.queryParam));
|
|
69
|
+
case "account": return null;
|
|
70
|
+
case "storage": return read(() => window.localStorage.getItem(projection.storageKey));
|
|
71
|
+
case "navigator": return read(() => window.navigator.languages?.[0] ?? window.navigator.language ?? null);
|
|
72
|
+
case "server": return read(() => document.documentElement.lang || null);
|
|
73
|
+
case "default": return projection.defaultLocale;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/** Read a global, answering `null` rather than throwing when the global is absent or refuses. */
|
|
77
|
+
function read(get) {
|
|
78
|
+
try {
|
|
79
|
+
return get();
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
//#endregion
|
|
85
|
+
export { applyProjectedLocale };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { BrowserChain, BrowserLocaleSignals } from "../resolve/browser";
|
|
2
|
+
/**
|
|
3
|
+
* The browser-side edge of locale resolution: the three signals a page can read for itself.
|
|
4
|
+
*
|
|
5
|
+
* **Separate from `resolve/browser.ts` on purpose.** That module is a pure function — signals in,
|
|
6
|
+
* locale out — so it typechecks and tests in the Worker program with no DOM library in scope. This one
|
|
7
|
+
* touches globals, so it lives in the browser program (`tsconfig.browser.json`) and nothing a Worker
|
|
8
|
+
* bundles can reach it. Effects at the edge; the decision in the middle.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* What the page can see: the query string, this device's memory, and what the server put on `<html lang>`.
|
|
12
|
+
*
|
|
13
|
+
* `account` is deliberately absent — the app knows whether someone is signed in and this function does
|
|
14
|
+
* not. Merge it in at the call site.
|
|
15
|
+
*
|
|
16
|
+
* **Every read is wrapped**, because two of the three throw in real browsers. `localStorage` raises in
|
|
17
|
+
* a private window, behind a site-data block, and in some embedded views; and during a server render
|
|
18
|
+
* there is no `window` or `document` at all. A reader whose browser refuses storage still gets a
|
|
19
|
+
* language — just not a remembered one.
|
|
20
|
+
*
|
|
21
|
+
* Takes the two names a page looks itself up by and nothing else, so the projection a browser holds and
|
|
22
|
+
* the config a Worker resolves both answer it without either being widened into the other.
|
|
23
|
+
*/
|
|
24
|
+
export declare function readBrowserSignals(config: Pick<BrowserChain, "queryParam" | "storageKey">): BrowserLocaleSignals;
|
|
25
|
+
/** Remember `locale` on this device. Silent when storage is unavailable — a preference is not worth a throw. */
|
|
26
|
+
export declare function rememberBrowserLocale(locale: string, config: Pick<BrowserChain, "storageKey">): void;
|
|
27
|
+
/**
|
|
28
|
+
* Put `lang` and `dir` on the document.
|
|
29
|
+
*
|
|
30
|
+
* The one thing no catalog can do and every screen needs: assistive technology, hyphenation, and the
|
|
31
|
+
* browser's own text handling all read `lang`, and `dir` is what makes a right-to-left locale lay out
|
|
32
|
+
* at all. `templates/index.html` ships `lang="en"` as static text with no substitution token, so the
|
|
33
|
+
* script side is the only hook there is.
|
|
34
|
+
*/
|
|
35
|
+
export declare function applyDocumentLocale(locale: string, direction: "ltr" | "rtl"): void;
|
|
36
|
+
//# sourceMappingURL=signals.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signals.d.ts","sourceRoot":"","sources":["../../src/browser/signals.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAE7E;;;;;;;GAOG;AAEH;;;;;;;;;;;;;GAaG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,GAAG,YAAY,CAAC,GAAG,oBAAoB,CAShH;AAED,gHAAgH;AAChH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,IAAI,CAOpG;AAED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,GAAG,KAAK,GAAG,IAAI,CAQlF"}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
//#region src/browser/signals.ts
|
|
2
|
+
/**
|
|
3
|
+
* The browser-side edge of locale resolution: the three signals a page can read for itself.
|
|
4
|
+
*
|
|
5
|
+
* **Separate from `resolve/browser.ts` on purpose.** That module is a pure function — signals in,
|
|
6
|
+
* locale out — so it typechecks and tests in the Worker program with no DOM library in scope. This one
|
|
7
|
+
* touches globals, so it lives in the browser program (`tsconfig.browser.json`) and nothing a Worker
|
|
8
|
+
* bundles can reach it. Effects at the edge; the decision in the middle.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* What the page can see: the query string, this device's memory, and what the server put on `<html lang>`.
|
|
12
|
+
*
|
|
13
|
+
* `account` is deliberately absent — the app knows whether someone is signed in and this function does
|
|
14
|
+
* not. Merge it in at the call site.
|
|
15
|
+
*
|
|
16
|
+
* **Every read is wrapped**, because two of the three throw in real browsers. `localStorage` raises in
|
|
17
|
+
* a private window, behind a site-data block, and in some embedded views; and during a server render
|
|
18
|
+
* there is no `window` or `document` at all. A reader whose browser refuses storage still gets a
|
|
19
|
+
* language — just not a remembered one.
|
|
20
|
+
*
|
|
21
|
+
* Takes the two names a page looks itself up by and nothing else, so the projection a browser holds and
|
|
22
|
+
* the config a Worker resolves both answer it without either being widened into the other.
|
|
23
|
+
*/
|
|
24
|
+
function readBrowserSignals(config) {
|
|
25
|
+
return {
|
|
26
|
+
query: read(() => new URL(window.location.href).searchParams.get(config.queryParam)),
|
|
27
|
+
storage: read(() => window.localStorage.getItem(config.storageKey)),
|
|
28
|
+
navigator: readList(() => [...window.navigator.languages ?? [window.navigator.language]]),
|
|
29
|
+
server: read(() => document.documentElement.lang || null)
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
/** Remember `locale` on this device. Silent when storage is unavailable — a preference is not worth a throw. */
|
|
33
|
+
function rememberBrowserLocale(locale, config) {
|
|
34
|
+
try {
|
|
35
|
+
window.localStorage.setItem(config.storageKey, locale);
|
|
36
|
+
} catch {}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Put `lang` and `dir` on the document.
|
|
40
|
+
*
|
|
41
|
+
* The one thing no catalog can do and every screen needs: assistive technology, hyphenation, and the
|
|
42
|
+
* browser's own text handling all read `lang`, and `dir` is what makes a right-to-left locale lay out
|
|
43
|
+
* at all. `templates/index.html` ships `lang="en"` as static text with no substitution token, so the
|
|
44
|
+
* script side is the only hook there is.
|
|
45
|
+
*/
|
|
46
|
+
function applyDocumentLocale(locale, direction) {
|
|
47
|
+
try {
|
|
48
|
+
document.documentElement.lang = locale;
|
|
49
|
+
document.documentElement.dir = direction;
|
|
50
|
+
} catch {}
|
|
51
|
+
}
|
|
52
|
+
/** Read a list off a global, answering `null` rather than throwing when the global is absent. */
|
|
53
|
+
function readList(get) {
|
|
54
|
+
try {
|
|
55
|
+
const list = get().filter((tag) => typeof tag === "string" && tag.trim().length > 0);
|
|
56
|
+
return list.length > 0 ? list : null;
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Read a global, answering `null` rather than throwing when the global is absent or refuses. */
|
|
62
|
+
function read(get) {
|
|
63
|
+
try {
|
|
64
|
+
return get();
|
|
65
|
+
} catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { applyDocumentLocale, readBrowserSignals, rememberBrowserLocale };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type Capability } from "@pithy-sh/core/src/capability/capability";
|
|
2
|
+
import type { LocaleCatalogs, MessageCatalog } from "@pithy-sh/core/src/i18n/catalog";
|
|
3
|
+
import { I18nConfig, type I18nConfigInput } from "./config/config";
|
|
4
|
+
/**
|
|
5
|
+
* The i18n capability, with its resolved config and the catalogs it composed attached for inspection.
|
|
6
|
+
*
|
|
7
|
+
* The composed catalogs are populated by the `compose` hook rather than at construction, because a
|
|
8
|
+
* capability sees only itself when it is constructed and the kit's English lives on the capabilities
|
|
9
|
+
* that own it.
|
|
10
|
+
*/
|
|
11
|
+
export interface I18nCapability extends Capability {
|
|
12
|
+
/** The validated config, for `@pithy-sh/ui-react` and anything else that needs the negotiated set. */
|
|
13
|
+
i18nConfig: I18nConfig;
|
|
14
|
+
/** Every composed capability's English, merged under the domain rule. Empty until `compose` runs. */
|
|
15
|
+
composedMessages: LocaleCatalogs;
|
|
16
|
+
/** The catalog layers `t()` walks for `locale`, most-specific first. */
|
|
17
|
+
layersFor(locale: string): readonly (MessageCatalog | undefined)[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* The i18n capability.
|
|
21
|
+
*
|
|
22
|
+
* **Stateless, like `@pithy-sh/turnstile`** — no tables, no migrations, no `<NAME>_MIGRATION_ORDER`, no
|
|
23
|
+
* error codes, no bindings of its own. It contributes one middleware, which resolves the request's
|
|
24
|
+
* locale and replaces `c.var.t` with a translator over the merged catalogs.
|
|
25
|
+
*
|
|
26
|
+
* It declares no error codes deliberately. The moment an `i18n/*` code entered `KitErrorPayload` the
|
|
27
|
+
* whole `i18n` domain would be reserved against adopters, and `i18n` is a generic enough word that
|
|
28
|
+
* somebody has already defined `i18n/missing_catalog` through `defineErrorPayload`. Nothing here needs
|
|
29
|
+
* a code that `core/*` and `validation/*` do not already carry.
|
|
30
|
+
*
|
|
31
|
+
* ## What the layers are, and why the order is this
|
|
32
|
+
*
|
|
33
|
+
* `t(key)` walks, per key, in this order:
|
|
34
|
+
*
|
|
35
|
+
* 1. the adopter's catalog for the resolved locale,
|
|
36
|
+
* 2. the adopter's catalog for the project default,
|
|
37
|
+
* 3. **the kit's translation** for the resolved locale — the `es` this package ships,
|
|
38
|
+
* 4. every composed capability's English, for the resolved locale,
|
|
39
|
+
* 5. every composed capability's copy for the project default,
|
|
40
|
+
* 6. and their English, which is the language the kit is written in.
|
|
41
|
+
*
|
|
42
|
+
* Per key, never per catalog: overriding one sentence is one entry, and every key an adopter did not
|
|
43
|
+
* mention keeps flowing from the package. That is what makes an override a merge rather than a fork,
|
|
44
|
+
* and it is why kit catalogs are never copied into an adopter's repository.
|
|
45
|
+
*
|
|
46
|
+
* Errors are not in any of those layers and do not need to be: the payload already carries its English
|
|
47
|
+
* `message`, and a translating client renders `t.maybe(payload.code, payload.params) ?? payload.message`. So
|
|
48
|
+
* the kit ships translations for the 120 codes and no English duplicate of what is already on the wire.
|
|
49
|
+
*/
|
|
50
|
+
export declare function i18n(config?: I18nConfigInput): I18nCapability;
|
|
51
|
+
/** Whether a capability is the i18n capability — carries its resolved config and composed catalogs. */
|
|
52
|
+
export declare function isI18nCapability(capability: Capability): capability is I18nCapability;
|
|
53
|
+
//# sourceMappingURL=capability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,UAAU,EAAoB,MAAM,0CAA0C,CAAC;AAC7F,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAKtF,OAAO,EAAE,UAAU,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAKnE;;;;;;GAMG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,sGAAsG;IACtG,UAAU,EAAE,UAAU,CAAC;IACvB,qGAAqG;IACrG,gBAAgB,EAAE,cAAc,CAAC;IACjC,wEAAwE;IACxE,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,cAAc,GAAG,SAAS,CAAC,EAAE,CAAC;CACpE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,wBAAgB,IAAI,CAAC,MAAM,GAAE,eAAoB,GAAG,cAAc,CA0EjE;AAED,uGAAuG;AACvG,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,IAAI,cAAc,CAErF"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { KIT_CATALOGS } from "./catalogs/kit.js";
|
|
2
|
+
import { I18nConfig } from "./config/config.js";
|
|
3
|
+
import { i18nMiddleware } from "./http/middleware.js";
|
|
4
|
+
import { i18nSettings } from "./settings/coverage.js";
|
|
5
|
+
import { PACKAGE_VERSION } from "./version.generated.js";
|
|
6
|
+
import { defineCapability } from "@pithy-sh/core/src/capability/capability";
|
|
7
|
+
import { composeMessages } from "@pithy-sh/core/src/i18n/registry";
|
|
8
|
+
import { DEFAULT_LOCALE } from "@pithy-sh/core/src/i18n/translator";
|
|
9
|
+
//#region src/capability.ts
|
|
10
|
+
/**
|
|
11
|
+
* The i18n capability.
|
|
12
|
+
*
|
|
13
|
+
* **Stateless, like `@pithy-sh/turnstile`** — no tables, no migrations, no `<NAME>_MIGRATION_ORDER`, no
|
|
14
|
+
* error codes, no bindings of its own. It contributes one middleware, which resolves the request's
|
|
15
|
+
* locale and replaces `c.var.t` with a translator over the merged catalogs.
|
|
16
|
+
*
|
|
17
|
+
* It declares no error codes deliberately. The moment an `i18n/*` code entered `KitErrorPayload` the
|
|
18
|
+
* whole `i18n` domain would be reserved against adopters, and `i18n` is a generic enough word that
|
|
19
|
+
* somebody has already defined `i18n/missing_catalog` through `defineErrorPayload`. Nothing here needs
|
|
20
|
+
* a code that `core/*` and `validation/*` do not already carry.
|
|
21
|
+
*
|
|
22
|
+
* ## What the layers are, and why the order is this
|
|
23
|
+
*
|
|
24
|
+
* `t(key)` walks, per key, in this order:
|
|
25
|
+
*
|
|
26
|
+
* 1. the adopter's catalog for the resolved locale,
|
|
27
|
+
* 2. the adopter's catalog for the project default,
|
|
28
|
+
* 3. **the kit's translation** for the resolved locale — the `es` this package ships,
|
|
29
|
+
* 4. every composed capability's English, for the resolved locale,
|
|
30
|
+
* 5. every composed capability's copy for the project default,
|
|
31
|
+
* 6. and their English, which is the language the kit is written in.
|
|
32
|
+
*
|
|
33
|
+
* Per key, never per catalog: overriding one sentence is one entry, and every key an adopter did not
|
|
34
|
+
* mention keeps flowing from the package. That is what makes an override a merge rather than a fork,
|
|
35
|
+
* and it is why kit catalogs are never copied into an adopter's repository.
|
|
36
|
+
*
|
|
37
|
+
* Errors are not in any of those layers and do not need to be: the payload already carries its English
|
|
38
|
+
* `message`, and a translating client renders `t.maybe(payload.code, payload.params) ?? payload.message`. So
|
|
39
|
+
* the kit ships translations for the 120 codes and no English duplicate of what is already on the wire.
|
|
40
|
+
*/
|
|
41
|
+
function i18n(config = {}) {
|
|
42
|
+
const resolved = I18nConfig.parse(config);
|
|
43
|
+
let composedMessages = {};
|
|
44
|
+
const layersFor = (locale) => [
|
|
45
|
+
resolved.messages[locale],
|
|
46
|
+
resolved.messages[resolved.defaultLocale],
|
|
47
|
+
KIT_CATALOGS[locale],
|
|
48
|
+
composedMessages[locale],
|
|
49
|
+
composedMessages[resolved.defaultLocale],
|
|
50
|
+
composedMessages[DEFAULT_LOCALE]
|
|
51
|
+
];
|
|
52
|
+
const capability = defineCapability({
|
|
53
|
+
name: "i18n",
|
|
54
|
+
version: PACKAGE_VERSION,
|
|
55
|
+
config: I18nConfig,
|
|
56
|
+
/**
|
|
57
|
+
* Merge every composed capability's English once, at assembly.
|
|
58
|
+
*
|
|
59
|
+
* Here rather than at construction because a capability sees only itself when it is built, and the
|
|
60
|
+
* kit's English is contributed by the capabilities that own it — `@pithy-sh/email`'s template copy
|
|
61
|
+
* under `email/`, an adopter's own `app` capability under theirs. `composeMessages` enforces that
|
|
62
|
+
* each may only write keys under its own domain.
|
|
63
|
+
*/
|
|
64
|
+
compose: ({ capabilities }) => {
|
|
65
|
+
composedMessages = composeMessages(capabilities);
|
|
66
|
+
},
|
|
67
|
+
middleware: [i18nMiddleware(resolved, layersFor)],
|
|
68
|
+
/**
|
|
69
|
+
* Locale **metadata**, never catalogs. `renderVirtualModule` inlines this as a `JSON.stringify`
|
|
70
|
+
* literal in the main chunk, so a catalog here would be downloaded by every reader in every
|
|
71
|
+
* language before first paint and would defeat the per-locale splitting the design rests on.
|
|
72
|
+
*
|
|
73
|
+
* The return type is {@link I18nClientProjection} — declared, not inferred, so a dropped field is a
|
|
74
|
+
* compile error here rather than a browser's problem.
|
|
75
|
+
*/
|
|
76
|
+
client: () => ({
|
|
77
|
+
enabled: true,
|
|
78
|
+
supportedLocales: [...resolved.supportedLocales],
|
|
79
|
+
defaultLocale: resolved.defaultLocale,
|
|
80
|
+
queryParam: resolved.queryParam,
|
|
81
|
+
storageKey: resolved.storageKey,
|
|
82
|
+
browserResolvers: [...resolved.browserResolvers],
|
|
83
|
+
exceptions: { ...resolved.exceptions }
|
|
84
|
+
}),
|
|
85
|
+
/**
|
|
86
|
+
* Catalog coverage, as `pithy doctor`'s local tier rather than as a `pithy i18n check` command.
|
|
87
|
+
*
|
|
88
|
+
* It is offline, project-file-only, and a local finding already fails `doctor`'s exit code — which
|
|
89
|
+
* is exactly what the command would have been, at the cost of a new page in `docs/commands/`, a
|
|
90
|
+
* row in five exact-count CLI gates, and a byte-pinned re-paste of `pithy --help`.
|
|
91
|
+
*/
|
|
92
|
+
settings: i18nSettings(resolved, () => composedMessages),
|
|
93
|
+
requiredBindings: []
|
|
94
|
+
});
|
|
95
|
+
const composed = Object.assign(capability, {
|
|
96
|
+
i18nConfig: resolved,
|
|
97
|
+
layersFor
|
|
98
|
+
});
|
|
99
|
+
Object.defineProperty(composed, "composedMessages", {
|
|
100
|
+
get: () => composedMessages,
|
|
101
|
+
enumerable: true
|
|
102
|
+
});
|
|
103
|
+
return composed;
|
|
104
|
+
}
|
|
105
|
+
/** Whether a capability is the i18n capability — carries its resolved config and composed catalogs. */
|
|
106
|
+
function isI18nCapability(capability) {
|
|
107
|
+
return capability.name === "i18n" && "i18nConfig" in capability;
|
|
108
|
+
}
|
|
109
|
+
//#endregion
|
|
110
|
+
export { i18n, isI18nCapability };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { MessageCatalog } from "@pithy-sh/core/src/i18n/catalog";
|
|
2
|
+
/**
|
|
3
|
+
* Whether the kit ships a catalog for `locale`. English answers `false` — it is the baked fallback.
|
|
4
|
+
*
|
|
5
|
+
* **`Object.hasOwn`, never `in`.** The argument is a language tag, and a language tag is whatever a
|
|
6
|
+
* reader put after `?lang=`. `"__proto__" in LOADERS` is `true` — the accessor is on `Object.prototype`
|
|
7
|
+
* — so `in` answered yes for a locale the kit has never written, and {@link loadKitCatalog} then read
|
|
8
|
+
* `Object.prototype` back as a loader and called it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hasKitCatalog(locale: string): boolean;
|
|
11
|
+
/** The kit's catalog for `locale`, or an empty one when the kit writes nothing in that language. */
|
|
12
|
+
export declare function loadKitCatalog(locale: string): Promise<MessageCatalog>;
|
|
13
|
+
//# sourceMappingURL=browser.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"browser.d.ts","sourceRoot":"","sources":["../../src/catalogs/browser.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAkBtE;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAErD;AAED,oGAAoG;AACpG,wBAAsB,cAAc,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAI5E"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/catalogs/browser.ts
|
|
2
|
+
/**
|
|
3
|
+
* The kit's catalogs, as one dynamic import per locale.
|
|
4
|
+
*
|
|
5
|
+
* **A static map of thunks, not a computed specifier.** `import(\`./es/${locale}\`)` is a specifier no
|
|
6
|
+
* bundler can resolve, so it either fails the build or pulls every match into one chunk; written out,
|
|
7
|
+
* Rollup emits exactly one chunk per locale and a reader downloads only their own. That is the whole
|
|
8
|
+
* reason catalogs are not on the `virtual:pithy/i18n` projection, which `renderVirtualModule` inlines
|
|
9
|
+
* as a `JSON.stringify` literal into the main chunk.
|
|
10
|
+
*
|
|
11
|
+
* English is absent for the same reason it is absent from {@link KIT_CATALOGS}: a copied screen already
|
|
12
|
+
* carries the English it was scaffolded with, and that is the only catalog that survives being copied.
|
|
13
|
+
*/
|
|
14
|
+
const LOADERS = { es: () => import("./es/index.js").then((module) => module.es) };
|
|
15
|
+
/**
|
|
16
|
+
* Whether the kit ships a catalog for `locale`. English answers `false` — it is the baked fallback.
|
|
17
|
+
*
|
|
18
|
+
* **`Object.hasOwn`, never `in`.** The argument is a language tag, and a language tag is whatever a
|
|
19
|
+
* reader put after `?lang=`. `"__proto__" in LOADERS` is `true` — the accessor is on `Object.prototype`
|
|
20
|
+
* — so `in` answered yes for a locale the kit has never written, and {@link loadKitCatalog} then read
|
|
21
|
+
* `Object.prototype` back as a loader and called it.
|
|
22
|
+
*/
|
|
23
|
+
function hasKitCatalog(locale) {
|
|
24
|
+
return Object.hasOwn(LOADERS, locale);
|
|
25
|
+
}
|
|
26
|
+
/** The kit's catalog for `locale`, or an empty one when the kit writes nothing in that language. */
|
|
27
|
+
async function loadKitCatalog(locale) {
|
|
28
|
+
if (!hasKitCatalog(locale)) return {};
|
|
29
|
+
const load = LOADERS[locale];
|
|
30
|
+
return load ? await load() : {};
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
export { hasKitCatalog, loadKitCatalog };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MessageCatalog } from "@pithy-sh/core/src/i18n/catalog";
|
|
2
|
+
/**
|
|
3
|
+
* Every kit error code, in Spanish. **Keyed by the code itself** — for an error the catalog key *is*
|
|
4
|
+
* the code, so there is no second identifier to keep in sync and `KitErrorCode` is the exhaustive
|
|
5
|
+
* checklist this file has to cover.
|
|
6
|
+
*
|
|
7
|
+
* There is no English twin. A payload already carries its English `message` on the wire, and a
|
|
8
|
+
* translating client renders `t.maybe(payload.code, payload.params) ?? payload.message` — so English is the
|
|
9
|
+
* fallback by construction and a duplicate of it here would be a second place for one sentence to
|
|
10
|
+
* drift.
|
|
11
|
+
*
|
|
12
|
+
* **These are the caller's words, not the operator's.** `message` is the only field on the wire;
|
|
13
|
+
* `action` names `pithy` commands and repository files and never leaves the terminal, so nothing here
|
|
14
|
+
* translates one. Placeholders match the `params` the throw sites pass.
|
|
15
|
+
*
|
|
16
|
+
* **Not one sentence below carries a placeholder, and that is a finding rather than a shortcut.** No
|
|
17
|
+
* throw site in this repository passes `params` today — `git grep "params: {"` over every package's
|
|
18
|
+
* `src` tree returns Workflow dispatch parameters and nothing else — so a `{name}` written here would
|
|
19
|
+
* have no value to receive and `interpolate` leaves an unsupplied placeholder as written. `Sala {code}
|
|
20
|
+
* llena.` on a caller's screen is worse than `Esa sala ya no admite más jugadores.`, which is why the
|
|
21
|
+
* generic clause wins until the English side has a value to send. `./errors.test.ts` pins the absence,
|
|
22
|
+
* and says there what would replace the pin the day a throw site starts naming one.
|
|
23
|
+
*
|
|
24
|
+
* Peninsular-neutral: one `es` serves es-ES, es-MX and es-AR, so a regionalism that reads as home to
|
|
25
|
+
* one of them and as foreign to the other two is the wrong word even when it is the better word. Where
|
|
26
|
+
* the English distinguishes an internal cause the Spanish states only the part a caller can act on —
|
|
27
|
+
* a caller who cannot read our logs is not helped by learning that a binding was missing.
|
|
28
|
+
*/
|
|
29
|
+
export declare const esErrors: MessageCatalog;
|
|
30
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../../src/catalogs/es/errors.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iCAAiC,CAAC;AAEtE;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,eAAO,MAAM,QAAQ,EAAE,cAkKtB,CAAC"}
|