@furystack/shades-i18n 1.0.25 → 1.0.28
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/CHANGELOG.md +18 -0
- package/esm/create-i18n-component.spec.d.ts +2 -0
- package/esm/create-i18n-component.spec.d.ts.map +1 -0
- package/esm/create-i18n-component.spec.js +216 -0
- package/esm/create-i18n-component.spec.js.map +1 -0
- package/package.json +14 -9
- package/src/create-i18n-component.spec.tsx +263 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.0.28] - 2026-02-01
|
|
4
|
+
|
|
5
|
+
### ⬆️ Dependencies
|
|
6
|
+
|
|
7
|
+
- Updated peer dependency `@furystack/shades` to include new CSS styling features
|
|
8
|
+
|
|
9
|
+
## [1.0.27] - 2026-01-26
|
|
10
|
+
|
|
11
|
+
### 🔧 Chores
|
|
12
|
+
|
|
13
|
+
- Standardized author format, improved keywords, removed obsolete `gitHead`, added `engines` (Node 22+) and `sideEffects: false`
|
|
14
|
+
|
|
15
|
+
## [1.0.26] - 2026-01-26
|
|
16
|
+
|
|
17
|
+
### ⬆️ Dependencies
|
|
18
|
+
|
|
19
|
+
- Updated `@furystack/inject` with fix for singleton injector reference being overwritten by child injectors
|
|
20
|
+
|
|
3
21
|
## [1.0.25] - 2026-01-22
|
|
4
22
|
|
|
5
23
|
### ⬆️ Dependencies
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-i18n-component.spec.d.ts","sourceRoot":"","sources":["../src/create-i18n-component.spec.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { I18NService } from '@furystack/i18n';
|
|
2
|
+
import { Injector } from '@furystack/inject';
|
|
3
|
+
import { createComponent, initializeShadeRoot } from '@furystack/shades';
|
|
4
|
+
import { sleepAsync, usingAsync } from '@furystack/utils';
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
+
import { createI18nComponent } from './create-i18n-component.js';
|
|
7
|
+
const createTestService = () => {
|
|
8
|
+
return new I18NService({
|
|
9
|
+
code: 'en',
|
|
10
|
+
values: {
|
|
11
|
+
hello: 'Hello',
|
|
12
|
+
goodbye: 'Goodbye',
|
|
13
|
+
world: 'World',
|
|
14
|
+
},
|
|
15
|
+
}, {
|
|
16
|
+
code: 'hu',
|
|
17
|
+
values: {
|
|
18
|
+
hello: 'Szia',
|
|
19
|
+
goodbye: 'Viszlát',
|
|
20
|
+
world: 'Világ',
|
|
21
|
+
},
|
|
22
|
+
}, {
|
|
23
|
+
code: 'de',
|
|
24
|
+
values: {
|
|
25
|
+
hello: 'Hallo',
|
|
26
|
+
goodbye: 'Auf Wiedersehen',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
describe('createI18nComponent', () => {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
document.body.innerHTML = '<div id="root"></div>';
|
|
33
|
+
});
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
document.body.innerHTML = '';
|
|
36
|
+
});
|
|
37
|
+
it('should render with the initial translation', async () => {
|
|
38
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
39
|
+
const service = createTestService();
|
|
40
|
+
const I18n = createI18nComponent({
|
|
41
|
+
service,
|
|
42
|
+
shadowDomName: 'test-i18n-initial',
|
|
43
|
+
});
|
|
44
|
+
const rootElement = document.getElementById('root');
|
|
45
|
+
initializeShadeRoot({
|
|
46
|
+
injector,
|
|
47
|
+
rootElement,
|
|
48
|
+
jsxElement: createComponent(I18n, { key: "hello" }),
|
|
49
|
+
});
|
|
50
|
+
await sleepAsync(50);
|
|
51
|
+
expect(rootElement.textContent).toBe('Hello');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
it('should render with the correct translation for different keys', async () => {
|
|
55
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
56
|
+
const service = createTestService();
|
|
57
|
+
const I18n = createI18nComponent({
|
|
58
|
+
service,
|
|
59
|
+
shadowDomName: 'test-i18n-keys',
|
|
60
|
+
});
|
|
61
|
+
const rootElement = document.getElementById('root');
|
|
62
|
+
initializeShadeRoot({
|
|
63
|
+
injector,
|
|
64
|
+
rootElement,
|
|
65
|
+
jsxElement: (createComponent(createComponent, null,
|
|
66
|
+
createComponent(I18n, { key: "hello" }),
|
|
67
|
+
createComponent("span", null, " "),
|
|
68
|
+
createComponent(I18n, { key: "world" }))),
|
|
69
|
+
});
|
|
70
|
+
await sleepAsync(50);
|
|
71
|
+
expect(rootElement.textContent).toBe('Hello World');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
it('should update when language changes', async () => {
|
|
75
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
76
|
+
const service = createTestService();
|
|
77
|
+
const I18n = createI18nComponent({
|
|
78
|
+
service,
|
|
79
|
+
shadowDomName: 'test-i18n-language-change',
|
|
80
|
+
});
|
|
81
|
+
const rootElement = document.getElementById('root');
|
|
82
|
+
initializeShadeRoot({
|
|
83
|
+
injector,
|
|
84
|
+
rootElement,
|
|
85
|
+
jsxElement: createComponent(I18n, { key: "hello" }),
|
|
86
|
+
});
|
|
87
|
+
await sleepAsync(50);
|
|
88
|
+
expect(rootElement.textContent).toBe('Hello');
|
|
89
|
+
service.currentLanguage = 'hu';
|
|
90
|
+
await sleepAsync(50);
|
|
91
|
+
expect(rootElement.textContent).toBe('Szia');
|
|
92
|
+
service.currentLanguage = 'de';
|
|
93
|
+
await sleepAsync(50);
|
|
94
|
+
expect(rootElement.textContent).toBe('Hallo');
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
it('should fallback to default language for missing keys', async () => {
|
|
98
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
99
|
+
const service = createTestService();
|
|
100
|
+
const I18n = createI18nComponent({
|
|
101
|
+
service,
|
|
102
|
+
shadowDomName: 'test-i18n-fallback',
|
|
103
|
+
});
|
|
104
|
+
const rootElement = document.getElementById('root');
|
|
105
|
+
initializeShadeRoot({
|
|
106
|
+
injector,
|
|
107
|
+
rootElement,
|
|
108
|
+
jsxElement: createComponent(I18n, { key: "world" }),
|
|
109
|
+
});
|
|
110
|
+
await sleepAsync(50);
|
|
111
|
+
expect(rootElement.textContent).toBe('World');
|
|
112
|
+
// German doesn't have 'world' translation, should fallback to English
|
|
113
|
+
service.currentLanguage = 'de';
|
|
114
|
+
await sleepAsync(50);
|
|
115
|
+
expect(rootElement.textContent).toBe('World');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
it('should handle rapid language changes', async () => {
|
|
119
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
120
|
+
const service = createTestService();
|
|
121
|
+
const I18n = createI18nComponent({
|
|
122
|
+
service,
|
|
123
|
+
shadowDomName: 'test-i18n-rapid-changes',
|
|
124
|
+
});
|
|
125
|
+
const rootElement = document.getElementById('root');
|
|
126
|
+
initializeShadeRoot({
|
|
127
|
+
injector,
|
|
128
|
+
rootElement,
|
|
129
|
+
jsxElement: createComponent(I18n, { key: "hello" }),
|
|
130
|
+
});
|
|
131
|
+
await sleepAsync(50);
|
|
132
|
+
// Rapid language changes
|
|
133
|
+
service.currentLanguage = 'hu';
|
|
134
|
+
service.currentLanguage = 'de';
|
|
135
|
+
service.currentLanguage = 'en';
|
|
136
|
+
service.currentLanguage = 'hu';
|
|
137
|
+
await sleepAsync(50);
|
|
138
|
+
expect(rootElement.textContent).toBe('Szia');
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
it('should cleanup subscription on unmount', async () => {
|
|
142
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
143
|
+
const service = createTestService();
|
|
144
|
+
const unsubscribeSpy = vi.fn();
|
|
145
|
+
const originalSubscribe = service.subscribe.bind(service);
|
|
146
|
+
vi.spyOn(service, 'subscribe').mockImplementation((event, callback) => {
|
|
147
|
+
const subscription = originalSubscribe(event, callback);
|
|
148
|
+
return {
|
|
149
|
+
[Symbol.dispose]: () => {
|
|
150
|
+
unsubscribeSpy();
|
|
151
|
+
subscription[Symbol.dispose]();
|
|
152
|
+
},
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
const I18n = createI18nComponent({
|
|
156
|
+
service,
|
|
157
|
+
shadowDomName: 'test-i18n-cleanup',
|
|
158
|
+
});
|
|
159
|
+
const rootElement = document.getElementById('root');
|
|
160
|
+
initializeShadeRoot({
|
|
161
|
+
injector,
|
|
162
|
+
rootElement,
|
|
163
|
+
jsxElement: createComponent(I18n, { key: "hello" }),
|
|
164
|
+
});
|
|
165
|
+
await sleepAsync(50);
|
|
166
|
+
expect(unsubscribeSpy).not.toHaveBeenCalled();
|
|
167
|
+
// Unmount by clearing the DOM
|
|
168
|
+
document.body.innerHTML = '';
|
|
169
|
+
await sleepAsync(50);
|
|
170
|
+
expect(unsubscribeSpy).toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
it('should render as span element', async () => {
|
|
174
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
175
|
+
const service = createTestService();
|
|
176
|
+
const I18n = createI18nComponent({
|
|
177
|
+
service,
|
|
178
|
+
shadowDomName: 'test-i18n-span',
|
|
179
|
+
});
|
|
180
|
+
const rootElement = document.getElementById('root');
|
|
181
|
+
initializeShadeRoot({
|
|
182
|
+
injector,
|
|
183
|
+
rootElement,
|
|
184
|
+
jsxElement: createComponent(I18n, { key: "hello" }),
|
|
185
|
+
});
|
|
186
|
+
await sleepAsync(50);
|
|
187
|
+
// The component uses elementBaseName: 'span', so it renders as <span is="test-i18n-span">
|
|
188
|
+
const i18nElement = rootElement.querySelector('span[is="test-i18n-span"]');
|
|
189
|
+
expect(i18nElement).toBeInstanceOf(HTMLSpanElement);
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
it('should work with multiple instances using different keys', async () => {
|
|
193
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
194
|
+
const service = createTestService();
|
|
195
|
+
const I18n = createI18nComponent({
|
|
196
|
+
service,
|
|
197
|
+
shadowDomName: 'test-i18n-multiple',
|
|
198
|
+
});
|
|
199
|
+
const rootElement = document.getElementById('root');
|
|
200
|
+
initializeShadeRoot({
|
|
201
|
+
injector,
|
|
202
|
+
rootElement,
|
|
203
|
+
jsxElement: (createComponent("div", null,
|
|
204
|
+
createComponent(I18n, { key: "hello" }),
|
|
205
|
+
' - ',
|
|
206
|
+
createComponent(I18n, { key: "goodbye" }))),
|
|
207
|
+
});
|
|
208
|
+
await sleepAsync(50);
|
|
209
|
+
expect(rootElement.textContent).toBe('Hello - Goodbye');
|
|
210
|
+
service.currentLanguage = 'hu';
|
|
211
|
+
await sleepAsync(50);
|
|
212
|
+
expect(rootElement.textContent).toBe('Szia - Viszlát');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
//# sourceMappingURL=create-i18n-component.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-i18n-component.spec.js","sourceRoot":"","sources":["../src/create-i18n-component.spec.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/shades-i18n",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.28",
|
|
4
4
|
"description": "I18n translation package and components for Shades",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"build
|
|
7
|
+
"build": "tsc --outDir ./esm"
|
|
8
8
|
},
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"types": "./esm/index.d.ts",
|
|
11
12
|
"import": "./esm/index.js"
|
|
12
13
|
}
|
|
13
14
|
},
|
|
14
15
|
"files": [
|
|
15
16
|
"esm",
|
|
16
|
-
"types",
|
|
17
17
|
"src"
|
|
18
18
|
],
|
|
19
19
|
"repository": {
|
|
@@ -22,27 +22,32 @@
|
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"FuryStack",
|
|
25
|
+
"Shades",
|
|
25
26
|
"i18n",
|
|
26
|
-
"
|
|
27
|
+
"translations",
|
|
28
|
+
"internationalization"
|
|
27
29
|
],
|
|
28
30
|
"publishConfig": {
|
|
29
31
|
"access": "public"
|
|
30
32
|
},
|
|
31
|
-
"author": "gallay.lajos@gmail.com",
|
|
33
|
+
"author": "Gallay Lajos <gallay.lajos@gmail.com>",
|
|
32
34
|
"license": "GPL-2.0",
|
|
33
35
|
"bugs": {
|
|
34
36
|
"url": "https://github.com/furystack/furystack/issues"
|
|
35
37
|
},
|
|
36
38
|
"homepage": "https://github.com/furystack/furystack",
|
|
37
39
|
"dependencies": {
|
|
38
|
-
"@furystack/i18n": "^1.0.
|
|
39
|
-
"@furystack/inject": "^12.0.
|
|
40
|
-
"@furystack/shades": "^11.0
|
|
40
|
+
"@furystack/i18n": "^1.0.27",
|
|
41
|
+
"@furystack/inject": "^12.0.28",
|
|
42
|
+
"@furystack/shades": "^11.1.0"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
|
43
45
|
"@types/node": "^25.0.10",
|
|
44
46
|
"typescript": "^5.9.3",
|
|
45
47
|
"vitest": "^4.0.17"
|
|
46
48
|
},
|
|
47
|
-
"
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=22.0.0"
|
|
51
|
+
},
|
|
52
|
+
"sideEffects": false
|
|
48
53
|
}
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { I18NService } from '@furystack/i18n'
|
|
2
|
+
import { Injector } from '@furystack/inject'
|
|
3
|
+
import { createComponent, initializeShadeRoot } from '@furystack/shades'
|
|
4
|
+
import { sleepAsync, usingAsync } from '@furystack/utils'
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { createI18nComponent } from './create-i18n-component.js'
|
|
7
|
+
|
|
8
|
+
type TestKeys = 'hello' | 'goodbye' | 'world'
|
|
9
|
+
|
|
10
|
+
const createTestService = () => {
|
|
11
|
+
return new I18NService<TestKeys>(
|
|
12
|
+
{
|
|
13
|
+
code: 'en',
|
|
14
|
+
values: {
|
|
15
|
+
hello: 'Hello',
|
|
16
|
+
goodbye: 'Goodbye',
|
|
17
|
+
world: 'World',
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
code: 'hu',
|
|
22
|
+
values: {
|
|
23
|
+
hello: 'Szia',
|
|
24
|
+
goodbye: 'Viszlát',
|
|
25
|
+
world: 'Világ',
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
code: 'de',
|
|
30
|
+
values: {
|
|
31
|
+
hello: 'Hallo',
|
|
32
|
+
goodbye: 'Auf Wiedersehen',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
describe('createI18nComponent', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
document.body.innerHTML = '<div id="root"></div>'
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
document.body.innerHTML = ''
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should render with the initial translation', async () => {
|
|
48
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
49
|
+
const service = createTestService()
|
|
50
|
+
const I18n = createI18nComponent({
|
|
51
|
+
service,
|
|
52
|
+
shadowDomName: 'test-i18n-initial',
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
56
|
+
initializeShadeRoot({
|
|
57
|
+
injector,
|
|
58
|
+
rootElement,
|
|
59
|
+
jsxElement: <I18n key="hello" />,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
await sleepAsync(50)
|
|
63
|
+
expect(rootElement.textContent).toBe('Hello')
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should render with the correct translation for different keys', async () => {
|
|
68
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
69
|
+
const service = createTestService()
|
|
70
|
+
const I18n = createI18nComponent({
|
|
71
|
+
service,
|
|
72
|
+
shadowDomName: 'test-i18n-keys',
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
76
|
+
initializeShadeRoot({
|
|
77
|
+
injector,
|
|
78
|
+
rootElement,
|
|
79
|
+
jsxElement: (
|
|
80
|
+
<>
|
|
81
|
+
<I18n key="hello" />
|
|
82
|
+
<span> </span>
|
|
83
|
+
<I18n key="world" />
|
|
84
|
+
</>
|
|
85
|
+
),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
await sleepAsync(50)
|
|
89
|
+
expect(rootElement.textContent).toBe('Hello World')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should update when language changes', async () => {
|
|
94
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
95
|
+
const service = createTestService()
|
|
96
|
+
const I18n = createI18nComponent({
|
|
97
|
+
service,
|
|
98
|
+
shadowDomName: 'test-i18n-language-change',
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
102
|
+
initializeShadeRoot({
|
|
103
|
+
injector,
|
|
104
|
+
rootElement,
|
|
105
|
+
jsxElement: <I18n key="hello" />,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
await sleepAsync(50)
|
|
109
|
+
expect(rootElement.textContent).toBe('Hello')
|
|
110
|
+
|
|
111
|
+
service.currentLanguage = 'hu'
|
|
112
|
+
await sleepAsync(50)
|
|
113
|
+
expect(rootElement.textContent).toBe('Szia')
|
|
114
|
+
|
|
115
|
+
service.currentLanguage = 'de'
|
|
116
|
+
await sleepAsync(50)
|
|
117
|
+
expect(rootElement.textContent).toBe('Hallo')
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
it('should fallback to default language for missing keys', async () => {
|
|
122
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
123
|
+
const service = createTestService()
|
|
124
|
+
const I18n = createI18nComponent({
|
|
125
|
+
service,
|
|
126
|
+
shadowDomName: 'test-i18n-fallback',
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
130
|
+
initializeShadeRoot({
|
|
131
|
+
injector,
|
|
132
|
+
rootElement,
|
|
133
|
+
jsxElement: <I18n key="world" />,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
await sleepAsync(50)
|
|
137
|
+
expect(rootElement.textContent).toBe('World')
|
|
138
|
+
|
|
139
|
+
// German doesn't have 'world' translation, should fallback to English
|
|
140
|
+
service.currentLanguage = 'de'
|
|
141
|
+
await sleepAsync(50)
|
|
142
|
+
expect(rootElement.textContent).toBe('World')
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
it('should handle rapid language changes', async () => {
|
|
147
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
148
|
+
const service = createTestService()
|
|
149
|
+
const I18n = createI18nComponent({
|
|
150
|
+
service,
|
|
151
|
+
shadowDomName: 'test-i18n-rapid-changes',
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
155
|
+
initializeShadeRoot({
|
|
156
|
+
injector,
|
|
157
|
+
rootElement,
|
|
158
|
+
jsxElement: <I18n key="hello" />,
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
await sleepAsync(50)
|
|
162
|
+
|
|
163
|
+
// Rapid language changes
|
|
164
|
+
service.currentLanguage = 'hu'
|
|
165
|
+
service.currentLanguage = 'de'
|
|
166
|
+
service.currentLanguage = 'en'
|
|
167
|
+
service.currentLanguage = 'hu'
|
|
168
|
+
|
|
169
|
+
await sleepAsync(50)
|
|
170
|
+
expect(rootElement.textContent).toBe('Szia')
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('should cleanup subscription on unmount', async () => {
|
|
175
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
176
|
+
const service = createTestService()
|
|
177
|
+
const unsubscribeSpy = vi.fn()
|
|
178
|
+
const originalSubscribe = service.subscribe.bind(service)
|
|
179
|
+
vi.spyOn(service, 'subscribe').mockImplementation((event, callback) => {
|
|
180
|
+
const subscription = originalSubscribe(event, callback)
|
|
181
|
+
return {
|
|
182
|
+
[Symbol.dispose]: () => {
|
|
183
|
+
unsubscribeSpy()
|
|
184
|
+
subscription[Symbol.dispose]()
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const I18n = createI18nComponent({
|
|
190
|
+
service,
|
|
191
|
+
shadowDomName: 'test-i18n-cleanup',
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
195
|
+
initializeShadeRoot({
|
|
196
|
+
injector,
|
|
197
|
+
rootElement,
|
|
198
|
+
jsxElement: <I18n key="hello" />,
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
await sleepAsync(50)
|
|
202
|
+
expect(unsubscribeSpy).not.toHaveBeenCalled()
|
|
203
|
+
|
|
204
|
+
// Unmount by clearing the DOM
|
|
205
|
+
document.body.innerHTML = ''
|
|
206
|
+
await sleepAsync(50)
|
|
207
|
+
|
|
208
|
+
expect(unsubscribeSpy).toHaveBeenCalled()
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should render as span element', async () => {
|
|
213
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
214
|
+
const service = createTestService()
|
|
215
|
+
const I18n = createI18nComponent({
|
|
216
|
+
service,
|
|
217
|
+
shadowDomName: 'test-i18n-span',
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
221
|
+
initializeShadeRoot({
|
|
222
|
+
injector,
|
|
223
|
+
rootElement,
|
|
224
|
+
jsxElement: <I18n key="hello" />,
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
await sleepAsync(50)
|
|
228
|
+
// The component uses elementBaseName: 'span', so it renders as <span is="test-i18n-span">
|
|
229
|
+
const i18nElement = rootElement.querySelector('span[is="test-i18n-span"]')
|
|
230
|
+
expect(i18nElement).toBeInstanceOf(HTMLSpanElement)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
it('should work with multiple instances using different keys', async () => {
|
|
235
|
+
await usingAsync(new Injector(), async (injector) => {
|
|
236
|
+
const service = createTestService()
|
|
237
|
+
const I18n = createI18nComponent({
|
|
238
|
+
service,
|
|
239
|
+
shadowDomName: 'test-i18n-multiple',
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
243
|
+
initializeShadeRoot({
|
|
244
|
+
injector,
|
|
245
|
+
rootElement,
|
|
246
|
+
jsxElement: (
|
|
247
|
+
<div>
|
|
248
|
+
<I18n key="hello" />
|
|
249
|
+
{' - '}
|
|
250
|
+
<I18n key="goodbye" />
|
|
251
|
+
</div>
|
|
252
|
+
),
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
await sleepAsync(50)
|
|
256
|
+
expect(rootElement.textContent).toBe('Hello - Goodbye')
|
|
257
|
+
|
|
258
|
+
service.currentLanguage = 'hu'
|
|
259
|
+
await sleepAsync(50)
|
|
260
|
+
expect(rootElement.textContent).toBe('Szia - Viszlát')
|
|
261
|
+
})
|
|
262
|
+
})
|
|
263
|
+
})
|