@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.
- package/README.md +22 -12
- package/agent-meta/manifest.json +3 -3
- package/agent-meta/skills/i18n/SKILL.md +125 -21
- package/agent-meta/skills/localization/SKILL.md +50 -14
- package/build/consts.d.ts +0 -1
- package/build/consts.d.ts.map +1 -1
- package/build/consts.js.map +1 -1
- package/build/helper/types.d.ts +46 -0
- package/build/helper/types.d.ts.map +1 -0
- package/build/helper/types.js +2 -0
- package/build/helper/types.js.map +1 -0
- package/build/helper.d.ts +15 -1
- package/build/helper.d.ts.map +1 -1
- package/build/helper.js +117 -40
- package/build/helper.js.map +1 -1
- package/build/index.d.ts +1 -0
- package/build/index.d.ts.map +1 -1
- package/build/index.js.map +1 -1
- package/build/types.d.ts +20 -1
- package/build/types.d.ts.map +1 -1
- package/build/utils/consts.d.ts +7 -0
- package/build/utils/consts.d.ts.map +1 -1
- package/build/utils/consts.js +10 -0
- package/build/utils/consts.js.map +1 -1
- package/build/utils/index.d.ts +0 -1
- package/build/utils/index.d.ts.map +1 -1
- package/build/utils/index.js +0 -1
- package/build/utils/index.js.map +1 -1
- package/build/utils/storage/types.d.ts +9 -0
- package/build/utils/storage/types.d.ts.map +1 -0
- package/build/utils/storage/types.js +2 -0
- package/build/utils/storage/types.js.map +1 -0
- package/build/utils/storage.d.ts +3 -6
- package/build/utils/storage.d.ts.map +1 -1
- package/build/utils/storage.js +26 -18
- package/build/utils/storage.js.map +1 -1
- package/package.json +1 -1
- package/src/consts.ts +0 -2
- package/src/helper/types.ts +46 -0
- package/src/helper.ts +151 -39
- package/src/index.ts +1 -0
- package/src/types.ts +23 -1
- package/src/utils/consts.ts +13 -0
- package/src/utils/index.ts +0 -1
- package/src/utils/storage/types.ts +9 -0
- package/src/utils/storage.ts +28 -16
- package/tests/context.ts +3 -2
- package/tests/loader.spec.ts +153 -0
- package/tests/registration.spec.ts +24 -25
- 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 {
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
All registration and loading goes through the `i18nHelper` object (`createI18nHelper()` builds one
|
|
37
|
+
over the same process-wide storage).
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
### `i18nHelper.addI18nApp(lng, resource, data, opts?)`
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
Register translations at app tier (highest). Typically called in `src/i18n.ts`.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
### `i18nHelper.addI18nLib(lng, resource, data, opts?)`
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
Register translations at library tier (namespace `lib` by default). App-tier bundles override them.
|
|
46
46
|
|
|
47
|
-
|
|
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
|
package/agent-meta/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 3,
|
|
3
3
|
"package": "@owlmeans/i18n",
|
|
4
|
-
"version": "0.1.18-rc.
|
|
5
|
-
"generatedAt": "2026-
|
|
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,
|
|
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.
|
|
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
|
-
| `
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
55
|
-
addI18nLib('
|
|
56
|
-
addI18nLib('
|
|
57
|
-
addI18nLib('
|
|
58
|
-
addI18nLib('
|
|
59
|
-
addI18nLib('
|
|
60
|
-
addI18nLib('
|
|
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
|
|
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
|
|
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
|
-
//
|
|
51
|
-
<PanelContext resource="
|
|
52
|
-
<
|
|
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
|
-
|
|
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
|
-
<
|
|
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'
|
|
66
|
-
import {
|
|
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)
|
|
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",
|
package/build/consts.d.ts.map
CHANGED
|
@@ -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,
|
|
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"}
|
package/build/consts.js.map
CHANGED
|
@@ -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;
|
|
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 @@
|
|
|
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
|
package/build/helper.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helper.d.ts","sourceRoot":"","sources":["../src/helper.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,
|
|
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"}
|