@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 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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=create-i18n-component.spec.d.ts.map
@@ -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.25",
3
+ "version": "1.0.28",
4
4
  "description": "I18n translation package and components for Shades",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "build:es6": "tsc --outDir ./esm"
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
- "Shades"
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.25",
39
- "@furystack/inject": "^12.0.26",
40
- "@furystack/shades": "^11.0.33"
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
- "gitHead": "1045d854bfd8c475b7035471d130d401417a2321"
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
+ })