@owlmeans/client-i18n 0.1.18-rc.39 → 0.1.18-rc.40

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 CHANGED
@@ -12,7 +12,7 @@ React i18next adapter that loads translations registered via `@owlmeans/i18n` in
12
12
  ## Installation
13
13
 
14
14
  ```bash
15
- bun add @owlmeans/client-i18n@^0.1.18-rc.39
15
+ bun add @owlmeans/client-i18n@^0.1.18-rc.40
16
16
  ```
17
17
 
18
18
  ## Usage
@@ -68,7 +68,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
68
68
  your project's skill store (`.agents/skills/`):
69
69
 
70
70
  ```sh
71
- npx @owlmeans/agent-skills@^0.1.18-rc.38
71
+ npx @owlmeans/agent-skills@^0.1.18-rc.39
72
72
  ```
73
73
 
74
74
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@owlmeans/client-i18n",
4
- "version": "0.1.18-rc.39",
5
- "generatedAt": "2026-09-23T19:57:40.687Z",
4
+ "version": "0.1.18-rc.40",
5
+ "generatedAt": "2026-09-24T14:00:35.045Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: client-i18n
3
- description: How to use @owlmeans/client-i18n — React i18n context built on i18next. Auto-invoked when setting up translation in a React app, using translation hooks, or working with language switching.
3
+ description: How to use @owlmeans/client-i18n — React i18n context built on i18next. Auto-invoked when setting up translation in a React app, using translation hooks, working with language switching, or awaiting deferred language packs with prepareI18n.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,11 +8,11 @@ user-invocable: false
8
8
  # @owlmeans/client-i18n
9
9
 
10
10
  **Layer:** Client (React)
11
- **Install:** `"@owlmeans/client-i18n": "^0.1.18-rc.39"` in `dependencies`
11
+ **Install:** `"@owlmeans/client-i18n": "^0.1.18-rc.40"` in `dependencies`
12
12
 
13
13
  ## Purpose
14
14
 
15
- Wraps i18next + react-i18next, lazily loads registered bundles from `@owlmeans/i18n` storage into the i18next instance, and provides hooks for translating strings.
15
+ Wraps i18next + react-i18next, lazily loads registered bundles from `@owlmeans/i18n` storage into the i18next instance, awaits deferred language packs (`prepareI18n`, `setLanguage`), and provides hooks for translating strings.
16
16
 
17
17
  ## Key Exports
18
18
 
@@ -22,7 +22,8 @@ Wraps i18next + react-i18next, lazily loads registered bundles from `@owlmeans/i
22
22
  | `useI18nLib(resource, prefix?)` | Hook for library strings (ns=`'lib'`) |
23
23
  | `useI18nApp(resource?, prefix?)` | Hook for app strings (ns=resource name; defaults to `context.cfg.service`) |
24
24
  | `useI18n(resource, ns?, prefix?)` | Low-level hook with explicit ns — use when ns ≠ resource |
25
- | `useLanguage()` | `[currentLng, setLng]` — read and switch the active language |
25
+ | `useLanguage()` | `[currentLng, setLng]` — the i18next instance's language, and an async switch — see Language persistence |
26
+ | `prepareI18n`, `resolveInitialLanguage`, `setLanguage` | Re-exported from `./utils` — see below |
26
27
  | `composePrefix(parent?, child?)` | Canonical dot-join for prefix chaining; never call manually in panel code — used internally |
27
28
  | `I18nBaseProps` | `{ resource?, ns?, prefix?, suppress? }` |
28
29
  | `I18nProps` | `{ i18n?: I18nBaseProps }` |
@@ -36,19 +37,26 @@ The instance itself, for a host that has to configure i18next before the tree re
36
37
  |---|---|
37
38
  | `useI18nInstance(config)` | The i18next instance for this application, memoised for the mount |
38
39
  | `getI18nInstance(config)` | The same instance outside React |
39
- | `setLanguage(lng)` | Persist and switch, without a component — what `useLanguage`'s setter calls |
40
+ | `setLanguage(lng)` | Async: load `lng`'s pack (`loadI18nLanguage`), then persist and switch — what `useLanguage`'s setter calls |
41
+ | `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
+ | `resolveInitialLanguage(config)` | The persisted `owlmeans-lng` when it is in `supportedLngs`, else the browser's language (`preferredLanguageOf`), else `fallbackLng ?? defaultLng ?? DEFAULT_LNG` |
40
43
  | `preferredLanguageOf(supportedLngs, preferred)` | Pure: the first preferred tag the config supports, exact or by base (`de-DE` → `de`), else `null` |
41
44
 
45
+ All six live in `src/utils/instance.ts`; the package root re-exports `prepareI18n`,
46
+ `resolveInitialLanguage` and `setLanguage`.
47
+
42
48
  **There is exactly one instance per document**, created on first request and reused by every later
43
49
  call whatever config is passed. So configuration is read once, at the first creation, and a plugin
44
50
  is installed on the instance rather than passed to a second factory.
45
51
 
46
52
  **The instance is initialized at creation with an explicit `lng`**, which is why the language is
47
- resolved there and not by a plugin: the persisted choice (`owlmeans-lng`, when supported), else the
48
- browser's own language (`navigator.languages` through `preferredLanguageOf` — a first visit from a
49
- German browser opens in German), else `fallbackLng`. A language detector installed afterwards
50
- (`instance.use(detector)`, which `@owlmeans/web-panel`'s `render` still does) is never consulted —
51
- harmless, but not what detects. A detected language is not persisted; only an explicit choice is.
53
+ resolved there and not by a plugin — by `resolveInitialLanguage` (or `prepareI18n`, which runs it):
54
+ the persisted choice (`owlmeans-lng`, when supported), else the browser's own language
55
+ (`navigator.languages` through `preferredLanguageOf` — a first visit from a German browser opens in
56
+ German), else `fallbackLng`. A language detector installed afterwards (`instance.use(detector)`,
57
+ which `@owlmeans/web-panel`'s `render` still does) is never consulted — harmless, but not what
58
+ detects. A detected language is not persisted, not even by the start-up switch that follows a
59
+ deferred pack's load; only an explicit choice (`setLanguage`) is.
52
60
 
53
61
  ## Setup (app root)
54
62
 
@@ -66,6 +74,25 @@ function App() {
66
74
 
67
75
  `clientConfig.i18n` is optional; it defaults to `SUPPORTED_LNGS` and `'en'` fallback.
68
76
 
77
+ **An app that defers language packs** (`addI18nLoader`, the `i18n` skill) calls
78
+ `await prepareI18n(config)` BEFORE its first render — never after:
79
+
80
+ ```ts
81
+ import { prepareI18n } from '@owlmeans/client-i18n'
82
+
83
+ await prepareI18n(context.cfg) // fallback pack first, then the persisted / browser / fallback one
84
+ render(context)
85
+ ```
86
+
87
+ This composes with an async boot because `@owlmeans/web-client`'s `render` (and so `renderApp` and
88
+ `@owlmeans/web-panel`'s `render`) checks `document.readyState` instead of waiting for a
89
+ `DOMContentLoaded` that already fired. `prepareI18n` rejects only when the FALLBACK pack fails; when
90
+ the initial language's pack fails it logs, resolves to the fallback and leaves the persisted choice
91
+ alone, so the next visit retries it rather than stranding the user. Without `prepareI18n`, the
92
+ instance starts in the fallback whenever the wanted language is not loaded yet and switches once its
93
+ loaders finish (a brief fallback flash) — but nothing awaits a deferred FALLBACK pack, whose slots
94
+ are then drained empty for the session.
95
+
69
96
  ## Language persistence
70
97
 
71
98
  The active language is persisted in `localStorage` under `owlmeans-lng` and restored on init — but
@@ -76,6 +103,17 @@ the browser's language, then `fallbackLng`, instead of resolving nothing. Every
76
103
  `tests/instance.spec.ts` pins `preferredLanguageOf` (base matching, browser order, exact tag first,
77
104
  `null` when nothing matches).
78
105
 
106
+ `setLanguage(lng)` is async: it awaits `loadI18nLanguage(lng)`, then persists and calls i18next's
107
+ `changeLanguage` — before the instance exists it records the language the instance will start in
108
+ instead. The LAST call wins: an earlier call whose load finishes later does nothing. A failed load
109
+ rejects, and nothing is persisted or switched.
110
+
111
+ `useLanguage()` returns `i18n.language` from `useTranslation()`, not local state, so every caller
112
+ re-renders on i18next's `languageChanged` and shows the same language whoever switched it. Its
113
+ setter returns a promise that never rejects (a failure is logged): the value changes only once the
114
+ pack has loaded and i18next switched, so a controlled `<select value={lng}>` stays on the current
115
+ language meanwhile, and stays there when the load fails.
116
+
79
117
  ```tsx
80
118
  function LangSwitch() {
81
119
  const [lng, setLng] = useLanguage()
@@ -9,5 +9,11 @@ export declare const composePrefix: (parent?: string, child?: string) => string;
9
9
  export declare const useI18n: (resource: string, ns?: string, prefix?: string) => TFunction;
10
10
  export declare const useI18nLib: (libName: string, prefix?: string) => TFunction;
11
11
  export declare const useI18nApp: (appName?: string, prefix?: string) => TFunction;
12
- export declare const useLanguage: () => [string, (lng: string) => void];
12
+ /**
13
+ * `[currentLng, setLng]`. The current language is read from the i18next instance itself, whose
14
+ * `languageChanged` event re-renders every subscriber, so all callers agree whoever switched it.
15
+ * The setter loads the language's pack first; a failed switch is logged and leaves the language
16
+ * unchanged.
17
+ */
18
+ export declare const useLanguage: () => [string, (lng: string) => Promise<void>];
13
19
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAKxC,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CAI3C,CAAA;AAEF,kGAAkG;AAClG,eAAO,MAAM,aAAa,YAAa,MAAM,UAAU,MAAM,KAAG,MAK/D,CAAA;AAyCD;4FAC4F;AAC5F,eAAO,MAAM,OAAO,aAAc,MAAM,OAAO,MAAM,WAAW,MAAM,KAAG,SAExE,CAAA;AAED,eAAO,MAAM,UAAU,YAAa,MAAM,WAAW,MAAM,KAAG,SAE7D,CAAA;AAED,eAAO,MAAM,UAAU,aAAc,MAAM,WAAW,MAAM,KAAG,SAK9D,CAAA;AAED,eAAO,MAAM,WAAW,QAAO,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAU5D,CAAA"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAA;AAC/B,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAElD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAKxC,eAAO,MAAM,WAAW,EAAE,EAAE,CAAC,gBAAgB,CAI3C,CAAA;AAEF,kGAAkG;AAClG,eAAO,MAAM,aAAa,YAAa,MAAM,UAAU,MAAM,KAAG,MAK/D,CAAA;AAyCD;4FAC4F;AAC5F,eAAO,MAAM,OAAO,aAAc,MAAM,OAAO,MAAM,WAAW,MAAM,KAAG,SAExE,CAAA;AAED,eAAO,MAAM,UAAU,YAAa,MAAM,WAAW,MAAM,KAAG,SAE7D,CAAA;AAED,eAAO,MAAM,UAAU,aAAc,MAAM,WAAW,MAAM,KAAG,SAK9D,CAAA;AAED;;;;;GAKG;AACH,eAAO,MAAM,WAAW,QAAO,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAYrE,CAAA"}
package/build/context.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { memo, useCallback, useState } from 'react';
2
+ import { memo, useCallback } from 'react';
3
3
  import { I18nextProvider, useTranslation } from 'react-i18next';
4
4
  import { setLanguage, useI18nInstance } from './utils/instance.js';
5
5
  import { DEFAULT_LNG, DEFAULT_NAMESPACE, initI18nResource, LIB_NAMESPACE } from '@owlmeans/i18n';
@@ -57,13 +57,22 @@ export const useI18nApp = (appName, prefix) => {
57
57
  const resolvedName = appName ?? context.cfg.service;
58
58
  return useI18nResource(resolvedName, resolvedName, prefix);
59
59
  };
60
+ /**
61
+ * `[currentLng, setLng]`. The current language is read from the i18next instance itself, whose
62
+ * `languageChanged` event re-renders every subscriber, so all callers agree whoever switched it.
63
+ * The setter loads the language's pack first; a failed switch is logged and leaves the language
64
+ * unchanged.
65
+ */
60
66
  export const useLanguage = () => {
61
67
  const { i18n } = useTranslation();
62
- const [lng, setLng] = useState(i18n.language);
63
- const changeLng = useCallback((next) => {
64
- setLanguage(next);
65
- setLng(next);
68
+ const changeLng = useCallback(async (next) => {
69
+ try {
70
+ await setLanguage(next);
71
+ }
72
+ catch (error) {
73
+ console.error('[i18n] language switch failed', error);
74
+ }
66
75
  }, []);
67
- return [lng, changeLng];
76
+ return [i18n.language, changeLng];
68
77
  };
69
78
  //# sourceMappingURL=context.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"context.js","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"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"}
package/build/index.d.ts CHANGED
@@ -1,3 +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
4
  //# sourceMappingURL=index.d.ts.map
@@ -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"}
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,EAAE,WAAW,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA"}
package/build/index.js CHANGED
@@ -1,3 +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
4
  //# sourceMappingURL=index.js.map
@@ -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"}
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,EAAE,WAAW,EAAE,sBAAsB,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAA"}
@@ -2,7 +2,29 @@ import type { i18n } from 'i18next';
2
2
  import type { ClientConfig } from '@owlmeans/client-context';
3
3
  export declare const useI18nInstance: (config: ClientConfig) => i18n;
4
4
  export declare const getI18nInstance: (config: ClientConfig) => i18n;
5
- export declare const setLanguage: (lng: string) => void;
5
+ /**
6
+ * Load `lng`'s registered loaders, then persist it and switch the instance to it. Race-safe:
7
+ * when several calls overlap, the latest one wins whatever order their loads finish in.
8
+ * Rejects when the language's pack fails to load; nothing is persisted or switched then.
9
+ */
10
+ export declare const setLanguage: (lng: string) => Promise<void>;
11
+ /**
12
+ * Await the app's fallback language and its initial one (`resolveInitialLanguage`) before the app
13
+ * renders, so the first paint is already in the right language instead of flashing the
14
+ * fallback and then switching. Call it BEFORE `render(...)`. Resolves to the language the
15
+ * instance will start in: the fallback when the initial language's pack fails to load — the
16
+ * persisted choice is kept then, so the next visit retries it. Rejects only when the fallback
17
+ * language's own pack fails.
18
+ */
19
+ export declare const prepareI18n: (config: ClientConfig) => Promise<string>;
20
+ /**
21
+ * The language the app starts in: the one persisted under `owlmeans-lng` when it is still in
22
+ * `supportedLngs` (a person's explicit choice); else, on a first visit, the browser's own language
23
+ * (`navigator.languages` through `preferredLanguageOf` — a German browser opens in German); else
24
+ * `fallbackLng ?? defaultLng ?? DEFAULT_LNG`. The instance is initialized with this explicit `lng`,
25
+ * so a detector plugin installed afterwards (`instance.use(detector)`) is never consulted.
26
+ */
27
+ export declare const resolveInitialLanguage: (config: ClientConfig) => string;
6
28
  /**
7
29
  * The first of the browser's preferred languages the application supports — exact (`pt-BR`) or by
8
30
  * its base (`de-DE` → `de`) — or `null` when none is. Pure, so a host (or a test) can pass the list.
@@ -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;AAO5D,eAAO,MAAM,eAAe,WAAY,YAAY,KAAG,IAItD,CAAA;AAED,eAAO,MAAM,eAAe,WAAY,YAAY,KAAG,IAC3B,CAAA;AAE5B,eAAO,MAAM,WAAW,QAAS,MAAM,KAAG,IAMzC,CAAA;AAUD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,kBACf,SAAS,MAAM,EAAE,aAAa,SAAS,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAClF,MAAM,GAAG,IAUX,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;AAa5D,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;AAwBpF;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,WAAkB,YAAY,KAAG,OAAO,CAAC,MAAM,CAatE,CAAA;AAED;;;;;;GAMG;AACH,eAAO,MAAM,sBAAsB,WAAY,YAAY,KAAG,MAO7D,CAAA;AAaD;;;GAGG;AACH,eAAO,MAAM,mBAAmB,kBACf,SAAS,MAAM,EAAE,aAAa,SAAS,CAAC,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC,EAAE,KAClF,MAAM,GAAG,IAUX,CAAA"}
@@ -1,23 +1,83 @@
1
1
  import { createInstance } from 'i18next';
2
2
  import { initReactI18next } from 'react-i18next';
3
3
  import { useMemo } from 'react';
4
- import { DEFAULT_LNG, DEFAULT_NAMESPACE, SUPPORTED_LNGS } from '@owlmeans/i18n';
4
+ import { DEFAULT_LNG, DEFAULT_NAMESPACE, SUPPORTED_LNGS, isI18nLanguageLoaded, loadI18nLanguage } from '@owlmeans/i18n';
5
5
  const LNG_STORAGE_KEY = 'owlmeans-lng';
6
6
  let i18nInstance = null;
7
+ /** The language `prepareI18n` loaded; the instance starts in it when created afterwards. */
8
+ let preparedLng;
9
+ /** The language of the latest `setLanguage` call — an earlier call that finishes later yields to it. */
10
+ let requestedLng;
7
11
  export const useI18nInstance = (config) => {
8
12
  const instance = useMemo(() => createI18nInstance(config), []);
9
13
  return instance;
10
14
  };
11
15
  export const getI18nInstance = (config) => createI18nInstance(config);
12
- export const setLanguage = (lng) => {
13
- if (i18nInstance == null)
16
+ /**
17
+ * Load `lng`'s registered loaders, then persist it and switch the instance to it. Race-safe:
18
+ * when several calls overlap, the latest one wins whatever order their loads finish in.
19
+ * Rejects when the language's pack fails to load; nothing is persisted or switched then.
20
+ */
21
+ export const setLanguage = (lng) => switchLanguage(lng, true);
22
+ /**
23
+ * `setLanguage`'s body. `persist` is false only for the start-up switch to a language nobody chose
24
+ * (a browser-detected one): only an explicit choice is ever written to `owlmeans-lng`.
25
+ */
26
+ const switchLanguage = async (lng, persist) => {
27
+ requestedLng = lng;
28
+ await loadI18nLanguage(lng);
29
+ if (requestedLng !== lng) {
14
30
  return;
31
+ }
32
+ if (persist) {
33
+ try {
34
+ localStorage.setItem(LNG_STORAGE_KEY, lng);
35
+ }
36
+ catch (_) { /* noop in non-browser environments */ }
37
+ }
38
+ if (i18nInstance == null) {
39
+ preparedLng = lng;
40
+ return;
41
+ }
42
+ await i18nInstance.changeLanguage(lng);
43
+ };
44
+ /**
45
+ * Await the app's fallback language and its initial one (`resolveInitialLanguage`) before the app
46
+ * renders, so the first paint is already in the right language instead of flashing the
47
+ * fallback and then switching. Call it BEFORE `render(...)`. Resolves to the language the
48
+ * instance will start in: the fallback when the initial language's pack fails to load — the
49
+ * persisted choice is kept then, so the next visit retries it. Rejects only when the fallback
50
+ * language's own pack fails.
51
+ */
52
+ export const prepareI18n = async (config) => {
53
+ const fallback = resolveFallbackLanguage(config);
54
+ await loadI18nLanguage(fallback);
55
+ const lng = resolveInitialLanguage(config);
15
56
  try {
16
- localStorage.setItem(LNG_STORAGE_KEY, lng);
57
+ await loadI18nLanguage(lng);
58
+ preparedLng = lng;
17
59
  }
18
- catch (_) { /* noop in non-browser environments */ }
19
- i18nInstance.changeLanguage(lng);
60
+ catch (error) {
61
+ console.error(`[i18n] failed to load pack for "${lng}"`, error);
62
+ preparedLng = fallback;
63
+ }
64
+ return preparedLng;
20
65
  };
66
+ /**
67
+ * The language the app starts in: the one persisted under `owlmeans-lng` when it is still in
68
+ * `supportedLngs` (a person's explicit choice); else, on a first visit, the browser's own language
69
+ * (`navigator.languages` through `preferredLanguageOf` — a German browser opens in German); else
70
+ * `fallbackLng ?? defaultLng ?? DEFAULT_LNG`. The instance is initialized with this explicit `lng`,
71
+ * so a detector plugin installed afterwards (`instance.use(detector)`) is never consulted.
72
+ */
73
+ export const resolveInitialLanguage = (config) => {
74
+ const supportedLngs = config.i18n?.supportedLngs ?? [...SUPPORTED_LNGS];
75
+ const persistedLng = getPersistedLanguage();
76
+ return persistedLng != null && supportedLngs.includes(persistedLng)
77
+ ? persistedLng
78
+ : preferredLanguageOf(supportedLngs, browserLanguages()) ?? resolveFallbackLanguage(config);
79
+ };
80
+ const resolveFallbackLanguage = (config) => config.i18n?.fallbackLng ?? config.i18n?.defaultLng ?? DEFAULT_LNG;
21
81
  const getPersistedLanguage = () => {
22
82
  try {
23
83
  return localStorage.getItem(LNG_STORAGE_KEY);
@@ -59,15 +119,13 @@ const createI18nInstance = (config) => {
59
119
  if (i18nInstance != null) {
60
120
  return i18nInstance;
61
121
  }
62
- const fallbackLng = config.i18n?.fallbackLng ?? config.i18n?.defaultLng ?? DEFAULT_LNG;
122
+ const fallbackLng = resolveFallbackLanguage(config);
63
123
  const supportedLngs = config.i18n?.supportedLngs ?? [...SUPPORTED_LNGS];
64
- const persistedLng = getPersistedLanguage();
65
- // A person's explicit choice first; on a first visit the browser's own language — the instance is
66
- // initialized right here with an explicit `lng`, so a detector plugin installed afterwards
67
- // (`instance.use(detector)`) is never consulted and cannot do this.
68
- const lng = persistedLng != null && supportedLngs.includes(persistedLng)
69
- ? persistedLng
70
- : preferredLanguageOf(supportedLngs, browserLanguages()) ?? fallbackLng;
124
+ const wantedLng = preparedLng ?? resolveInitialLanguage(config);
125
+ // Safety net for a host that did not await `prepareI18n`: start in the fallback and switch once
126
+ // the wanted language's loaders finish — a brief fallback flash instead of a half-loaded language.
127
+ const loaded = isI18nLanguageLoaded(wantedLng);
128
+ const lng = loaded ? wantedLng : fallbackLng;
71
129
  const instance = createInstance({
72
130
  // No `compatibilityJSON` — i18next >= 26 accepts only the v4 JSON format
73
131
  // (Intl.PluralRules suffixes `_one`/`_other`, not the v3 `_plural`/`_0`/`_1`).
@@ -80,6 +138,13 @@ const createI18nInstance = (config) => {
80
138
  debug: config.debug?.all ?? config.debug?.i18n ?? false,
81
139
  });
82
140
  instance.use(initReactI18next).init();
83
- return i18nInstance = instance;
141
+ i18nInstance = instance;
142
+ if (!loaded) {
143
+ // Not persisted: `wantedLng` may be a browser-detected language nobody chose.
144
+ switchLanguage(wantedLng, false).catch((error) => {
145
+ console.error(`[i18n] failed to load pack for "${wantedLng}"`, error);
146
+ });
147
+ }
148
+ return instance;
84
149
  };
85
150
  //# sourceMappingURL=instance.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"instance.js","sourceRoot":"","sources":["../../src/utils/instance.ts"],"names":[],"mappings":"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"}
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.39",
3
+ "version": "0.1.18-rc.40",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -37,9 +37,9 @@
37
37
  "typescript": "^7.0.2"
38
38
  },
39
39
  "dependencies": {
40
- "@owlmeans/client": "^0.1.18-rc.40",
41
- "@owlmeans/client-context": "^0.1.18-rc.38",
42
- "@owlmeans/i18n": "^0.1.18-rc.36",
40
+ "@owlmeans/client": "^0.1.18-rc.41",
41
+ "@owlmeans/client-context": "^0.1.18-rc.39",
42
+ "@owlmeans/i18n": "^0.1.18-rc.37",
43
43
  "i18next": "^26.2.0",
44
44
  "react-i18next": "^17.0.11"
45
45
  },
package/src/context.tsx CHANGED
@@ -1,4 +1,4 @@
1
- import { memo, useCallback, useState } from 'react'
1
+ import { memo, useCallback } from 'react'
2
2
  import type { FC } from 'react'
3
3
  import type { I18nContextProps } from './types.js'
4
4
  import { I18nextProvider, useTranslation } from 'react-i18next'
@@ -77,14 +77,22 @@ export const useI18nApp = (appName?: string, prefix?: string): TFunction => {
77
77
  return useI18nResource(resolvedName, resolvedName, prefix)
78
78
  }
79
79
 
80
- export const useLanguage = (): [string, (lng: string) => void] => {
80
+ /**
81
+ * `[currentLng, setLng]`. The current language is read from the i18next instance itself, whose
82
+ * `languageChanged` event re-renders every subscriber, so all callers agree whoever switched it.
83
+ * The setter loads the language's pack first; a failed switch is logged and leaves the language
84
+ * unchanged.
85
+ */
86
+ export const useLanguage = (): [string, (lng: string) => Promise<void>] => {
81
87
  const { i18n } = useTranslation()
82
- const [lng, setLng] = useState(i18n.language)
83
88
 
84
- const changeLng = useCallback((next: string) => {
85
- setLanguage(next)
86
- setLng(next)
89
+ const changeLng = useCallback(async (next: string) => {
90
+ try {
91
+ await setLanguage(next)
92
+ } catch (error) {
93
+ console.error('[i18n] language switch failed', error)
94
+ }
87
95
  }, [])
88
96
 
89
- return [lng, changeLng]
97
+ return [i18n.language, changeLng]
90
98
  }
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
 
2
2
  export * from './types.js'
3
3
  export { I18nContext, composePrefix, useI18n, useI18nLib, useI18nApp, useLanguage } from './context.js'
4
+ export { prepareI18n, resolveInitialLanguage, setLanguage } from './utils/instance.js'
@@ -4,12 +4,18 @@ import { initReactI18next } from 'react-i18next'
4
4
  import type { i18n } from 'i18next'
5
5
  import { useMemo } from 'react'
6
6
  import type { ClientConfig } from '@owlmeans/client-context'
7
- import { DEFAULT_LNG, DEFAULT_NAMESPACE, SUPPORTED_LNGS } from '@owlmeans/i18n'
7
+ import { DEFAULT_LNG, DEFAULT_NAMESPACE, SUPPORTED_LNGS, isI18nLanguageLoaded, loadI18nLanguage } from '@owlmeans/i18n'
8
8
 
9
9
  const LNG_STORAGE_KEY = 'owlmeans-lng'
10
10
 
11
11
  let i18nInstance: i18n | null = null
12
12
 
13
+ /** The language `prepareI18n` loaded; the instance starts in it when created afterwards. */
14
+ let preparedLng: string | undefined
15
+
16
+ /** The language of the latest `setLanguage` call — an earlier call that finishes later yields to it. */
17
+ let requestedLng: string | undefined
18
+
13
19
  export const useI18nInstance = (config: ClientConfig): i18n => {
14
20
  const instance = useMemo(() => createI18nInstance(config), [])
15
21
 
@@ -19,14 +25,77 @@ export const useI18nInstance = (config: ClientConfig): i18n => {
19
25
  export const getI18nInstance = (config: ClientConfig): i18n =>
20
26
  createI18nInstance(config)
21
27
 
22
- export const setLanguage = (lng: string): void => {
23
- if (i18nInstance == null) return
28
+ /**
29
+ * Load `lng`'s registered loaders, then persist it and switch the instance to it. Race-safe:
30
+ * when several calls overlap, the latest one wins whatever order their loads finish in.
31
+ * Rejects when the language's pack fails to load; nothing is persisted or switched then.
32
+ */
33
+ export const setLanguage = (lng: string): Promise<void> => switchLanguage(lng, true)
34
+
35
+ /**
36
+ * `setLanguage`'s body. `persist` is false only for the start-up switch to a language nobody chose
37
+ * (a browser-detected one): only an explicit choice is ever written to `owlmeans-lng`.
38
+ */
39
+ const switchLanguage = async (lng: string, persist: boolean): Promise<void> => {
40
+ requestedLng = lng
41
+ await loadI18nLanguage(lng)
42
+ if (requestedLng !== lng) {
43
+ return
44
+ }
45
+ if (persist) {
46
+ try {
47
+ localStorage.setItem(LNG_STORAGE_KEY, lng)
48
+ } catch (_) { /* noop in non-browser environments */ }
49
+ }
50
+ if (i18nInstance == null) {
51
+ preparedLng = lng
52
+ return
53
+ }
54
+ await i18nInstance.changeLanguage(lng)
55
+ }
56
+
57
+ /**
58
+ * Await the app's fallback language and its initial one (`resolveInitialLanguage`) before the app
59
+ * renders, so the first paint is already in the right language instead of flashing the
60
+ * fallback and then switching. Call it BEFORE `render(...)`. Resolves to the language the
61
+ * instance will start in: the fallback when the initial language's pack fails to load — the
62
+ * persisted choice is kept then, so the next visit retries it. Rejects only when the fallback
63
+ * language's own pack fails.
64
+ */
65
+ export const prepareI18n = async (config: ClientConfig): Promise<string> => {
66
+ const fallback = resolveFallbackLanguage(config)
67
+ await loadI18nLanguage(fallback)
68
+ const lng = resolveInitialLanguage(config)
24
69
  try {
25
- localStorage.setItem(LNG_STORAGE_KEY, lng)
26
- } catch (_) { /* noop in non-browser environments */ }
27
- i18nInstance.changeLanguage(lng)
70
+ await loadI18nLanguage(lng)
71
+ preparedLng = lng
72
+ } catch (error) {
73
+ console.error(`[i18n] failed to load pack for "${lng}"`, error)
74
+ preparedLng = fallback
75
+ }
76
+
77
+ return preparedLng
28
78
  }
29
79
 
80
+ /**
81
+ * The language the app starts in: the one persisted under `owlmeans-lng` when it is still in
82
+ * `supportedLngs` (a person's explicit choice); else, on a first visit, the browser's own language
83
+ * (`navigator.languages` through `preferredLanguageOf` — a German browser opens in German); else
84
+ * `fallbackLng ?? defaultLng ?? DEFAULT_LNG`. The instance is initialized with this explicit `lng`,
85
+ * so a detector plugin installed afterwards (`instance.use(detector)`) is never consulted.
86
+ */
87
+ export const resolveInitialLanguage = (config: ClientConfig): string => {
88
+ const supportedLngs = config.i18n?.supportedLngs ?? [...SUPPORTED_LNGS]
89
+ const persistedLng = getPersistedLanguage()
90
+
91
+ return persistedLng != null && supportedLngs.includes(persistedLng)
92
+ ? persistedLng
93
+ : preferredLanguageOf(supportedLngs, browserLanguages()) ?? resolveFallbackLanguage(config)
94
+ }
95
+
96
+ const resolveFallbackLanguage = (config: ClientConfig): string =>
97
+ config.i18n?.fallbackLng ?? config.i18n?.defaultLng ?? DEFAULT_LNG
98
+
30
99
  const getPersistedLanguage = (): string | null => {
31
100
  try {
32
101
  return localStorage.getItem(LNG_STORAGE_KEY)
@@ -70,15 +139,13 @@ const createI18nInstance = (config: ClientConfig): i18n => {
70
139
  return i18nInstance
71
140
  }
72
141
 
73
- const fallbackLng = config.i18n?.fallbackLng ?? config.i18n?.defaultLng ?? DEFAULT_LNG
142
+ const fallbackLng = resolveFallbackLanguage(config)
74
143
  const supportedLngs = config.i18n?.supportedLngs ?? [...SUPPORTED_LNGS]
75
- const persistedLng = getPersistedLanguage()
76
- // A person's explicit choice first; on a first visit the browser's own language — the instance is
77
- // initialized right here with an explicit `lng`, so a detector plugin installed afterwards
78
- // (`instance.use(detector)`) is never consulted and cannot do this.
79
- const lng = persistedLng != null && supportedLngs.includes(persistedLng)
80
- ? persistedLng
81
- : preferredLanguageOf(supportedLngs, browserLanguages()) ?? fallbackLng
144
+ const wantedLng = preparedLng ?? resolveInitialLanguage(config)
145
+ // Safety net for a host that did not await `prepareI18n`: start in the fallback and switch once
146
+ // the wanted language's loaders finish — a brief fallback flash instead of a half-loaded language.
147
+ const loaded = isI18nLanguageLoaded(wantedLng)
148
+ const lng = loaded ? wantedLng : fallbackLng
82
149
 
83
150
  const instance = createInstance({
84
151
  // No `compatibilityJSON` — i18next >= 26 accepts only the v4 JSON format
@@ -94,5 +161,14 @@ const createI18nInstance = (config: ClientConfig): i18n => {
94
161
 
95
162
  instance.use(initReactI18next).init()
96
163
 
97
- return i18nInstance = instance
164
+ i18nInstance = instance
165
+
166
+ if (!loaded) {
167
+ // Not persisted: `wantedLng` may be a browser-detected language nobody chose.
168
+ switchLanguage(wantedLng, false).catch((error: unknown) => {
169
+ console.error(`[i18n] failed to load pack for "${wantedLng}"`, error)
170
+ })
171
+ }
172
+
173
+ return instance
98
174
  }