@firsthandjs/i18n 0.4.0
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/LICENSE +21 -0
- package/README.md +42 -0
- package/dist/index.d.ts +98 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1 -0
- package/package.json +48 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Firsthand contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# @firsthandjs/i18n
|
|
2
|
+
|
|
3
|
+
Makes a translation function reactive: switch the language and every translated
|
|
4
|
+
part on the page updates, without a re-render, a provider or a context.
|
|
5
|
+
|
|
6
|
+
**Documentation:** [guide](https://github.com/firsthandjs/firsthand/blob/main/docs/guide/12-internationalisation.md) · [API reference](https://github.com/firsthandjs/firsthand/blob/main/docs/reference/i18n.md) · [all docs](https://github.com/firsthandjs/firsthand/blob/main/docs/README.md)
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
npm install @firsthandjs/i18n
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
0.38 kB gzip. It depends on `@firsthandjs/core` and on nothing else — not even
|
|
13
|
+
on i18next, whose shape it describes structurally rather than importing.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import i18next from 'i18next';
|
|
17
|
+
import { fromI18next } from '@firsthandjs/i18n';
|
|
18
|
+
|
|
19
|
+
await i18next.init({ lng: 'en', resources });
|
|
20
|
+
export const { t, language } = fromI18next(i18next);
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<h1>{t('greeting', { name: 'Ada' })}</h1>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
That `h1` re-reads when the language changes, and so does every other part that
|
|
28
|
+
called `t` — and nothing else does.
|
|
29
|
+
|
|
30
|
+
i18next, FormatJS, Lingui and Polyglot all work in this framework already; they
|
|
31
|
+
are plain JavaScript. What none of them can do is tell the reactive graph that
|
|
32
|
+
the answer changed, so a label rendered before the switch keeps its old text.
|
|
33
|
+
This package is the connection and nothing more: there is no dictionary here,
|
|
34
|
+
no plural rules and no date formatting, because the libraries that do those
|
|
35
|
+
things are good at them.
|
|
36
|
+
|
|
37
|
+
`translator()` adapts anything with a `t`, a current language and a way to
|
|
38
|
+
subscribe. `fromI18next()` is that adapter for i18next, listening on the
|
|
39
|
+
instance _and_ on its resource store — a namespace that arrives late, or a key
|
|
40
|
+
added at runtime, changes the answer as surely as a language switch does.
|
|
41
|
+
|
|
42
|
+
MIT licensed.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@firsthandjs/i18n` — makes a translation function reactive.
|
|
3
|
+
*
|
|
4
|
+
* i18next, FormatJS, Lingui and Polyglot all work here already: they are plain
|
|
5
|
+
* JavaScript, and nothing about this framework stops you calling `t('greeting')`.
|
|
6
|
+
* What none of them do is tell the *graph* that the answer changed, so a label
|
|
7
|
+
* rendered before the language switch keeps its old text.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import i18next from 'i18next';
|
|
11
|
+
* import { fromI18next } from '@firsthandjs/i18n';
|
|
12
|
+
*
|
|
13
|
+
* await i18next.init({ lng: 'en', resources });
|
|
14
|
+
* export const { t, language } = fromI18next(i18next);
|
|
15
|
+
*
|
|
16
|
+
* // and then, in a component:
|
|
17
|
+
* <h1>{t('greeting', { name: 'Ada' })}</h1> // re-reads on a language change
|
|
18
|
+
* ```
|
|
19
|
+
*
|
|
20
|
+
* The whole package is a version signal and a wrapper that reads it. There is
|
|
21
|
+
* no dictionary here, no plural rules, no date formatting and no intention of
|
|
22
|
+
* ever having any: the libraries that do that are good, and this is the eleven
|
|
23
|
+
* lines that connect them.
|
|
24
|
+
*/
|
|
25
|
+
import { type Dispose, type ReadonlyCell } from '@firsthandjs/core';
|
|
26
|
+
/** A translation function, whatever shape the library gave it. */
|
|
27
|
+
type Translate = (...args: never[]) => unknown;
|
|
28
|
+
/** What a localisation library has to offer to be made reactive. */
|
|
29
|
+
export interface Source<T extends Translate> {
|
|
30
|
+
/** The library's own `t`, untouched — its types are the ones you keep. */
|
|
31
|
+
translate: T;
|
|
32
|
+
/** The language in effect right now, read when something asks. */
|
|
33
|
+
language: () => string;
|
|
34
|
+
/**
|
|
35
|
+
* Calls `changed` whenever a translation could answer differently, and
|
|
36
|
+
* returns the way to stop listening.
|
|
37
|
+
*
|
|
38
|
+
* "Could answer differently" is wider than "the language changed": a backend
|
|
39
|
+
* that loads a namespace late changes the answer too, and forgetting that is
|
|
40
|
+
* why translations sometimes appear as their keys for a moment and then
|
|
41
|
+
* never recover.
|
|
42
|
+
*/
|
|
43
|
+
subscribe: (changed: () => void) => Dispose;
|
|
44
|
+
}
|
|
45
|
+
/** A reactive translation function, and the language it is reading. */
|
|
46
|
+
export interface Translator<T extends Translate> {
|
|
47
|
+
/** The same function, with the same types, that now re-runs when it should. */
|
|
48
|
+
t: T;
|
|
49
|
+
/** The current language, as a cell: read it in a part and it stays current. */
|
|
50
|
+
language: ReadonlyCell<string>;
|
|
51
|
+
/** Stops listening. Registered with the enclosing scope, if there is one. */
|
|
52
|
+
dispose: Dispose;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Wraps any translation function so that reading it subscribes.
|
|
56
|
+
*
|
|
57
|
+
* The wrapper reads a version signal and then calls through. That is the whole
|
|
58
|
+
* mechanism: a part that renders a translation has read the signal, so a
|
|
59
|
+
* language change re-runs exactly that part and nothing else — no re-render,
|
|
60
|
+
* no provider, no context.
|
|
61
|
+
*
|
|
62
|
+
* If called inside a component or a root, the subscription is disposed with
|
|
63
|
+
* that scope. At module level — which is where an application usually sets up
|
|
64
|
+
* its translations — nothing owns it, so it lives as long as the page and
|
|
65
|
+
* `dispose` is there for the cases that need it.
|
|
66
|
+
*/
|
|
67
|
+
export declare function translator<T extends Translate>(source: Source<T>): Translator<T>;
|
|
68
|
+
/**
|
|
69
|
+
* The shape this package needs from i18next.
|
|
70
|
+
*
|
|
71
|
+
* Declared structurally rather than imported, so the package has no dependency
|
|
72
|
+
* on i18next, no opinion about its version, and works with anything that looks
|
|
73
|
+
* like it — `createInstance()`, a mock in a test, or a fork.
|
|
74
|
+
*/
|
|
75
|
+
export interface I18nextLike<T extends Translate> {
|
|
76
|
+
t: T;
|
|
77
|
+
language: string;
|
|
78
|
+
on(event: string, handler: () => void): void;
|
|
79
|
+
off(event: string, handler: () => void): void;
|
|
80
|
+
/**
|
|
81
|
+
* The resource store, if this instance has one.
|
|
82
|
+
*
|
|
83
|
+
* i18next announces a *language* change on the instance and a *resource*
|
|
84
|
+
* change on the store. Listening only to the instance is the mistake that
|
|
85
|
+
* makes `addResource` look like it did nothing — it was found by testing
|
|
86
|
+
* against the real library rather than against something shaped like it.
|
|
87
|
+
*/
|
|
88
|
+
store?: Emitter;
|
|
89
|
+
}
|
|
90
|
+
/** The part of an event emitter this package uses. */
|
|
91
|
+
export interface Emitter {
|
|
92
|
+
on(event: string, handler: () => void): void;
|
|
93
|
+
off(event: string, handler: () => void): void;
|
|
94
|
+
}
|
|
95
|
+
/** Connects an i18next instance. See {@link translator} for what it does. */
|
|
96
|
+
export declare function fromI18next<T extends Translate>(instance: I18nextLike<T>): Translator<T>;
|
|
97
|
+
export {};
|
|
98
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,EAKL,KAAK,OAAO,EACZ,KAAK,YAAY,EAClB,MAAM,mBAAmB,CAAC;AAE3B,kEAAkE;AAClE,KAAK,SAAS,GAAG,CAAC,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,OAAO,CAAC;AAE/C,oEAAoE;AACpE,MAAM,WAAW,MAAM,CAAC,CAAC,SAAS,SAAS;IACzC,0EAA0E;IAC1E,SAAS,EAAE,CAAC,CAAC;IACb,kEAAkE;IAClE,QAAQ,EAAE,MAAM,MAAM,CAAC;IACvB;;;;;;;;OAQG;IACH,SAAS,EAAE,CAAC,OAAO,EAAE,MAAM,IAAI,KAAK,OAAO,CAAC;CAC7C;AAED,uEAAuE;AACvE,MAAM,WAAW,UAAU,CAAC,CAAC,SAAS,SAAS;IAC7C,+EAA+E;IAC/E,CAAC,EAAE,CAAC,CAAC;IACL,+EAA+E;IAC/E,QAAQ,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IAC/B,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;CAClB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,CAAC,SAAS,SAAS,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CA8BhF;AAED;;;;;;GAMG;AACH,MAAM,WAAW,WAAW,CAAC,CAAC,SAAS,SAAS;IAC9C,CAAC,EAAE,CAAC,CAAC;IACL,QAAQ,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC7C,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC9C;;;;;;;OAOG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,sDAAsD;AACtD,MAAM,WAAW,OAAO;IACtB,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;IAC7C,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,IAAI,GAAG,IAAI,CAAC;CAC/C;AAcD,6EAA6E;AAC7E,wBAAgB,WAAW,CAAC,CAAC,SAAS,SAAS,EAAE,QAAQ,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CA4BxF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{batch as f,getOwner as g,onCleanup as T,signal as a}from"@firsthandjs/core";function u(e){let n=a(0),o=a(e.language()),t=e.subscribe(()=>{f(()=>{o.value=e.language(),n.value++})}),i=((...d)=>(n.value,e.translate(...d))),r=()=>{t()};return g()!==null&&T(r),{t:i,language:o,dispose:r}}var s=["languageChanged","loaded"],l=["added","removed"];function c(e){return u({translate:((...n)=>e.t(...n)),language:()=>e.language,subscribe:n=>{let o=e.store;for(let t of s)e.on(t,n);if(o!==void 0)for(let t of l)o.on(t,n);return()=>{for(let t of s)e.off(t,n);if(o!==void 0)for(let t of l)o.off(t,n)}}})}export{c as fromI18next,u as translator};
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@firsthandjs/i18n",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Makes i18next (or any translation function) reactive in Firsthand.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"main": "./dist/index.js",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@firsthandjs/core": "0.4.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20.11.0"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public",
|
|
29
|
+
"provenance": true
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/firsthandjs/firsthand.git",
|
|
34
|
+
"directory": "packages/i18n"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/firsthandjs/firsthand/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/firsthandjs/firsthand#readme",
|
|
40
|
+
"keywords": [
|
|
41
|
+
"firsthand",
|
|
42
|
+
"i18n",
|
|
43
|
+
"i18next",
|
|
44
|
+
"localisation",
|
|
45
|
+
"translation",
|
|
46
|
+
"reactive"
|
|
47
|
+
]
|
|
48
|
+
}
|