@owlmeans/client-i18n 0.1.18-rc.40 → 0.1.18-rc.41
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/README.md +1 -1
- package/agent-meta/manifest.json +2 -2
- package/agent-meta/skills/client-i18n/SKILL.md +18 -5
- package/build/index.d.ts +1 -1
- package/build/index.d.ts.map +1 -1
- package/build/index.js +1 -1
- package/build/index.js.map +1 -1
- package/build/utils/instance.d.ts +18 -0
- package/build/utils/instance.d.ts.map +1 -1
- package/build/utils/instance.js +62 -3
- package/build/utils/instance.js.map +1 -1
- package/package.json +2 -2
- package/src/index.ts +3 -1
- package/src/utils/instance.ts +68 -3
- package/tests/instance.spec.ts +72 -1
package/README.md
CHANGED
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 2,
|
|
3
3
|
"package": "@owlmeans/client-i18n",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-09-
|
|
4
|
+
"version": "0.1.18-rc.41",
|
|
5
|
+
"generatedAt": "2026-09-25T15:32:39.584Z",
|
|
6
6
|
"canonicalRepo": "https://github.com/owlmeans/common",
|
|
7
7
|
"entries": [
|
|
8
8
|
{
|
|
@@ -8,7 +8,7 @@ user-invocable: false
|
|
|
8
8
|
# @owlmeans/client-i18n
|
|
9
9
|
|
|
10
10
|
**Layer:** Client (React)
|
|
11
|
-
**Install:** `"@owlmeans/client-i18n": "^0.1.18-rc.
|
|
11
|
+
**Install:** `"@owlmeans/client-i18n": "^0.1.18-rc.41"` in `dependencies`
|
|
12
12
|
|
|
13
13
|
## Purpose
|
|
14
14
|
|
|
@@ -37,13 +37,16 @@ The instance itself, for a host that has to configure i18next before the tree re
|
|
|
37
37
|
|---|---|
|
|
38
38
|
| `useI18nInstance(config)` | The i18next instance for this application, memoised for the mount |
|
|
39
39
|
| `getI18nInstance(config)` | The same instance outside React |
|
|
40
|
-
| `setLanguage(lng)` | Async: load `lng`'s pack (`loadI18nLanguage`), then persist and switch — what `useLanguage`'s setter calls |
|
|
40
|
+
| `setLanguage(lng)` | Async: load `lng`'s pack (`loadI18nLanguage`), then persist (if the persistence guard allows) and switch — what `useLanguage`'s setter calls |
|
|
41
|
+
| `setLanguagePersistence(guard \| null)` | Install the guard that decides whether an explicit choice may be written to storage; `null` removes it. Call BEFORE `prepareI18n` |
|
|
42
|
+
| `persistLanguage()` | Write the explicit choice storage refused earlier, now that the guard allows; `false` when nothing waits or storage is still refused |
|
|
43
|
+
| `getExplicitLanguage()` | The language a person chose explicitly since this page loaded (stored or not), else `null` — what a host asks before applying a language that arrived some other way |
|
|
41
44
|
| `prepareI18n(config)` | Await the fallback language's loaders, then the initial language's; resolves to the language the instance will start in. Call once, before the first render |
|
|
42
45
|
| `resolveInitialLanguage(config)` | The persisted `owlmeans-lng` when it is in `supportedLngs`, else the browser's language (`preferredLanguageOf`), else `fallbackLng ?? defaultLng ?? DEFAULT_LNG` |
|
|
43
46
|
| `preferredLanguageOf(supportedLngs, preferred)` | Pure: the first preferred tag the config supports, exact or by base (`de-DE` → `de`), else `null` |
|
|
44
47
|
|
|
45
|
-
All
|
|
46
|
-
`resolveInitialLanguage` and
|
|
48
|
+
All of these live in `src/utils/instance.ts`; the package root re-exports `prepareI18n`,
|
|
49
|
+
`resolveInitialLanguage`, `setLanguage` and the three persistence functions above.
|
|
47
50
|
|
|
48
51
|
**There is exactly one instance per document**, created on first request and reused by every later
|
|
49
52
|
call whatever config is passed. So configuration is read once, at the first creation, and a plugin
|
|
@@ -101,7 +104,17 @@ the browser's language, then `fallbackLng`, instead of resolving nothing. Every
|
|
|
101
104
|
`navigator` access is guarded, so a browser that refuses site data (or no browser) still renders.
|
|
102
105
|
|
|
103
106
|
`tests/instance.spec.ts` pins `preferredLanguageOf` (base matching, browser order, exact tag first,
|
|
104
|
-
`null` when nothing matches).
|
|
107
|
+
`null` when nothing matches) and the persistence guard.
|
|
108
|
+
|
|
109
|
+
**Persistence is guarded, not assumed.** With no guard installed every explicit choice is written, as
|
|
110
|
+
always. An application that asks its visitors before remembering anything installs one
|
|
111
|
+
(`setLanguagePersistence(() => allowed)`; `@owlmeans/web-panel/consent`'s `installConsentLanguage()` is
|
|
112
|
+
the ready-made one for the cookie dialog's `functional` category — see the `consent` skill). While it
|
|
113
|
+
says no: `setLanguage` still switches the UI but writes nothing and remembers the refused choice
|
|
114
|
+
(`persistLanguage()` writes it when storage becomes allowed), and a value already in storage counts
|
|
115
|
+
as ABSENT at start-up (`resolveInitialLanguage`), so a language nobody was allowed to remember is not
|
|
116
|
+
used. Installing a guard also resets what this page has chosen so far — install it before anything
|
|
117
|
+
calls `setLanguage`.
|
|
105
118
|
|
|
106
119
|
`setLanguage(lng)` is async: it awaits `loadI18nLanguage(lng)`, then persists and calls i18next's
|
|
107
120
|
`changeLanguage` — before the instance exists it records the language the instance will start in
|
package/build/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export { I18nContext, composePrefix, useI18n, useI18nLib, useI18nApp, useLanguage } from './context.js';
|
|
3
|
-
export { prepareI18n, resolveInitialLanguage, setLanguage } from './utils/instance.js';
|
|
3
|
+
export { prepareI18n, resolveInitialLanguage, setLanguage, setLanguagePersistence, persistLanguage, getExplicitLanguage, } from './utils/instance.js';
|
|
4
4
|
//# sourceMappingURL=index.d.ts.map
|
package/build/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACvG,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACvG,OAAO,EACL,WAAW,EAAE,sBAAsB,EAAE,WAAW,EAAE,sBAAsB,EAAE,eAAe,EAAE,mBAAmB,GAC/G,MAAM,qBAAqB,CAAA"}
|
package/build/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export * from './types.js';
|
|
2
2
|
export { I18nContext, composePrefix, useI18n, useI18nLib, useI18nApp, useLanguage } from './context.js';
|
|
3
|
-
export { prepareI18n, resolveInitialLanguage, setLanguage } from './utils/instance.js';
|
|
3
|
+
export { prepareI18n, resolveInitialLanguage, setLanguage, setLanguagePersistence, persistLanguage, getExplicitLanguage, } from './utils/instance.js';
|
|
4
4
|
//# sourceMappingURL=index.js.map
|
package/build/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACvG,OAAO,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,YAAY,CAAA;AAC1B,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AACvG,OAAO,EACL,WAAW,EAAE,sBAAsB,EAAE,WAAW,EAAE,sBAAsB,EAAE,eAAe,EAAE,mBAAmB,GAC/G,MAAM,qBAAqB,CAAA"}
|
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
import type { i18n } from 'i18next';
|
|
2
2
|
import type { ClientConfig } from '@owlmeans/client-context';
|
|
3
|
+
/**
|
|
4
|
+
* The language a person chose explicitly since this page loaded (`setLanguage`), or `null`. What a
|
|
5
|
+
* host asks before it applies a language that arrived some other way — a link — which must never
|
|
6
|
+
* override what the person just picked.
|
|
7
|
+
*/
|
|
8
|
+
export declare const getExplicitLanguage: () => string | null;
|
|
9
|
+
/**
|
|
10
|
+
* Install (or, with `null`, remove) the guard that decides whether a language choice may be
|
|
11
|
+
* remembered. Call it BEFORE `prepareI18n`: the initial language is resolved from storage, and a
|
|
12
|
+
* guard that says no makes a stored value count as absent.
|
|
13
|
+
*/
|
|
14
|
+
export declare const setLanguagePersistence: (guard: (() => boolean) | null) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Write the explicit choice storage refused earlier, now that it may be — call it when the guard's
|
|
17
|
+
* answer turns to yes (the visitor granted functional storage). `false` when there was nothing
|
|
18
|
+
* waiting, storage is still refused, or the write failed.
|
|
19
|
+
*/
|
|
20
|
+
export declare const persistLanguage: () => boolean;
|
|
3
21
|
export declare const useI18nInstance: (config: ClientConfig) => i18n;
|
|
4
22
|
export declare const getI18nInstance: (config: ClientConfig) => i18n;
|
|
5
23
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/utils/instance.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAEnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;
|
|
1
|
+
{"version":3,"file":"instance.d.ts","sourceRoot":"","sources":["../../src/utils/instance.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAA;AAEnC,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAA;AA4B5D;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAAO,MAAM,GAAG,IAAsB,CAAA;AAItE;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,UAAW,CAAC,MAAM,OAAO,CAAC,GAAG,IAAI,KAAG,IAKtE,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,eAAe,QAAO,OAYlC,CAAA;AAED,eAAO,MAAM,eAAe,WAAY,YAAY,KAAG,IAItD,CAAA;AAED,eAAO,MAAM,eAAe,WAAY,YAAY,KAAG,IAC3B,CAAA;AAE5B;;;;GAIG;AACH,eAAO,MAAM,WAAW,QAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAA8B,CAAA;AA+BpF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,WAAkB,YAAY,KAAG,OAAO,CAAC,MAAM,CAatE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,WAAY,YAAY,KAAG,MAO7D,CAAA;AAgBD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,kBACf,SAAS,MAAM,EAAE,aAAa,SAAS,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAClF,MAAM,GAAG,IAUX,CAAA"}
|
package/build/utils/instance.js
CHANGED
|
@@ -8,6 +8,54 @@ let i18nInstance = null;
|
|
|
8
8
|
let preparedLng;
|
|
9
9
|
/** The language of the latest `setLanguage` call — an earlier call that finishes later yields to it. */
|
|
10
10
|
let requestedLng;
|
|
11
|
+
/**
|
|
12
|
+
* Whether an explicit language choice may be written to storage right now. Unset, storage is always
|
|
13
|
+
* allowed — an application with no notion of consent behaves exactly as before. A host that asks its
|
|
14
|
+
* visitors (`@owlmeans/web-panel/consent`'s `installConsentLanguage`) installs one that answers
|
|
15
|
+
* "has the visitor allowed functional storage?", so a language is not remembered on a device nobody
|
|
16
|
+
* asked about.
|
|
17
|
+
*/
|
|
18
|
+
let persistenceGuard = null;
|
|
19
|
+
/** The latest explicit choice that storage refused; written the moment storage is allowed. */
|
|
20
|
+
let refusedChoice = null;
|
|
21
|
+
/** The latest language a person chose explicitly in this page's life, stored or not. */
|
|
22
|
+
let explicitChoice = null;
|
|
23
|
+
/**
|
|
24
|
+
* The language a person chose explicitly since this page loaded (`setLanguage`), or `null`. What a
|
|
25
|
+
* host asks before it applies a language that arrived some other way — a link — which must never
|
|
26
|
+
* override what the person just picked.
|
|
27
|
+
*/
|
|
28
|
+
export const getExplicitLanguage = () => explicitChoice;
|
|
29
|
+
const persistenceAllowed = () => persistenceGuard == null || persistenceGuard();
|
|
30
|
+
/**
|
|
31
|
+
* Install (or, with `null`, remove) the guard that decides whether a language choice may be
|
|
32
|
+
* remembered. Call it BEFORE `prepareI18n`: the initial language is resolved from storage, and a
|
|
33
|
+
* guard that says no makes a stored value count as absent.
|
|
34
|
+
*/
|
|
35
|
+
export const setLanguagePersistence = (guard) => {
|
|
36
|
+
persistenceGuard = guard;
|
|
37
|
+
// A new guard starts a new account of what this page's visitor has chosen and what storage refused.
|
|
38
|
+
refusedChoice = null;
|
|
39
|
+
explicitChoice = null;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Write the explicit choice storage refused earlier, now that it may be — call it when the guard's
|
|
43
|
+
* answer turns to yes (the visitor granted functional storage). `false` when there was nothing
|
|
44
|
+
* waiting, storage is still refused, or the write failed.
|
|
45
|
+
*/
|
|
46
|
+
export const persistLanguage = () => {
|
|
47
|
+
if (refusedChoice == null || !persistenceAllowed()) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
localStorage.setItem(LNG_STORAGE_KEY, refusedChoice);
|
|
52
|
+
refusedChoice = null;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
catch (_) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
11
59
|
export const useI18nInstance = (config) => {
|
|
12
60
|
const instance = useMemo(() => createI18nInstance(config), []);
|
|
13
61
|
return instance;
|
|
@@ -30,10 +78,18 @@ const switchLanguage = async (lng, persist) => {
|
|
|
30
78
|
return;
|
|
31
79
|
}
|
|
32
80
|
if (persist) {
|
|
33
|
-
|
|
34
|
-
|
|
81
|
+
explicitChoice = lng;
|
|
82
|
+
if (persistenceAllowed()) {
|
|
83
|
+
refusedChoice = null;
|
|
84
|
+
try {
|
|
85
|
+
localStorage.setItem(LNG_STORAGE_KEY, lng);
|
|
86
|
+
}
|
|
87
|
+
catch (_) { /* noop in non-browser environments */ }
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
// Not remembered — but not forgotten either: the visitor may allow it a moment later.
|
|
91
|
+
refusedChoice = lng;
|
|
35
92
|
}
|
|
36
|
-
catch (_) { /* noop in non-browser environments */ }
|
|
37
93
|
}
|
|
38
94
|
if (i18nInstance == null) {
|
|
39
95
|
preparedLng = lng;
|
|
@@ -79,6 +135,9 @@ export const resolveInitialLanguage = (config) => {
|
|
|
79
135
|
};
|
|
80
136
|
const resolveFallbackLanguage = (config) => config.i18n?.fallbackLng ?? config.i18n?.defaultLng ?? DEFAULT_LNG;
|
|
81
137
|
const getPersistedLanguage = () => {
|
|
138
|
+
if (!persistenceAllowed()) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
82
141
|
try {
|
|
83
142
|
return localStorage.getItem(LNG_STORAGE_KEY);
|
|
84
143
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"instance.js","sourceRoot":"","sources":["../../src/utils/instance.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,cAAc,EAAE,MAAM,SAAS,CAAA;AACxC,OAAO,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAEhD,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAA;AAE/B,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,cAAc,EAAE,oBAAoB,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAA;AAEvH,MAAM,eAAe,GAAG,cAAc,CAAA;AAEtC,IAAI,YAAY,GAAgB,IAAI,CAAA;AAEpC,4FAA4F;AAC5F,IAAI,WAA+B,CAAA;AAEnC,wGAAwG;AACxG,IAAI,YAAgC,CAAA;AAEpC,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAoB,EAAQ,EAAE;IAC5D,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC,kBAAkB,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC,CAAA;IAE9D,OAAO,QAAQ,CAAA;AACjB,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAoB,EAAQ,EAAE,CAC5D,kBAAkB,CAAC,MAAM,CAAC,CAAA;AAE5B;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,GAAW,EAAiB,EAAE,CAAC,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;AAEpF;;;GAGG;AACH,MAAM,cAAc,GAAG,KAAK,EAAE,GAAW,EAAE,OAAgB,EAAiB,EAAE;IAC5E,YAAY,GAAG,GAAG,CAAA;IAClB,MAAM,gBAAgB,CAAC,GAAG,CAAC,CAAA;IAC3B,IAAI,YAAY,KAAK,GAAG,EAAE,CAAC;QACzB,OAAM;IACR,CAAC;IACD,IAAI,OAAO,EAAE,CAAC;QACZ,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"instance.js","sourceRoot":"","sources":["../../src/utils/instance.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@owlmeans/client-i18n",
|
|
3
|
-
"version": "0.1.18-rc.
|
|
3
|
+
"version": "0.1.18-rc.41",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"typescript": "^7.0.2"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@owlmeans/client": "^0.1.18-rc.
|
|
40
|
+
"@owlmeans/client": "^0.1.18-rc.42",
|
|
41
41
|
"@owlmeans/client-context": "^0.1.18-rc.39",
|
|
42
42
|
"@owlmeans/i18n": "^0.1.18-rc.37",
|
|
43
43
|
"i18next": "^26.2.0",
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
export * from './types.js'
|
|
3
3
|
export { I18nContext, composePrefix, useI18n, useI18nLib, useI18nApp, useLanguage } from './context.js'
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
prepareI18n, resolveInitialLanguage, setLanguage, setLanguagePersistence, persistLanguage, getExplicitLanguage,
|
|
6
|
+
} from './utils/instance.js'
|
package/src/utils/instance.ts
CHANGED
|
@@ -16,6 +16,61 @@ let preparedLng: string | undefined
|
|
|
16
16
|
/** The language of the latest `setLanguage` call — an earlier call that finishes later yields to it. */
|
|
17
17
|
let requestedLng: string | undefined
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Whether an explicit language choice may be written to storage right now. Unset, storage is always
|
|
21
|
+
* allowed — an application with no notion of consent behaves exactly as before. A host that asks its
|
|
22
|
+
* visitors (`@owlmeans/web-panel/consent`'s `installConsentLanguage`) installs one that answers
|
|
23
|
+
* "has the visitor allowed functional storage?", so a language is not remembered on a device nobody
|
|
24
|
+
* asked about.
|
|
25
|
+
*/
|
|
26
|
+
let persistenceGuard: (() => boolean) | null = null
|
|
27
|
+
|
|
28
|
+
/** The latest explicit choice that storage refused; written the moment storage is allowed. */
|
|
29
|
+
let refusedChoice: string | null = null
|
|
30
|
+
|
|
31
|
+
/** The latest language a person chose explicitly in this page's life, stored or not. */
|
|
32
|
+
let explicitChoice: string | null = null
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The language a person chose explicitly since this page loaded (`setLanguage`), or `null`. What a
|
|
36
|
+
* host asks before it applies a language that arrived some other way — a link — which must never
|
|
37
|
+
* override what the person just picked.
|
|
38
|
+
*/
|
|
39
|
+
export const getExplicitLanguage = (): string | null => explicitChoice
|
|
40
|
+
|
|
41
|
+
const persistenceAllowed = (): boolean => persistenceGuard == null || persistenceGuard()
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Install (or, with `null`, remove) the guard that decides whether a language choice may be
|
|
45
|
+
* remembered. Call it BEFORE `prepareI18n`: the initial language is resolved from storage, and a
|
|
46
|
+
* guard that says no makes a stored value count as absent.
|
|
47
|
+
*/
|
|
48
|
+
export const setLanguagePersistence = (guard: (() => boolean) | null): void => {
|
|
49
|
+
persistenceGuard = guard
|
|
50
|
+
// A new guard starts a new account of what this page's visitor has chosen and what storage refused.
|
|
51
|
+
refusedChoice = null
|
|
52
|
+
explicitChoice = null
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Write the explicit choice storage refused earlier, now that it may be — call it when the guard's
|
|
57
|
+
* answer turns to yes (the visitor granted functional storage). `false` when there was nothing
|
|
58
|
+
* waiting, storage is still refused, or the write failed.
|
|
59
|
+
*/
|
|
60
|
+
export const persistLanguage = (): boolean => {
|
|
61
|
+
if (refusedChoice == null || !persistenceAllowed()) {
|
|
62
|
+
return false
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
localStorage.setItem(LNG_STORAGE_KEY, refusedChoice)
|
|
66
|
+
refusedChoice = null
|
|
67
|
+
|
|
68
|
+
return true
|
|
69
|
+
} catch (_) {
|
|
70
|
+
return false
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
19
74
|
export const useI18nInstance = (config: ClientConfig): i18n => {
|
|
20
75
|
const instance = useMemo(() => createI18nInstance(config), [])
|
|
21
76
|
|
|
@@ -43,9 +98,16 @@ const switchLanguage = async (lng: string, persist: boolean): Promise<void> => {
|
|
|
43
98
|
return
|
|
44
99
|
}
|
|
45
100
|
if (persist) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
101
|
+
explicitChoice = lng
|
|
102
|
+
if (persistenceAllowed()) {
|
|
103
|
+
refusedChoice = null
|
|
104
|
+
try {
|
|
105
|
+
localStorage.setItem(LNG_STORAGE_KEY, lng)
|
|
106
|
+
} catch (_) { /* noop in non-browser environments */ }
|
|
107
|
+
} else {
|
|
108
|
+
// Not remembered — but not forgotten either: the visitor may allow it a moment later.
|
|
109
|
+
refusedChoice = lng
|
|
110
|
+
}
|
|
49
111
|
}
|
|
50
112
|
if (i18nInstance == null) {
|
|
51
113
|
preparedLng = lng
|
|
@@ -97,6 +159,9 @@ const resolveFallbackLanguage = (config: ClientConfig): string =>
|
|
|
97
159
|
config.i18n?.fallbackLng ?? config.i18n?.defaultLng ?? DEFAULT_LNG
|
|
98
160
|
|
|
99
161
|
const getPersistedLanguage = (): string | null => {
|
|
162
|
+
if (!persistenceAllowed()) {
|
|
163
|
+
return null
|
|
164
|
+
}
|
|
100
165
|
try {
|
|
101
166
|
return localStorage.getItem(LNG_STORAGE_KEY)
|
|
102
167
|
} catch (_) {
|
package/tests/instance.spec.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { afterEach, beforeEach } from 'bun:test'
|
|
3
|
+
import {
|
|
4
|
+
persistLanguage, preferredLanguageOf, resolveInitialLanguage, setLanguage, setLanguagePersistence,
|
|
5
|
+
} from '../src/utils/instance.js'
|
|
3
6
|
|
|
4
7
|
const SUPPORTED = ['en', 'pl', 'ru', 'be', 'uk', 'es', 'de', 'fr']
|
|
5
8
|
|
|
@@ -25,3 +28,71 @@ describe('preferredLanguageOf — the first-visit language', () => {
|
|
|
25
28
|
expect(preferredLanguageOf(SUPPORTED, [null, undefined, ''])).toBeNull()
|
|
26
29
|
})
|
|
27
30
|
})
|
|
31
|
+
|
|
32
|
+
describe('language persistence guard — a choice is remembered only while storage is allowed', () => {
|
|
33
|
+
const store = new Map<string, string>()
|
|
34
|
+
const config = { i18n: { supportedLngs: SUPPORTED, fallbackLng: 'en' } } as never
|
|
35
|
+
let allowed = false
|
|
36
|
+
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
store.clear()
|
|
39
|
+
allowed = false
|
|
40
|
+
;(globalThis as any).localStorage = {
|
|
41
|
+
getItem: (k: string) => store.get(k) ?? null,
|
|
42
|
+
setItem: (k: string, v: string) => { store.set(k, v) },
|
|
43
|
+
removeItem: (k: string) => { store.delete(k) },
|
|
44
|
+
}
|
|
45
|
+
setLanguagePersistence(() => allowed)
|
|
46
|
+
})
|
|
47
|
+
afterEach(() => { setLanguagePersistence(null) })
|
|
48
|
+
|
|
49
|
+
test('with the guard saying no, an explicit choice is not written', async () => {
|
|
50
|
+
await setLanguage('pl')
|
|
51
|
+
|
|
52
|
+
expect(store.has('owlmeans-lng')).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('the refused choice is written the moment storage is allowed', async () => {
|
|
56
|
+
await setLanguage('pl')
|
|
57
|
+
expect(persistLanguage()).toBe(false)
|
|
58
|
+
|
|
59
|
+
allowed = true
|
|
60
|
+
|
|
61
|
+
expect(persistLanguage()).toBe(true)
|
|
62
|
+
expect(store.get('owlmeans-lng')).toBe('pl')
|
|
63
|
+
expect(persistLanguage()).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
test('the latest refused choice is the one that is written', async () => {
|
|
67
|
+
await setLanguage('pl')
|
|
68
|
+
await setLanguage('de')
|
|
69
|
+
allowed = true
|
|
70
|
+
persistLanguage()
|
|
71
|
+
|
|
72
|
+
expect(store.get('owlmeans-lng')).toBe('de')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test('with the guard saying yes, a choice is written at once, as before', async () => {
|
|
76
|
+
allowed = true
|
|
77
|
+
await setLanguage('fr')
|
|
78
|
+
|
|
79
|
+
expect(store.get('owlmeans-lng')).toBe('fr')
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('a stored language counts as absent while the guard says no, and is used once it says yes', () => {
|
|
83
|
+
store.set('owlmeans-lng', 'de')
|
|
84
|
+
|
|
85
|
+
expect(resolveInitialLanguage(config)).not.toBe('de')
|
|
86
|
+
|
|
87
|
+
allowed = true
|
|
88
|
+
expect(resolveInitialLanguage(config)).toBe('de')
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('with no guard installed nothing changes: choices are written and read', async () => {
|
|
92
|
+
setLanguagePersistence(null)
|
|
93
|
+
await setLanguage('es')
|
|
94
|
+
|
|
95
|
+
expect(store.get('owlmeans-lng')).toBe('es')
|
|
96
|
+
expect(resolveInitialLanguage(config)).toBe('es')
|
|
97
|
+
})
|
|
98
|
+
})
|