@pithy-sh/i18n 0.12.0 → 0.14.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/dist/capability.d.ts +1 -1
- package/dist/capability.d.ts.map +1 -1
- package/dist/capability.js +1 -1
- package/dist/catalogs/es/errors.d.ts.map +1 -1
- package/dist/catalogs/es/errors.js +18 -0
- package/dist/catalogs/es/screens.d.ts.map +1 -1
- package/dist/catalogs/es/screens.js +79 -0
- package/dist/config/config.d.ts.map +1 -1
- package/dist/config/config.js +0 -1
- package/dist/native/environment.d.ts +16 -0
- package/dist/native/environment.d.ts.map +1 -0
- package/dist/native/environment.js +56 -0
- package/dist/native/useNativeDirection.d.ts +13 -0
- package/dist/native/useNativeDirection.d.ts.map +1 -0
- package/dist/native/useNativeDirection.js +25 -0
- package/dist/react/browserLocaleEnvironment.d.ts +6 -0
- package/dist/react/browserLocaleEnvironment.d.ts.map +1 -0
- package/dist/react/browserLocaleEnvironment.js +28 -0
- package/dist/react/localeEnvironment.d.ts +29 -0
- package/dist/react/localeEnvironment.d.ts.map +1 -0
- package/dist/react/localeEnvironment.js +2 -0
- package/dist/react/useDocumentLocale.d.ts +10 -0
- package/dist/react/useDocumentLocale.d.ts.map +1 -0
- package/dist/react/useDocumentLocale.js +23 -0
- package/dist/react/useNegotiatedLocale.d.ts +16 -3
- package/dist/react/useNegotiatedLocale.d.ts.map +1 -1
- package/dist/react/useNegotiatedLocale.js +30 -37
- package/dist/resolve/browser.d.ts +2 -1
- package/dist/resolve/browser.d.ts.map +1 -1
- package/dist/resolve/browser.js +2 -1
- package/dist/version.generated.d.ts +1 -1
- package/dist/version.generated.js +1 -1
- package/package.json +20 -3
- package/src/capability.ts +5 -9
- package/src/catalogs/es/errors.ts +20 -0
- package/src/catalogs/es/screens.ts +94 -1
- package/src/config/config.ts +0 -1
- package/src/native/environment.ts +57 -0
- package/src/native/useNativeDirection.ts +24 -0
- package/src/react/browserLocaleEnvironment.ts +32 -0
- package/src/react/localeEnvironment.ts +31 -0
- package/src/react/useDocumentLocale.ts +18 -0
- package/src/react/useNegotiatedLocale.ts +52 -51
- package/src/resolve/browser.ts +2 -1
- package/src/version.generated.ts +1 -1
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
import type { MessageCatalog } from "@pithy-sh/core/src/i18n/catalog";
|
|
5
5
|
import { localeDirection } from "@pithy-sh/core/src/i18n/locale";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { useRead } from "@pithy-sh/ui-core/src/read";
|
|
7
|
+
import { useCallback, useMemo, useState } from "react";
|
|
8
8
|
import { loadKitCatalog } from "../catalogs/browser";
|
|
9
9
|
import type { I18nClientProjection } from "../client/projection";
|
|
10
10
|
import { resolveBrowserLocale } from "../resolve/browser";
|
|
11
|
+
import { browserLocaleEnvironment } from "./browserLocaleEnvironment";
|
|
12
|
+
import type { LocaleEnvironment } from "./localeEnvironment";
|
|
11
13
|
import type { TranslatorSource } from "./translator";
|
|
12
14
|
|
|
13
15
|
/** What the hook hands back: what to mount, what was chosen, and how to choose again. */
|
|
@@ -20,8 +22,14 @@ export interface NegotiatedLocale {
|
|
|
20
22
|
readonly source: TranslatorSource | null;
|
|
21
23
|
/** The catalog locale in force — the words the app has. */
|
|
22
24
|
readonly locale: string;
|
|
23
|
-
/** Choose a language. Remembers it on this device and
|
|
25
|
+
/** Choose a language. Remembers it on this device and applies it through the environment. */
|
|
24
26
|
readonly choose: (locale: string) => void;
|
|
27
|
+
/**
|
|
28
|
+
* `true` when the locale in force lays out in the other direction from the one this launch started with, and the
|
|
29
|
+
* new direction takes effect only after the app reloads. React Native's rule; a browser never answers `true`. A
|
|
30
|
+
* screen says so rather than flip half a layout.
|
|
31
|
+
*/
|
|
32
|
+
readonly reloadRequired: boolean;
|
|
25
33
|
}
|
|
26
34
|
|
|
27
35
|
/** What the caller knows that the browser cannot work out for itself. */
|
|
@@ -50,6 +58,12 @@ export interface NegotiatedLocaleOptions {
|
|
|
50
58
|
* catch it here where you know what your API meant.
|
|
51
59
|
*/
|
|
52
60
|
readonly persist?: (locale: string) => void | Promise<void>;
|
|
61
|
+
/**
|
|
62
|
+
* Where the starting signals come from, where a choice is remembered and how the locale is applied. The browser's
|
|
63
|
+
* by default; pass `nativeLocaleEnvironment` from `@pithy-sh/i18n/src/native/environment` on React Native. Keep it
|
|
64
|
+
* the same object for the life of the component.
|
|
65
|
+
*/
|
|
66
|
+
readonly environment?: LocaleEnvironment;
|
|
53
67
|
}
|
|
54
68
|
|
|
55
69
|
/**
|
|
@@ -62,8 +76,8 @@ export interface NegotiatedLocaleOptions {
|
|
|
62
76
|
const UNNEGOTIATED_LOCALE = "en";
|
|
63
77
|
|
|
64
78
|
/**
|
|
65
|
-
* Negotiate the reader's locale
|
|
66
|
-
*
|
|
79
|
+
* Negotiate the reader's locale, load that locale's catalog, and keep the platform in step: the document in a
|
|
80
|
+
* browser, `I18nManager` on React Native, through `options.environment`.
|
|
67
81
|
*
|
|
68
82
|
* **It takes what a browser holds.** `projection` is `virtual:pithy/i18n` — locale metadata and nothing
|
|
69
83
|
* else — so a screen reads the chain order **this project** configured rather than one the front end
|
|
@@ -91,24 +105,23 @@ export function useNegotiatedLocale(
|
|
|
91
105
|
// and every hook below runs either way and answers for it rather than being skipped — a hook behind a
|
|
92
106
|
// condition is the one thing React does not allow.
|
|
93
107
|
const config = projection.enabled ? projection : null;
|
|
94
|
-
// The same fact as a boolean, and the
|
|
95
|
-
//
|
|
96
|
-
// caller is free to hand the hook a fresh object every render (`{ ...i18nConfig }`, or a parse in a
|
|
97
|
-
// component body). An effect keyed on that identity re-runs on every render; the catalog effect sets
|
|
98
|
-
// state, so it re-renders itself, forever, with no error anywhere to read. Both effects want nothing
|
|
99
|
-
// from `config` but whether it is there, and a boolean is stable by value however the caller spells
|
|
100
|
-
// its projection.
|
|
108
|
+
// The same fact as a boolean, which the catalog read and the document hook key on rather than `config`. A caller
|
|
109
|
+
// is free to hand the hook a fresh projection every render, and a boolean is stable by value however it is spelled.
|
|
101
110
|
const enabled = projection.enabled;
|
|
111
|
+
const environment = options.environment ?? browserLocaleEnvironment;
|
|
102
112
|
|
|
113
|
+
const signals = environment.useSignals(config);
|
|
114
|
+
// A signal still being read, such as storage on React Native: nothing is negotiated, fetched or applied until it
|
|
115
|
+
// answers, so a remembered choice is not preceded by a flash of another language.
|
|
116
|
+
const pending = enabled && signals === undefined;
|
|
103
117
|
const resolved = useMemo(
|
|
104
118
|
() =>
|
|
105
|
-
config === null
|
|
119
|
+
config === null || signals === undefined
|
|
106
120
|
? null
|
|
107
|
-
: resolveBrowserLocale({ ...
|
|
108
|
-
[config, options.account],
|
|
121
|
+
: resolveBrowserLocale({ ...signals, account: options.account }, config),
|
|
122
|
+
[config, signals, options.account],
|
|
109
123
|
);
|
|
110
124
|
const [chosen, setChosen] = useState<string | null>(null);
|
|
111
|
-
const [kit, setKit] = useState<{ locale: string; catalog: MessageCatalog } | null>(null);
|
|
112
125
|
|
|
113
126
|
const locale = chosen ?? resolved?.catalogLocale ?? UNNEGOTIATED_LOCALE;
|
|
114
127
|
// A chosen locale is a catalog locale, so it carries no region; the negotiated one may be more
|
|
@@ -119,28 +132,18 @@ export function useNegotiatedLocale(
|
|
|
119
132
|
// project without the capability could mean.
|
|
120
133
|
const defaultLocale = config?.defaultLocale ?? UNNEGOTIATED_LOCALE;
|
|
121
134
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
// "declares a language it does not speak" failure by another road: a screen reader believes the
|
|
135
|
-
// attribute and mispronounces English. An empty catalog mounts the provider, so every screen
|
|
136
|
-
// falls through to the English it was scaffolded with, which is what is actually on screen.
|
|
137
|
-
.catch(() => {
|
|
138
|
-
if (live) setKit({ locale, catalog: {} });
|
|
139
|
-
});
|
|
140
|
-
return () => {
|
|
141
|
-
live = false;
|
|
142
|
-
};
|
|
143
|
-
}, [enabled, locale]);
|
|
135
|
+
// No chunk is asked for with the capability uncomposed, which is what keeps `source` null there. A per-locale chunk
|
|
136
|
+
// that will not load — a 404 after a deploy, an offline tab — mounts an empty catalog rather than leaving the
|
|
137
|
+
// provider unmounted under a `lang` already changed on the document: every screen then falls through to the
|
|
138
|
+
// English it was scaffolded with, which is what is actually on screen.
|
|
139
|
+
const kit = useRead(
|
|
140
|
+
`catalog:${locale}`,
|
|
141
|
+
async (): Promise<{ locale: string; catalog: MessageCatalog }> => ({
|
|
142
|
+
locale,
|
|
143
|
+
catalog: await loadKitCatalog(locale).catch((): MessageCatalog => ({})),
|
|
144
|
+
}),
|
|
145
|
+
{ skip: !enabled || pending },
|
|
146
|
+
).value;
|
|
144
147
|
|
|
145
148
|
// Derived from the locale in force, never taken off `resolved`. `resolveBrowserLocale` answers for
|
|
146
149
|
// what the *chain* negotiated, and `choose` moves past it: a reader who picks Arabic from a language
|
|
@@ -150,13 +153,7 @@ export function useNegotiatedLocale(
|
|
|
150
153
|
// byte-identical to what it answered before.
|
|
151
154
|
const direction = localeDirection(locale);
|
|
152
155
|
|
|
153
|
-
|
|
154
|
-
// Nothing negotiated it, so nothing declares it. A project without the capability keeps the `lang`
|
|
155
|
-
// its `index.html` shipped — restamping the document with a locale no chain chose would be the
|
|
156
|
-
// "declares a language it does not speak" failure with the negotiation removed rather than added.
|
|
157
|
-
if (!enabled) return;
|
|
158
|
-
applyDocumentLocale(locale, direction);
|
|
159
|
-
}, [enabled, locale, direction]);
|
|
156
|
+
const reloadRequired = environment.useApply(enabled && !pending, locale, direction);
|
|
160
157
|
|
|
161
158
|
const persist = options.persist;
|
|
162
159
|
const choose = useCallback(
|
|
@@ -164,9 +161,13 @@ export function useNegotiatedLocale(
|
|
|
164
161
|
// No languages to choose between, and no `storageKey` to remember one under.
|
|
165
162
|
if (config === null) return;
|
|
166
163
|
if (!config.supportedLocales.includes(next)) return;
|
|
167
|
-
// The device first, always: it is
|
|
168
|
-
//
|
|
169
|
-
|
|
164
|
+
// The device first, always: it is what answers on the next visit if the account write does not land. Not
|
|
165
|
+
// awaited, and a failure costs only the memory of the choice, never the choice.
|
|
166
|
+
try {
|
|
167
|
+
Promise.resolve(environment.remember(next, config)).catch(() => undefined);
|
|
168
|
+
} catch {
|
|
169
|
+
// A store that throws before it answers. The reader still gets the language.
|
|
170
|
+
}
|
|
170
171
|
setChosen(next);
|
|
171
172
|
// Then the account, when the app knows who is reading. Not awaited — a language switch is a
|
|
172
173
|
// render, not a round trip, and nothing on screen should wait for a preference.
|
|
@@ -178,13 +179,13 @@ export function useNegotiatedLocale(
|
|
|
178
179
|
// reporting is a question about the adopter's own API, so it is answered inside `persist`.
|
|
179
180
|
Promise.resolve(persist?.(next)).catch(() => undefined);
|
|
180
181
|
},
|
|
181
|
-
[config, persist],
|
|
182
|
+
[config, persist, environment],
|
|
182
183
|
);
|
|
183
184
|
|
|
184
185
|
const source = useMemo<TranslatorSource | null>(() => {
|
|
185
186
|
// Only once the catalog for *this* locale has landed. Mounting the previous locale's catalog under
|
|
186
187
|
// the new locale's tag is a page that says it is Spanish and reads English. This is also the one
|
|
187
|
-
// test the uncomposed case has to pass: nothing ever loads a catalog there, so `kit` stays
|
|
188
|
+
// test the uncomposed case has to pass: nothing ever loads a catalog there, so `kit` stays undefined
|
|
188
189
|
// and no provider is ever mounted over screens already rendering their baked English.
|
|
189
190
|
if (kit?.locale !== locale) return null;
|
|
190
191
|
return {
|
|
@@ -194,5 +195,5 @@ export function useNegotiatedLocale(
|
|
|
194
195
|
};
|
|
195
196
|
}, [kit, locale, formattingLocale, options.messages, defaultLocale]);
|
|
196
197
|
|
|
197
|
-
return { source, locale, choose };
|
|
198
|
+
return { source, locale, choose, reloadRequired };
|
|
198
199
|
}
|
package/src/resolve/browser.ts
CHANGED
|
@@ -88,7 +88,8 @@ export interface BrowserLocaleSignals {
|
|
|
88
88
|
/**
|
|
89
89
|
* The reader's locale, by the configured browser chain.
|
|
90
90
|
*
|
|
91
|
-
* Default order: `?lang=`, the account, local storage, the
|
|
91
|
+
* Default order: `?lang=`, the account, local storage, the browser's own languages, the server's answer,
|
|
92
|
+
* the project default.
|
|
92
93
|
*
|
|
93
94
|
* **The account outranks the device.** `pithy_auth_users.locale` is where a person's locale lives, so a
|
|
94
95
|
* signed-in reader who picks a language on one device must not silently be reading another language on
|
package/src/version.generated.ts
CHANGED