@owlmeans/i18n 0.1.18-rc.9 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +22 -12
  2. package/agent-meta/manifest.json +3 -3
  3. package/agent-meta/skills/i18n/SKILL.md +98 -33
  4. package/agent-meta/skills/localization/SKILL.md +15 -7
  5. package/build/consts.d.ts +0 -1
  6. package/build/consts.d.ts.map +1 -1
  7. package/build/consts.js.map +1 -1
  8. package/build/helper/types.d.ts +46 -0
  9. package/build/helper/types.d.ts.map +1 -0
  10. package/build/helper/types.js +2 -0
  11. package/build/helper/types.js.map +1 -0
  12. package/build/helper.d.ts +15 -1
  13. package/build/helper.d.ts.map +1 -1
  14. package/build/helper.js +117 -40
  15. package/build/helper.js.map +1 -1
  16. package/build/index.d.ts +1 -1
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js +0 -1
  19. package/build/index.js.map +1 -1
  20. package/build/types.d.ts +20 -1
  21. package/build/types.d.ts.map +1 -1
  22. package/build/utils/consts.d.ts +7 -0
  23. package/build/utils/consts.d.ts.map +1 -1
  24. package/build/utils/consts.js +10 -0
  25. package/build/utils/consts.js.map +1 -1
  26. package/build/utils/index.d.ts +0 -1
  27. package/build/utils/index.d.ts.map +1 -1
  28. package/build/utils/index.js +0 -1
  29. package/build/utils/index.js.map +1 -1
  30. package/build/utils/storage/types.d.ts +9 -0
  31. package/build/utils/storage/types.d.ts.map +1 -0
  32. package/build/utils/storage/types.js +2 -0
  33. package/build/utils/storage/types.js.map +1 -0
  34. package/build/utils/storage.d.ts +3 -6
  35. package/build/utils/storage.d.ts.map +1 -1
  36. package/build/utils/storage.js +26 -18
  37. package/build/utils/storage.js.map +1 -1
  38. package/package.json +2 -5
  39. package/src/consts.ts +0 -2
  40. package/src/helper/types.ts +46 -0
  41. package/src/helper.ts +151 -39
  42. package/src/index.ts +1 -1
  43. package/src/types.ts +23 -1
  44. package/src/utils/consts.ts +13 -0
  45. package/src/utils/index.ts +0 -1
  46. package/src/utils/storage/types.ts +9 -0
  47. package/src/utils/storage.ts +28 -16
  48. package/tests/context.ts +3 -2
  49. package/tests/loader.spec.ts +153 -0
  50. package/tests/registration.spec.ts +24 -25
  51. package/tests/resolve.spec.ts +64 -0
  52. package/build/advertise.d.ts +0 -2
  53. package/build/advertise.d.ts.map +0 -1
  54. package/build/advertise.js +0 -12
  55. package/build/advertise.js.map +0 -1
  56. package/src/advertise.ts +0 -12
package/README.md CHANGED
@@ -11,7 +11,7 @@ Multi-level translation resource registration for OwlMeans applications.
11
11
  ## Installation
12
12
 
13
13
  ```bash
14
- bun add @owlmeans/i18n@^0.1.18-rc.7
14
+ bun add @owlmeans/i18n@^0.1.18
15
15
  ```
16
16
 
17
17
  ## Usage
@@ -19,32 +19,42 @@ bun add @owlmeans/i18n@^0.1.18-rc.7
19
19
  Register translations for a package at startup:
20
20
 
21
21
  ```typescript
22
- import { addI18nApp } from '@owlmeans/i18n'
22
+ import { i18nHelper } from '@owlmeans/i18n'
23
23
 
24
24
  // Register app-level translations (highest priority)
25
- addI18nApp('en', 'manager-web', {
25
+ i18nHelper.addI18nApp('en', 'manager-web', {
26
26
  'project.create.title': 'Create Project',
27
27
  'project.create.submit': 'Create',
28
28
  })
29
29
 
30
30
  // Register library-level translations (lower priority — overridable by apps)
31
- import { addI18n } from '@owlmeans/i18n'
32
- addI18n('en', 'client-panel', { 'form.submit': 'Submit' })
31
+ i18nHelper.addI18nLib('en', 'client-panel', { 'form.submit': 'Submit' })
33
32
  ```
34
33
 
35
34
  ## API
36
35
 
37
- ### `addI18nApp(lng, resource, data, opts?)`
36
+ All registration and loading goes through the `i18nHelper` object (`createI18nHelper()` builds one
37
+ over the same process-wide storage).
38
38
 
39
- Register translations at app priority (highest). Typically called in `src/i18n.ts`.
39
+ ### `i18nHelper.addI18nApp(lng, resource, data, opts?)`
40
40
 
41
- ### `addI18n(level, lng, resource, data, opts?)`
41
+ Register translations at app tier (highest). Typically called in `src/i18n.ts`.
42
42
 
43
- Register translations at a specific `I18nLevel`. Lower levels are overridden by higher ones.
43
+ ### `i18nHelper.addI18nLib(lng, resource, data, opts?)`
44
44
 
45
- ### `I18nLevel`
45
+ Register translations at library tier (namespace `lib` by default). App-tier bundles override them.
46
46
 
47
- Priority levels: `Lib` < `Package` < `App`. App-level translations win over library translations for the same key.
47
+ ### `i18nHelper.initI18nResource(lng, resource, ns?)` / `i18nHelper.resolveI18nResource(lng, resource, ns?)`
48
+
49
+ Drain the bundles of one slot in merge order, or read their merged result without draining.
50
+
51
+ ### `i18nHelper.addI18nLoader(lng, loader)` / `i18nHelper.loadI18nLanguage(lng)` / `i18nHelper.isI18nLanguageLoaded(lng)`
52
+
53
+ Register lazy language loaders, await them, and check whether they completed.
54
+
55
+ ### `I18nTier`
56
+
57
+ Priority tiers: `Library` < `App`. App-tier translations win over library translations for the same key.
48
58
 
49
59
  ### `i18nStorage`
50
60
 
@@ -62,7 +72,7 @@ This package ships embedded agent skills under `agent-meta/`. After installing y
62
72
  your project's skill store (`.agents/skills/`):
63
73
 
64
74
  ```sh
65
- npx @owlmeans/agent-skills@^0.1.18-rc.13
75
+ npx @owlmeans/agent-skills@^0.1.18
66
76
  ```
67
77
 
68
78
  The embedded files are version-matched to this package release. Do not edit them
@@ -1,8 +1,8 @@
1
1
  {
2
- "schemaVersion": 2,
2
+ "schemaVersion": 3,
3
3
  "package": "@owlmeans/i18n",
4
- "version": "0.1.18-rc.9",
5
- "generatedAt": "2026-09-10T18:54:42.484Z",
4
+ "version": "0.1.18",
5
+ "generatedAt": "2026-10-28T00:31:34.701Z",
6
6
  "canonicalRepo": "https://github.com/owlmeans/common",
7
7
  "entries": [
8
8
  {
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: i18n
3
- description: How to use @owlmeans/i18n — the core localization registry (no runtime deps). Auto-invoked when adding translatable strings to a library package, importing from this package, or working with the tier/priority system.
3
+ description: How to use @owlmeans/i18n — the core localization registry (no runtime deps). Auto-invoked when adding translatable strings to a library package, importing from this package, working with the tier/priority system, or deferring a language pack with i18nHelper.addI18nLoader/loadI18nLanguage.
4
4
  user-invocable: false
5
5
  ---
6
6
  <!-- AUTO-GENERATED — do not edit. Regenerate via sync-agent-meta. -->
@@ -8,19 +8,23 @@ user-invocable: false
8
8
  # @owlmeans/i18n
9
9
 
10
10
  **Layer:** Core (no runtime deps)
11
- **Install:** `"@owlmeans/i18n": "^0.1.18-rc.9"` in `dependencies`
11
+ **Install:** `"@owlmeans/i18n": "^0.1.18"` in `dependencies`
12
12
 
13
13
  ## Purpose
14
14
 
15
- Global registration store that packages write into at import time. React clients drain it lazily via `@owlmeans/client-i18n`. The store is addressed by **(ns, resource, language)**.
15
+ Global registration store that packages write into at import time — or, for an application's deferred language packs, from an async loader. React clients drain it lazily via `@owlmeans/client-i18n`. The store is addressed by **(ns, resource, language)**.
16
16
 
17
17
  ## Key Exports
18
18
 
19
19
  | Export | Description |
20
20
  |--------|-------------|
21
- | `addI18nLib(lng, resource, data, opts?)` | Register library-owned strings (ns defaults to `'lib'`) |
22
- | `addI18nApp(lng, resource, data, opts?)` | Register app-owned strings (ns defaults to resource name) |
23
- | `initI18nResource(lng, resource, ns?)` | Drain a registered bundle for a language (called by client-i18n) |
21
+ | `i18nHelper.addI18nLib(lng, resource, data, opts?)` | Register library-owned strings (ns defaults to `'lib'`) |
22
+ | `i18nHelper.addI18nApp(lng, resource, data, opts?)` | Register app-owned strings (ns defaults to resource name) |
23
+ | `i18nHelper.initI18nResource(lng, resource, ns?)` | Drain a registered bundle for a language (called by client-i18n) |
24
+ | `i18nHelper.resolveI18nResource(lng, resource, ns = DEFAULT_NAMESPACE)` | The merged bundle of ANY language, read WITHOUT draining it — `null` when nothing is registered (see § Reading without draining) |
25
+ | `i18nHelper.addI18nLoader(lng, loader)` | Register an async `I18nLoader` (`() => Promise<unknown>`, typically a dynamic `import()` of a module that calls `i18nHelper.addI18nLib` / `addI18nApp`) for a language — see Deferred language packs |
26
+ | `i18nHelper.loadI18nLanguage(lng)` | Await every loader registered for `lng`; idempotent, retries a failed loader |
27
+ | `i18nHelper.isI18nLanguageLoaded(lng)` | Whether every loader of `lng` has completed; `true` for a language with none |
24
28
  | `SUPPORTED_LNGS` / `SupportedLng` | `['en','pl','ru','be','uk','es','de']` — the canonical language set, and its union type |
25
29
  | `DEFAULT_LNG` | `'en'` |
26
30
  | `LIB_NAMESPACE` | `'lib'` |
@@ -30,35 +34,37 @@ Global registration store that packages write into at import time. React clients
30
34
  | `I18nResourceOptions` | `{ ns?, priority? }` — the `opts` every `add*` takes |
31
35
  | `MAX_PRIORITY` | `Number.MAX_SAFE_INTEGER` — the value substituted for an unset `priority`, which is what makes an unset one sort last |
32
36
 
33
- `opts` also accepts a bare string, which is read as `ns`: `addI18nLib('en', 'wallet', walletEn, 'did')`.
37
+ `opts` also accepts a bare string, which is read as `ns`:
38
+ `i18nHelper.addI18nLib('en', 'wallet', walletEn, 'did')`.
34
39
 
35
40
  ## Subpath Exports
36
41
 
37
- - `./utils` — the store itself: `_OwlMeansI18nStorage` (`{ data }`, keyed ns → resource → language),
38
- `ensureStructure(lng, resource, ns?)` which creates and returns one slot, and `tierCost`, the
39
- `I18nTier` → number map the sort is written against.
42
+ - `./utils` — the store itself: `_OwlMeansI18nStorage` (`{ data }`, keyed ns → resource → language)
43
+ and `tierCost`, the `I18nTier` → number map the sort is written against; plus the separate loader
44
+ store `_OwlMeansI18nLoaders` (`{ data }`, keyed by language).
40
45
 
41
46
  Reach for `./utils` only to work around the drain-once rule below: assigning
42
47
  `_OwlMeansI18nStorage.data = {}` empties every slot including its `lngInitialized` marks, so a
43
- suite can register and drain the same resource repeatedly. This package's own tests do exactly
44
- that between cases. Application and library code registers through `addI18nLib` / `addI18nApp`.
48
+ suite can register and drain the same resource repeatedly; `_OwlMeansI18nLoaders.data = {}`
49
+ forgets every loader and request mark. This package's own tests reset both between cases.
50
+ Application and library code registers through `i18nHelper.addI18nLib` / `addI18nApp`.
45
51
 
46
52
  ## Tiers
47
53
 
48
54
  | Tier | Helper | Default ns | When to use |
49
55
  |------|--------|-----------|-------------|
50
- | Library | `addI18nLib` | `'lib'` | Any `@owlmeans/*` package |
51
- | App | `addI18nApp` | resource name | Project-specific app / shared project package |
56
+ | Library | `i18nHelper.addI18nLib` | `'lib'` | Any `@owlmeans/*` package |
57
+ | App | `i18nHelper.addI18nApp` | resource name | Project-specific app / shared project package |
52
58
 
53
59
  App-tier strings deep-merge **over** Library-tier strings at resolution time — but only for the
54
60
  same **(ns, resource, language)** slot, because that triple is the address the store is keyed on
55
- and the only thing `initI18nResource` drains. The default namespaces differ (`'lib'` for
61
+ and the only thing `i18nHelper.initI18nResource` drains. The default namespaces differ (`'lib'` for
56
62
  `addI18nLib`, the resource name for `addI18nApp`), so overriding a library bundle means saying so:
57
63
 
58
64
  ```typescript
59
- import { addI18nApp, LIB_NAMESPACE } from '@owlmeans/i18n'
65
+ import { i18nHelper, LIB_NAMESPACE } from '@owlmeans/i18n'
60
66
 
61
- addI18nApp('en', 'errors', myErrors, { ns: LIB_NAMESPACE })
67
+ i18nHelper.addI18nApp('en', 'errors', myErrors, { ns: LIB_NAMESPACE })
62
68
  ```
63
69
 
64
70
  Without `{ ns: LIB_NAMESPACE }` the app bundle lands in namespace `errors` while the library's sits
@@ -69,11 +75,35 @@ it — so the **last** applied wins. A registration that states no `priority` so
69
75
  therefore beats every one that states a number: `priority` lowers a bundle in the stack rather than
70
76
  raising it. Leave it unset unless one library must lose to another.
71
77
 
72
- **A bundle only reaches i18next if it was registered before the first draw for its language.**
73
- `initI18nResource` marks the (ns, resource, language) slot drained and answers `null` for every
74
- later call, so an `import '@owlmeans/<pkg>'` evaluated after a screen has rendered adds nothing a
75
- component can read. Register at module load — a side-effect import at the top of the entry file,
76
- which is what re-exporting `./i18n.js` from `src/index.ts` achieves.
78
+ **A bundle only reaches i18next if it was REGISTERED before its slot is first drained.**
79
+ `i18nHelper.initI18nResource` reads a (ns, resource, language) slot exactly once — it marks the slot
80
+ drained and answers `null` for every later call — so a bundle registered through `addI18nLib` /
81
+ `addI18nApp` after a screen has drawn that language adds nothing a component can read. Registering
82
+ synchronously at import time is the simplest way to guarantee it — a side-effect import at the top
83
+ of the entry file, which is what re-exporting `./i18n.js` from `src/index.ts` achieves. The loader
84
+ API (`i18nHelper.addI18nLoader` / `loadI18nLanguage`) lets an application defer registration to an
85
+ async load instead, AS LONG AS it awaits `i18nHelper.loadI18nLanguage` — or
86
+ `@owlmeans/client-i18n`'s `i18nInstanceHelper.prepareI18n` — before the first render that would
87
+ drain that language.
88
+
89
+ ## Reading without draining
90
+
91
+ `i18nHelper.resolveI18nResource(lng, resource, ns)` deep-merges every bundle registered for the slot
92
+ in the exact order `initI18nResource` hands them out — library tier, then app tier, `priority`
93
+ ascending, an unset priority last — so an app override wins as it does in i18next. Plain objects
94
+ merge key by key; any other value replaces (arrays are copied). It never marks the slot drained and
95
+ never creates an empty slot, so a later `initI18nResource` still returns the same bundles, and a
96
+ drained slot still resolves. The result never aliases a registered bundle.
97
+
98
+ Use it where there is no i18next instance or the language is not the active one: a server
99
+ rendering an e-mail or a paygate text, a legal text shown in the billing country's language while
100
+ the interface speaks another (`@owlmeans/payment`'s `consumerCopyHelper.consumerRightsCopy` is built
101
+ on it). Pass the namespace the bundle lives in — `LIB_NAMESPACE` for an `i18nHelper.addI18nLib`
102
+ bundle; the default is i18next's own `'translation'`.
103
+
104
+ It reads only what is REGISTERED: a language an application defers to a loader (Deferred language
105
+ packs) resolves `null` — or without its app-tier overrides — until
106
+ `await i18nHelper.loadI18nLanguage(lng)` has run for it.
77
107
 
78
108
  ## Per-package pattern
79
109
 
@@ -81,7 +111,7 @@ Every package that ships translatable strings exports a side-effect `i18n.ts`:
81
111
 
82
112
  ```typescript
83
113
  // src/i18n.ts
84
- import { addI18nLib } from '@owlmeans/i18n'
114
+ import { i18nHelper } from '@owlmeans/i18n'
85
115
  import en from './i18n/en.json' with { type: 'json' }
86
116
  import pl from './i18n/pl.json' with { type: 'json' }
87
117
  import ru from './i18n/ru.json' with { type: 'json' }
@@ -89,14 +119,16 @@ import be from './i18n/be.json' with { type: 'json' }
89
119
  import uk from './i18n/uk.json' with { type: 'json' }
90
120
  import es from './i18n/es.json' with { type: 'json' }
91
121
  import de from './i18n/de.json' with { type: 'json' }
92
-
93
- addI18nLib('en', 'my-package', en)
94
- addI18nLib('pl', 'my-package', pl)
95
- addI18nLib('ru', 'my-package', ru)
96
- addI18nLib('be', 'my-package', be)
97
- addI18nLib('uk', 'my-package', uk)
98
- addI18nLib('es', 'my-package', es)
99
- addI18nLib('de', 'my-package', de)
122
+ import fr from './i18n/fr.json' with { type: 'json' }
123
+
124
+ i18nHelper.addI18nLib('en', 'my-package', en)
125
+ i18nHelper.addI18nLib('pl', 'my-package', pl)
126
+ i18nHelper.addI18nLib('ru', 'my-package', ru)
127
+ i18nHelper.addI18nLib('be', 'my-package', be)
128
+ i18nHelper.addI18nLib('uk', 'my-package', uk)
129
+ i18nHelper.addI18nLib('es', 'my-package', es)
130
+ i18nHelper.addI18nLib('de', 'my-package', de)
131
+ i18nHelper.addI18nLib('fr', 'my-package', fr)
100
132
  ```
101
133
 
102
134
  Then re-export from `src/index.ts`:
@@ -123,12 +155,45 @@ Consumers use `useI18nLib('my-package', 'mySection')` → `t('title')` → resol
123
155
 
124
156
  Use the optional `opts.ns` when keys must live in a namespace other than `'lib'`:
125
157
  ```typescript
126
- addI18nLib('en', 'wallet', walletEn, { ns: 'did' })
158
+ i18nHelper.addI18nLib('en', 'wallet', walletEn, { ns: 'did' })
127
159
  ```
128
160
 
129
161
  ## Languages
130
162
 
131
- All packages **must** ship all 7 languages from `SUPPORTED_LNGS`. Adding a new key → add it to all 7 files in the same commit.
163
+ All packages **must** ship the 7 languages from `SUPPORTED_LNGS`. Adding a new key → add it to all
164
+ 7 files in the same commit. An application may opt into an additional language without changing the
165
+ global constant; a reusable package used by that application then ships and synchronously registers
166
+ the same extra bundle, while other applications retain their existing selectable languages.
167
+
168
+ ### Deferred language packs (applications only)
169
+
170
+ An application may keep a language's bundles out of its initial chunk and register them from a
171
+ loader. The loader store is separate from the resource store: a loader only REGISTERS (it calls
172
+ `i18nHelper.addI18nLib` / `addI18nApp` once its module evaluates); draining stays
173
+ `initI18nResource`'s, once per slot, unchanged.
174
+
175
+ ```typescript
176
+ // app entry, module scope — each module calls i18nHelper.addI18nApp('<lng>', …) when it evaluates
177
+ i18nHelper.addI18nLoader('de', () => import('./i18n/de.js'))
178
+ i18nHelper.addI18nLoader('pl', () => import('./i18n/pl.js'))
179
+ ```
180
+
181
+ - A loader does not run until its language is requested by `i18nHelper.loadI18nLanguage(lng)`; one
182
+ registered AFTER the language was requested starts at once, so a module evaluated late is never
183
+ skipped.
184
+ - `i18nHelper.loadI18nLanguage(lng)` resolves once every loader of `lng` has completed — including
185
+ loaders added while the call is in flight; one added after a call resolved is awaited by the next
186
+ call. Concurrent and repeated calls share in-flight runs and never re-run a completed loader.
187
+ - A failure rejects the call once the pass has settled; the failed loader alone is retried by the
188
+ next call, never cached as failed.
189
+ - The app awaits the fallback and the initial language before its first render —
190
+ `@owlmeans/client-i18n`'s `i18nInstanceHelper.prepareI18n` does both — and its
191
+ `i18nInstanceHelper.setLanguage` / `useLanguage` load a language before switching to it.
192
+ `@owlmeans/web-panel` re-exports `i18nHelper` (the loader API included) but not
193
+ `i18nInstanceHelper` — import that from `@owlmeans/client-i18n`.
194
+
195
+ A library package never defers: it registers every language synchronously at import (Per-package
196
+ pattern), because it cannot know the application it lands in awaits a boot step.
132
197
 
133
198
  ## Depends On
134
199
 
@@ -21,8 +21,8 @@ Keys are addressed as: `(namespace, resource.prefix.key)`.
21
21
 
22
22
  | Who registers | Helper | namespace | key path |
23
23
  |---|---|---|---|
24
- | `@owlmeans/*` library package | `addI18nLib` | `'lib'` | `resource.prefix.key` |
25
- | App or shared project package | `addI18nApp` | resource name | `resource.prefix.key` |
24
+ | `@owlmeans/*` library package | `i18nHelper.addI18nLib` | `'lib'` | `resource.prefix.key` |
25
+ | App or shared project package | `i18nHelper.addI18nApp` | resource name | `resource.prefix.key` |
26
26
 
27
27
  The **resource name** is a stable identifier for the registering package, e.g. `'errors'`, `'payment'`, `'viable-manager-web'`.
28
28
 
@@ -32,7 +32,7 @@ A key like `t('title')` resolves to `namespace : resource.prefix.key`. The prefi
32
32
 
33
33
  Example chain:
34
34
  ```
35
- addI18nApp('en', 'viable-manager-web', data)
35
+ i18nHelper.addI18nApp('en', 'viable-manager-web', data)
36
36
  // data = { "home-screen": { "title": "Welcome" } }
37
37
 
38
38
  const t = useI18nApp(undefined, 'home-screen')
@@ -84,14 +84,14 @@ fallbacks; a surrounding `PanelContext` is what redirects it.
84
84
  ## App-level override of library strings
85
85
 
86
86
  App-tier registrations deep-merge over Library-tier for the same `(ns, resource)` — and the
87
- namespace has to be said out loud, because `addI18nApp` defaults it to the **resource** name while
88
- the library's bundle sits in `lib`:
87
+ namespace has to be said out loud, because `i18nHelper.addI18nApp` defaults it to the **resource**
88
+ name while the library's bundle sits in `lib`:
89
89
  ```typescript
90
90
  import '@owlmeans/error' // Library tier: lib:errors.*
91
- import { addI18nApp, LIB_NAMESPACE } from '@owlmeans/i18n'
91
+ import { i18nHelper, LIB_NAMESPACE } from '@owlmeans/i18n'
92
92
  import myErrors from './i18n/en.json'
93
93
 
94
- addI18nApp('en', 'errors', myErrors, { ns: LIB_NAMESPACE }) // App tier wins for matching keys
94
+ i18nHelper.addI18nApp('en', 'errors', myErrors, { ns: LIB_NAMESPACE }) // App tier wins for matching keys
95
95
  ```
96
96
 
97
97
  Drop `{ ns: LIB_NAMESPACE }` and the app bundle lands in namespace `errors` while `useI18nLib('errors')`
@@ -106,9 +106,17 @@ When adding translatable strings to any package:
106
106
  3. Re-export from `src/index.ts`: `export * from './i18n.js'`
107
107
  4. In components, use `useI18nLib` (library package) or `useI18nApp` (app package) — never hardcode UI strings
108
108
 
109
+ A library package (`@owlmeans/*`) always registers every language synchronously at import: it has
110
+ no boot step that could await a loader, and a bundle registered after its slot was first drained
111
+ never renders. An APPLICATION may instead defer a language's pack with `i18nHelper.addI18nLoader`,
112
+ provided it `await`s `i18nInstanceHelper.prepareI18n(config)` before its first render (`i18n`,
113
+ `client-i18n` skills).
114
+
109
115
  ## Language switcher
110
116
 
111
117
  Use `useLanguage()` from `@owlmeans/client-i18n`. The selection is persisted to `localStorage`.
118
+ The setter is async — it loads a deferred pack before switching — and the returned language is
119
+ i18next's own, so it changes only once the switch has happened.
112
120
 
113
121
  ```tsx
114
122
  import { useLanguage } from '@owlmeans/client-i18n'
package/build/consts.d.ts CHANGED
@@ -2,7 +2,6 @@ export declare const DEFAULT_NAMESPACE = "translation";
2
2
  export declare const LIB_NAMESPACE = "lib";
3
3
  export declare const DEFAULT_LNG = "en";
4
4
  export declare const SUPPORTED_LNGS: readonly ['en', 'pl', 'ru', 'be', 'uk', 'es', 'de'];
5
- export type SupportedLng = (typeof SUPPORTED_LNGS)[number];
6
5
  export declare const MAX_PRIORITY: number;
7
6
  export declare enum I18nTier {
8
7
  Library = "library",
@@ -1 +1 @@
1
- {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,iBAAiB,gBAAgB,CAAA;AAE9C,eAAO,MAAM,aAAa,QAAQ,CAAA;AAElC,eAAO,MAAM,WAAW,OAAO,CAAA;AAE/B,eAAO,MAAM,cAAc,YAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAEjF,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,cAAc,CAAC,CAAC,MAAM,CAAC,CAAA;AAE1D,eAAO,MAAM,YAAY,QAA0B,CAAA;AAEnD,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,GAAG,QAAQ;CACZ;AAED,+BAA+B;AAC/B,eAAO,MAAM,SAAS,iBAAW,CAAA"}
1
+ {"version":3,"file":"consts.d.ts","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,iBAAiB,gBAAgB,CAAA;AAE9C,eAAO,MAAM,aAAa,QAAQ,CAAA;AAElC,eAAO,MAAM,WAAW,OAAO,CAAA;AAE/B,eAAO,MAAM,cAAc,YAAI,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAEjF,eAAO,MAAM,YAAY,QAA0B,CAAA;AAEnD,oBAAY,QAAQ;IAClB,OAAO,YAAY;IACnB,GAAG,QAAQ;CACZ;AAED,+BAA+B;AAC/B,eAAO,MAAM,SAAS,iBAAW,CAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAA;AAE9C,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAA;AAElC,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAA;AAE/B,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAIjF,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,gBAAgB,CAAA;AAEnD,MAAM,CAAN,IAAY,QAGX;AAHD,WAAY,QAAQ;IAClB,+BAAmB,CAAA;IACnB,uBAAW,CAAA;AACb,CAAC,EAHW,QAAQ,KAAR,QAAQ,QAGnB;AAED,+BAA+B;AAC/B,MAAM,CAAC,MAAM,SAAS,GAAG,QAAQ,CAAA"}
1
+ {"version":3,"file":"consts.js","sourceRoot":"","sources":["../src/consts.ts"],"names":[],"mappings":"AACA,MAAM,CAAC,MAAM,iBAAiB,GAAG,aAAa,CAAA;AAE9C,MAAM,CAAC,MAAM,aAAa,GAAG,KAAK,CAAA;AAElC,MAAM,CAAC,MAAM,WAAW,GAAG,IAAI,CAAA;AAE/B,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAU,CAAA;AAEjF,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC,gBAAgB,CAAA;AAEnD,MAAM,CAAN,IAAY,QAGX;AAHD,WAAY,QAAQ;IAClB,+BAAmB,CAAA;IACnB,uBAAW,CAAA;AACb,CAAC,EAHW,QAAQ,KAAR,QAAQ,QAGnB;AAED,+BAA+B;AAC/B,MAAM,CAAC,MAAM,SAAS,GAAG,QAAQ,CAAA"}
@@ -0,0 +1,46 @@
1
+ import type { I18nLeveledResourceSignature, I18nLoader, I18nResource } from '../types.js';
2
+ /** The process-wide i18n registry: resource bundles per (ns, resource, language) and language loaders. */
3
+ export interface I18nHelper {
4
+ /** Registers a library-tier bundle; its namespace defaults to `lib`. */
5
+ addI18nLib: I18nLeveledResourceSignature;
6
+ /** Registers an app-tier bundle; its namespace defaults to the resource name. */
7
+ addI18nApp: I18nLeveledResourceSignature;
8
+ /**
9
+ * Drains the bundles of one (ns, resource, language) slot in merge order — library tier before app
10
+ * tier, then `priority` ascending. `null` once the slot was drained for the language.
11
+ */
12
+ initI18nResource: (lng: string, resource: string, ns?: string) => null | I18nResource[];
13
+ /**
14
+ * The merged bundle of one (ns, resource, language) slot, read WITHOUT draining it.
15
+ *
16
+ * Every registered bundle is deep-merged in the order `initI18nResource` hands them out — library
17
+ * tier before app tier, then `priority` ascending, a bundle with no priority last — so an app
18
+ * override wins exactly as it does in i18next. The slot is never marked drained and no empty slot
19
+ * is created, so a later `initI18nResource` still returns the same bundles. This is the read for
20
+ * code with no i18next instance (a server rendering an e-mail) and for a language other than the
21
+ * active one (a legal text shown in the billing country's language). `null` when nothing is
22
+ * registered for the slot.
23
+ */
24
+ resolveI18nResource: (lng: string, resource: string, ns?: string) => Record<string, unknown> | null;
25
+ /**
26
+ * Register a loader for a language's resources — typically a dynamic `import()` of a module that
27
+ * calls `addI18nLib` / `addI18nApp`. If the language was already requested (`loadI18nLanguage` was
28
+ * called for it), the loader starts at once: one registered late, by a module that only evaluates
29
+ * after the app started, is never silently skipped. Its failure then surfaces through the next
30
+ * `loadI18nLanguage` call, which retries it.
31
+ */
32
+ addI18nLoader: (lng: string, loader: I18nLoader) => void;
33
+ /**
34
+ * Await every loader registered for `lng` — including those registered while this call is in
35
+ * flight — then resolve. Concurrent and repeated calls share in-flight runs and never re-run a
36
+ * completed loader; a loader registered after an earlier call resolved is awaited by the next one.
37
+ * A language with no loaders resolves at once.
38
+ *
39
+ * Rejects with the first failure once every loader of the pass has settled; the failed loader
40
+ * alone is re-run by the next call.
41
+ */
42
+ loadI18nLanguage: (lng: string) => Promise<void>;
43
+ /** Whether every loader registered for `lng` has completed; true for a language with none. */
44
+ isI18nLanguageLoaded: (lng: string) => boolean;
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/helper/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,4BAA4B,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAEzF,0GAA0G;AAC1G,MAAM,WAAW,UAAU;IACzB,wEAAwE;IACxE,UAAU,EAAE,4BAA4B,CAAA;IACxC,iFAAiF;IACjF,UAAU,EAAE,4BAA4B,CAAA;IACxC;;;OAGG;IACH,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,YAAY,EAAE,CAAA;IACvF;;;;;;;;;;OAUG;IACH,mBAAmB,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACnG;;;;;;OAMG;IACH,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,UAAU,KAAK,IAAI,CAAA;IACxD;;;;;;;;OAQG;IACH,gBAAgB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IAChD,8FAA8F;IAC9F,oBAAoB,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAA;CAC/C"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/helper/types.ts"],"names":[],"mappings":""}
package/build/helper.d.ts CHANGED
@@ -1,5 +1,19 @@
1
- import type { I18nLeveledResourceSignature, I18nResource } from './types.js';
1
+ import type { I18nLeveledResourceSignature, I18nLoader, I18nResource } from './types.js';
2
+ import type { I18nHelper } from './helper/types.js';
3
+ export declare const createI18nHelper: () => I18nHelper;
4
+ export declare const i18nHelper: I18nHelper;
5
+ /** @deprecated compat:factory-refactor — use `i18nHelper.addI18nLib(…)` */
2
6
  export declare const addI18nLib: I18nLeveledResourceSignature;
7
+ /** @deprecated compat:factory-refactor — use `i18nHelper.addI18nApp(…)` */
3
8
  export declare const addI18nApp: I18nLeveledResourceSignature;
9
+ /** @deprecated compat:factory-refactor — use `i18nHelper.initI18nResource(…)` */
4
10
  export declare const initI18nResource: (lng: string, resource: string, ns?: string) => null | I18nResource[];
11
+ /** @deprecated compat:factory-refactor — use `i18nHelper.resolveI18nResource(…)` */
12
+ export declare const resolveI18nResource: (lng: string, resource: string, ns?: string) => Record<string, unknown> | null;
13
+ /** @deprecated compat:factory-refactor — use `i18nHelper.addI18nLoader(…)` */
14
+ export declare const addI18nLoader: (lng: string, loader: I18nLoader) => void;
15
+ /** @deprecated compat:factory-refactor — use `i18nHelper.loadI18nLanguage(…)` */
16
+ export declare const loadI18nLanguage: (lng: string) => Promise<void>;
17
+ /** @deprecated compat:factory-refactor — use `i18nHelper.isI18nLanguageLoaded(…)` */
18
+ export declare const isI18nLanguageLoaded: (lng: string) => boolean;
5
19
  //# sourceMappingURL=helper.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,4BAA4B,EAAE,YAAY,EAA8C,MAAM,YAAY,CAAA;AA6BxH,eAAO,MAAM,UAAU,EAAE,4BAGxB,CAAA;AAED,eAAO,MAAM,UAAU,EAAE,4BAGxB,CAAA;AAED,eAAO,MAAM,gBAAgB,QAAS,MAAM,YAAY,MAAM,OAAO,MAAM,KAAG,IAAI,GAAG,YAAY,EAoBhG,CAAA"}
1
+ {"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EACV,4BAA4B,EAAE,UAAU,EAAmB,YAAY,EACxE,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAA;AAInD,eAAO,MAAM,gBAAgB,QAAO,UA6InC,CAAA;AAED,eAAO,MAAM,UAAU,YAAqB,CAAA;AAE5C,2EAA2E;AAC3E,eAAO,MAAM,UAAU,EAAE,4BAA8G,CAAA;AAEvI,2EAA2E;AAC3E,eAAO,MAAM,UAAU,EAAE,4BAA8G,CAAA;AAEvI,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,QAAS,MAAM,YAAY,MAAM,OAAO,MAAM,KAAG,IAAI,GAAG,YAAY,EAAoD,CAAA;AAErJ,oFAAoF;AACpF,eAAO,MAAM,mBAAmB,QAAS,MAAM,YAAY,MAAM,OAAO,MAAM,KAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAyD,CAAA;AAEpK,8EAA8E;AAC9E,eAAO,MAAM,aAAa,QAAS,MAAM,UAAU,UAAU,KAAG,IAA6C,CAAA;AAE7G,iFAAiF;AACjF,eAAO,MAAM,gBAAgB,QAAS,MAAM,KAAG,OAAO,CAAC,IAAI,CAAqC,CAAA;AAEhG,qFAAqF;AACrF,eAAO,MAAM,oBAAoB,QAAS,MAAM,KAAG,OAA+C,CAAA"}
package/build/helper.js CHANGED
@@ -1,42 +1,39 @@
1
1
  import { DEFAULT_NAMESPACE, I18nTier, LIB_NAMESPACE, MAX_PRIORITY } from './consts.js';
2
- import { tierCost } from './utils/consts.js';
3
- import { ensureStructure } from './utils/storage.js';
4
- const prepareOptions = (opts, ns) => {
5
- if (typeof opts === 'string') {
6
- opts = { ns: opts };
7
- }
8
- else if (typeof opts === 'undefined') {
9
- opts = {};
10
- }
11
- if (opts.ns == null && ns != null) {
12
- opts.ns = ns;
13
- }
14
- return opts;
15
- };
16
- const _addI18n = (tier, lng, resource, data, opts) => {
17
- opts = prepareOptions(opts);
18
- const storage = ensureStructure(lng, resource, opts.ns);
19
- const ns = opts.ns ?? DEFAULT_NAMESPACE;
20
- const entry = {
21
- ns, lng, tier, resource, data, priority: opts.priority
22
- };
23
- storage.resources.push(entry);
24
- };
25
- export const addI18nLib = (lng, resource, data, opts) => {
26
- opts = prepareOptions(opts, LIB_NAMESPACE);
27
- _addI18n(I18nTier.Library, lng, resource, data, opts);
28
- };
29
- export const addI18nApp = (lng, resource, data, opts) => {
30
- opts = prepareOptions(opts, resource);
31
- _addI18n(I18nTier.App, lng, resource, data, opts);
32
- };
33
- export const initI18nResource = (lng, resource, ns) => {
34
- ns = ns ?? DEFAULT_NAMESPACE;
35
- const translation = ensureStructure(lng, resource, ns);
36
- if (translation.lngInitialized.includes(lng)) {
37
- return null;
38
- }
39
- const result = [...translation.resources].sort((a, b) => {
2
+ import { tierCost, _OwlMeansI18nLoaders, _OwlMeansI18nStorage } from './utils/consts.js';
3
+ import { i18nStorageUtils } from './utils/storage.js';
4
+ export const createI18nHelper = () => {
5
+ const { ensureLoaders, ensureStructure } = i18nStorageUtils;
6
+ const prepareOptions = (opts, ns) => {
7
+ if (typeof opts === 'string') {
8
+ opts = { ns: opts };
9
+ }
10
+ else if (typeof opts === 'undefined') {
11
+ opts = {};
12
+ }
13
+ if (opts.ns == null && ns != null) {
14
+ opts.ns = ns;
15
+ }
16
+ return opts;
17
+ };
18
+ const _addI18n = (tier, lng, resource, data, opts) => {
19
+ opts = prepareOptions(opts);
20
+ const storage = ensureStructure(lng, resource, opts.ns);
21
+ const ns = opts.ns ?? DEFAULT_NAMESPACE;
22
+ const entry = {
23
+ ns, lng, tier, resource, data, priority: opts.priority
24
+ };
25
+ storage.resources.push(entry);
26
+ };
27
+ const addI18nLib = (lng, resource, data, opts) => {
28
+ opts = prepareOptions(opts, LIB_NAMESPACE);
29
+ _addI18n(I18nTier.Library, lng, resource, data, opts);
30
+ };
31
+ const addI18nApp = (lng, resource, data, opts) => {
32
+ opts = prepareOptions(opts, resource);
33
+ _addI18n(I18nTier.App, lng, resource, data, opts);
34
+ };
35
+ /** Library tier before app tier, then `priority` ascending — the order bundles are merged in. */
36
+ const ordered = (resources) => [...resources].sort((a, b) => {
40
37
  const aTier = tierCost[a.tier];
41
38
  const bTier = tierCost[b.tier];
42
39
  if (aTier !== bTier) {
@@ -44,7 +41,87 @@ export const initI18nResource = (lng, resource, ns) => {
44
41
  }
45
42
  return (a.priority ?? MAX_PRIORITY) - (b.priority ?? MAX_PRIORITY);
46
43
  });
47
- translation.lngInitialized.push(lng);
48
- return result;
44
+ const initI18nResource = (lng, resource, ns) => {
45
+ ns = ns ?? DEFAULT_NAMESPACE;
46
+ const translation = ensureStructure(lng, resource, ns);
47
+ if (translation.lngInitialized.includes(lng)) {
48
+ return null;
49
+ }
50
+ const result = ordered(translation.resources);
51
+ translation.lngInitialized.push(lng);
52
+ return result;
53
+ };
54
+ const isPlainObject = (value) => value != null && typeof value === 'object' && !Array.isArray(value);
55
+ /** Merge `source` over `target` in place; nested objects are copied, never shared with a bundle. */
56
+ const mergeInto = (target, source) => {
57
+ for (const [key, value] of Object.entries(source)) {
58
+ const current = target[key];
59
+ target[key] = isPlainObject(value)
60
+ ? mergeInto(isPlainObject(current) ? current : {}, value)
61
+ : Array.isArray(value) ? structuredClone(value) : value;
62
+ }
63
+ return target;
64
+ };
65
+ const resolveI18nResource = (lng, resource, ns = DEFAULT_NAMESPACE) => {
66
+ const translation = _OwlMeansI18nStorage.data[ns]?.[resource]?.[lng];
67
+ if (translation == null || translation.resources.length === 0) {
68
+ return null;
69
+ }
70
+ return ordered(translation.resources).reduce((merged, bundle) => mergeInto(merged, bundle.data), {});
71
+ };
72
+ /**
73
+ * Start `entry`'s loader, or join the run already in flight. A completed entry never runs again;
74
+ * a failed run clears `running`, so the next caller starts it afresh.
75
+ */
76
+ const runLoader = (entry) => {
77
+ if (entry.done) {
78
+ return Promise.resolve();
79
+ }
80
+ return entry.running ??= Promise.resolve().then(() => entry.loader()).then(() => { entry.done = true; }, (error) => {
81
+ entry.running = undefined;
82
+ throw error;
83
+ });
84
+ };
85
+ const addI18nLoader = (lng, loader) => {
86
+ const bucket = ensureLoaders(lng);
87
+ const entry = { loader, done: false };
88
+ bucket.entries.push(entry);
89
+ if (bucket.requested) {
90
+ runLoader(entry).catch(() => { });
91
+ }
92
+ };
93
+ const loadI18nLanguage = async (lng) => {
94
+ const bucket = ensureLoaders(lng);
95
+ bucket.requested = true;
96
+ // Each pass settles what was pending when it began; a loader added meanwhile is left for the
97
+ // next pass, so the call resolves only once nothing is pending.
98
+ for (let pending = bucket.entries.filter(entry => !entry.done); pending.length > 0; pending = bucket.entries.filter(entry => !entry.done)) {
99
+ const results = await Promise.allSettled(pending.map(runLoader));
100
+ const failure = results.find(result => result.status === 'rejected');
101
+ if (failure != null) {
102
+ throw failure.reason;
103
+ }
104
+ }
105
+ };
106
+ const isI18nLanguageLoaded = (lng) => _OwlMeansI18nLoaders.data[lng]?.entries.every(entry => entry.done) ?? true;
107
+ return {
108
+ addI18nLib, addI18nApp, initI18nResource, resolveI18nResource,
109
+ addI18nLoader, loadI18nLanguage, isI18nLanguageLoaded
110
+ };
49
111
  };
112
+ export const i18nHelper = createI18nHelper();
113
+ /** @deprecated compat:factory-refactor — use `i18nHelper.addI18nLib(…)` */
114
+ export const addI18nLib = (lng, resource, data, opts) => i18nHelper.addI18nLib(lng, resource, data, opts);
115
+ /** @deprecated compat:factory-refactor — use `i18nHelper.addI18nApp(…)` */
116
+ export const addI18nApp = (lng, resource, data, opts) => i18nHelper.addI18nApp(lng, resource, data, opts);
117
+ /** @deprecated compat:factory-refactor — use `i18nHelper.initI18nResource(…)` */
118
+ export const initI18nResource = (lng, resource, ns) => i18nHelper.initI18nResource(lng, resource, ns);
119
+ /** @deprecated compat:factory-refactor — use `i18nHelper.resolveI18nResource(…)` */
120
+ export const resolveI18nResource = (lng, resource, ns) => i18nHelper.resolveI18nResource(lng, resource, ns);
121
+ /** @deprecated compat:factory-refactor — use `i18nHelper.addI18nLoader(…)` */
122
+ export const addI18nLoader = (lng, loader) => i18nHelper.addI18nLoader(lng, loader);
123
+ /** @deprecated compat:factory-refactor — use `i18nHelper.loadI18nLanguage(…)` */
124
+ export const loadI18nLanguage = (lng) => i18nHelper.loadI18nLanguage(lng);
125
+ /** @deprecated compat:factory-refactor — use `i18nHelper.isI18nLanguageLoaded(…)` */
126
+ export const isI18nLanguageLoaded = (lng) => i18nHelper.isI18nLanguageLoaded(lng);
50
127
  //# sourceMappingURL=helper.js.map