@owlmeans/i18n 0.1.18-rc.4 → 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.
Files changed (50) hide show
  1. package/README.md +22 -12
  2. package/agent-meta/manifest.json +3 -3
  3. package/agent-meta/skills/i18n/SKILL.md +125 -21
  4. package/agent-meta/skills/localization/SKILL.md +50 -14
  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 -0
  17. package/build/index.d.ts.map +1 -1
  18. package/build/index.js.map +1 -1
  19. package/build/types.d.ts +20 -1
  20. package/build/types.d.ts.map +1 -1
  21. package/build/utils/consts.d.ts +7 -0
  22. package/build/utils/consts.d.ts.map +1 -1
  23. package/build/utils/consts.js +10 -0
  24. package/build/utils/consts.js.map +1 -1
  25. package/build/utils/index.d.ts +0 -1
  26. package/build/utils/index.d.ts.map +1 -1
  27. package/build/utils/index.js +0 -1
  28. package/build/utils/index.js.map +1 -1
  29. package/build/utils/storage/types.d.ts +9 -0
  30. package/build/utils/storage/types.d.ts.map +1 -0
  31. package/build/utils/storage/types.js +2 -0
  32. package/build/utils/storage/types.js.map +1 -0
  33. package/build/utils/storage.d.ts +3 -6
  34. package/build/utils/storage.d.ts.map +1 -1
  35. package/build/utils/storage.js +26 -18
  36. package/build/utils/storage.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/consts.ts +0 -2
  39. package/src/helper/types.ts +46 -0
  40. package/src/helper.ts +151 -39
  41. package/src/index.ts +1 -0
  42. package/src/types.ts +23 -1
  43. package/src/utils/consts.ts +13 -0
  44. package/src/utils/index.ts +0 -1
  45. package/src/utils/storage/types.ts +9 -0
  46. package/src/utils/storage.ts +28 -16
  47. package/tests/context.ts +3 -2
  48. package/tests/loader.spec.ts +153 -0
  49. package/tests/registration.spec.ts +24 -25
  50. package/tests/resolve.spec.ts +64 -0
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
14
+ bun add @owlmeans/i18n@^0.1.18-rc.40
15
15
  ```
16
16
 
17
17
  ## Usage
@@ -19,32 +19,42 @@ bun add @owlmeans/i18n
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
75
+ npx @owlmeans/agent-skills@^0.1.18-rc.57
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.0",
5
- "generatedAt": "2026-08-16T22:20:50.497Z",
4
+ "version": "0.1.18-rc.40",
5
+ "generatedAt": "2026-10-20T23:19:58.598Z",
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,33 +8,102 @@ user-invocable: false
8
8
  # @owlmeans/i18n
9
9
 
10
10
  **Layer:** Core (no runtime deps)
11
- **Install:** `"@owlmeans/i18n": "^0.1.18-rc.0"` in `dependencies`
11
+ **Install:** `"@owlmeans/i18n": "^0.1.18-rc.40"` 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) |
24
- | `SUPPORTED_LNGS` | `['en','pl','ru','be','uk','es','de']` — the canonical language set |
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 |
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'` |
31
+ | `DEFAULT_NAMESPACE` | `'translation'` — i18next's own default namespace, used whenever a lookup names none and `I18nConfig.defaultNs` is unset |
27
32
  | `I18nTier` | `Library \| App` — enum used internally |
28
33
  | `I18nConfig` | `{ defaultLng?, defaultNs?, fallbackLng?, supportedLngs? }` |
34
+ | `I18nResourceOptions` | `{ ns?, priority? }` — the `opts` every `add*` takes |
35
+ | `MAX_PRIORITY` | `Number.MAX_SAFE_INTEGER` — the value substituted for an unset `priority`, which is what makes an unset one sort last |
36
+
37
+ `opts` also accepts a bare string, which is read as `ns`:
38
+ `i18nHelper.addI18nLib('en', 'wallet', walletEn, 'did')`.
39
+
40
+ ## Subpath Exports
41
+
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).
45
+
46
+ Reach for `./utils` only to work around the drain-once rule below: assigning
47
+ `_OwlMeansI18nStorage.data = {}` empties every slot including its `lngInitialized` marks, so a
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`.
29
51
 
30
52
  ## Tiers
31
53
 
32
54
  | Tier | Helper | Default ns | When to use |
33
55
  |------|--------|-----------|-------------|
34
- | Library | `addI18nLib` | `'lib'` | Any `@owlmeans/*` package |
35
- | 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 |
58
+
59
+ App-tier strings deep-merge **over** Library-tier strings at resolution time — but only for the
60
+ same **(ns, resource, language)** slot, because that triple is the address the store is keyed on
61
+ and the only thing `i18nHelper.initI18nResource` drains. The default namespaces differ (`'lib'` for
62
+ `addI18nLib`, the resource name for `addI18nApp`), so overriding a library bundle means saying so:
63
+
64
+ ```typescript
65
+ import { i18nHelper, LIB_NAMESPACE } from '@owlmeans/i18n'
66
+
67
+ i18nHelper.addI18nApp('en', 'errors', myErrors, { ns: LIB_NAMESPACE })
68
+ ```
36
69
 
37
- App-tier strings deep-merge **over** Library-tier strings for the same keys at resolution time — this is the override mechanism.
70
+ Without `{ ns: LIB_NAMESPACE }` the app bundle lands in namespace `errors` while the library's sits
71
+ in `lib`. Nothing errors, nothing merges, and the library strings keep rendering.
72
+
73
+ Within one tier the order is `priority`, ascending, and every bundle is merged over the one before
74
+ it — so the **last** applied wins. A registration that states no `priority` sorts last and
75
+ therefore beats every one that states a number: `priority` lowers a bundle in the stack rather than
76
+ raising it. Leave it unset unless one library must lose to another.
77
+
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.
38
107
 
39
108
  ## Per-package pattern
40
109
 
@@ -42,7 +111,7 @@ Every package that ships translatable strings exports a side-effect `i18n.ts`:
42
111
 
43
112
  ```typescript
44
113
  // src/i18n.ts
45
- import { addI18nLib } from '@owlmeans/i18n'
114
+ import { i18nHelper } from '@owlmeans/i18n'
46
115
  import en from './i18n/en.json' with { type: 'json' }
47
116
  import pl from './i18n/pl.json' with { type: 'json' }
48
117
  import ru from './i18n/ru.json' with { type: 'json' }
@@ -50,14 +119,16 @@ import be from './i18n/be.json' with { type: 'json' }
50
119
  import uk from './i18n/uk.json' with { type: 'json' }
51
120
  import es from './i18n/es.json' with { type: 'json' }
52
121
  import de from './i18n/de.json' with { type: 'json' }
53
-
54
- addI18nLib('en', 'my-package', en)
55
- addI18nLib('pl', 'my-package', pl)
56
- addI18nLib('ru', 'my-package', ru)
57
- addI18nLib('be', 'my-package', be)
58
- addI18nLib('uk', 'my-package', uk)
59
- addI18nLib('es', 'my-package', es)
60
- 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)
61
132
  ```
62
133
 
63
134
  Then re-export from `src/index.ts`:
@@ -84,12 +155,45 @@ Consumers use `useI18nLib('my-package', 'mySection')` → `t('title')` → resol
84
155
 
85
156
  Use the optional `opts.ns` when keys must live in a namespace other than `'lib'`:
86
157
  ```typescript
87
- addI18nLib('en', 'wallet', walletEn, { ns: 'did' })
158
+ i18nHelper.addI18nLib('en', 'wallet', walletEn, { ns: 'did' })
88
159
  ```
89
160
 
90
161
  ## Languages
91
162
 
92
- 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.
93
197
 
94
198
  ## Depends On
95
199
 
@@ -21,18 +21,18 @@ 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
 
29
29
  ## Compound prefix keys
30
30
 
31
- A key like `t('title')` resolves to `namespace : resource.prefix.key`. The prefix is composed by joining segments with `.` via `composePrefix` (never by hand-concatenating strings).
31
+ A key like `t('title')` resolves to `namespace : resource.prefix.key`. The prefix is composed by joining segments with `.` via `composePrefix` — exported from `@owlmeans/client-i18n` (and re-exported by `@owlmeans/web-panel` and `@owlmeans/mui-panel`), never by hand-concatenating strings.
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')
@@ -44,31 +44,59 @@ t('title')
44
44
 
45
45
  ## Context-based prefix switching
46
46
 
47
- Use `PanelContext` (from `@owlmeans/client-panel`) to inject `resource`, `ns`, or `prefix` into a React sub-tree. Child components that call `usePanelI18n()` or `useFormI18n()` automatically pick up the context value.
47
+ Use `PanelContext` (from `@owlmeans/client-panel`) to inject `resource`, `ns` or `prefix` into a
48
+ React sub-tree. Child components that call `usePanelI18n()` pick those three up, and a nested
49
+ `PanelContext` inherits the outer one field by field.
48
50
 
49
51
  ```tsx
50
- // Override the active namespace/resource for a sub-tree
51
- <PanelContext resource="wl-manager" ns="lib" prefix="wl-dns">
52
- <DnsForm /> {/* DnsForm calls usePanelI18n() — gets ns='lib', resource='wl-manager', prefix='wl-dns' */}
52
+ // Read a library's own bundle from inside an app sub-tree
53
+ <PanelContext resource="payment" ns="lib" prefix="plan">
54
+ <Text name="label.monthly" /> {/* usePanelI18n() → lib:payment.plan.label.monthly */}
53
55
  </PanelContext>
54
56
  ```
55
57
 
56
- Use `i18n` props on individual components to override just one level:
58
+ `suppress` is **not** one of them. `TPanelContext` accepts it because it spreads `I18nBaseProps`,
59
+ but `usePanelI18n` reads only `prefix`, `resource` and `ns`, so a `<PanelContext suppress>` changes
60
+ nothing for any child. Suppression is per component, from that component's own `i18n` prop — which
61
+ is what `Button` and `ActionCtrl` check before translating a label.
62
+
63
+ **Form fields sit in a different context.** `useFormI18n` reads the form context that `FormContext`
64
+ publishes from the enclosing `Form`'s own `i18n` and `name` props; it never touches `PanelContext`.
65
+ So wrapping a form or a field in `<PanelContext …>` changes nothing for any field — redirect a
66
+ field by naming the enclosing `Form` or by giving that `Form` an `i18n` prop.
67
+
68
+ Use the `i18n` prop on an individual component to override just one level. It is declared by the
69
+ components whose props extend `I18nProps` — `Block`, `Text`, `Link`, `Status`, `Button` and `Form`
70
+ in both panel families:
57
71
  ```tsx
58
- <StatusLabel i18n={{ prefix: 'my-status' }} />
72
+ <Status i18n={{ prefix: 'my-status' }} />
59
73
  ```
60
74
 
75
+ Not every component carrying copy has it, and the two that lack it read different contexts.
76
+ `TextInput` — props extend `FormFieldProps` (`name`, `def`) and add `label`, `placeholder`, `hint`,
77
+ `type` and `disableAutocomplete` — resolves `<name>.label`, `<name>.placeholder` and `<name>.hint`
78
+ through `useFormI18n` when the matching prop is `true`, and renders the prop's own string when it
79
+ is given one; redirect it through the enclosing `Form`. `ButtonSelector` — `SelectorProps` is `name`,
80
+ `current`, `options` and `onSelect` — renders one `Button` per option labelled `<name>.<option>`,
81
+ and `Button` resolves through `usePanelI18n` with the app `buttons` and `lib:client-panel.buttons`
82
+ fallbacks; a surrounding `PanelContext` is what redirects it.
83
+
61
84
  ## App-level override of library strings
62
85
 
63
- App-tier registrations deep-merge over Library-tier for the same `(ns, resource)`:
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 `i18nHelper.addI18nApp` defaults it to the **resource**
88
+ name while the library's bundle sits in `lib`:
64
89
  ```typescript
65
- import '@owlmeans/error' // Library tier: lib:errors.*
66
- import { addI18nApp } from '@owlmeans/i18n'
90
+ import '@owlmeans/error' // Library tier: lib:errors.*
91
+ import { i18nHelper, LIB_NAMESPACE } from '@owlmeans/i18n'
67
92
  import myErrors from './i18n/en.json'
68
93
 
69
- addI18nApp('en', 'errors', myErrors) // App tier wins for matching keys
94
+ i18nHelper.addI18nApp('en', 'errors', myErrors, { ns: LIB_NAMESPACE }) // App tier wins for matching keys
70
95
  ```
71
96
 
97
+ Drop `{ ns: LIB_NAMESPACE }` and the app bundle lands in namespace `errors` while `useI18nLib('errors')`
98
+ reads `lib`. Nothing errors and the library strings keep rendering.
99
+
72
100
  ## Per-package checklist
73
101
 
74
102
  When adding translatable strings to any package:
@@ -78,9 +106,17 @@ When adding translatable strings to any package:
78
106
  3. Re-export from `src/index.ts`: `export * from './i18n.js'`
79
107
  4. In components, use `useI18nLib` (library package) or `useI18nApp` (app package) — never hardcode UI strings
80
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
+
81
115
  ## Language switcher
82
116
 
83
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.
84
120
 
85
121
  ```tsx
86
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"}