Skip to content

Commit 8a22f24

Browse files
V16: Localization extensions load unordered (#19474)
* chore: export useful rxjs functions * fix: use switchMap to ensure correct loading of localization extensions also added filter() and distinctUntilChanged() to ensure the logic is not run more often than what is needed * test: adds tests for async localization extensions and weights * chore: apply simpler sorting syntax * chore: adds catchError to ensure the whole stream is not stopped because of an error * chore: lowest weight should win * chore: move catchError so it catches everything * chore: returns an observable to not break the stream * chore: reverse weight as the previous was correct * chore: adds a true comparer function that is more efficient * Import order sorting * Export order sorting --------- Co-authored-by: leekelleher <[email protected]>
1 parent f70d1c0 commit 8a22f24

File tree

4 files changed

+346
-85
lines changed

4 files changed

+346
-85
lines changed
Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
export {
2+
BehaviorSubject,
3+
Observable,
24
ReplaySubject,
35
Subject,
4-
Observable,
5-
BehaviorSubject,
66
Subscription,
7-
map,
8-
distinctUntilChanged,
7+
catchError,
98
combineLatest,
10-
shareReplay,
11-
takeUntil,
129
debounceTime,
13-
tap,
14-
of,
15-
lastValueFrom,
16-
firstValueFrom,
17-
switchMap,
10+
distinctUntilChanged,
1811
filter,
19-
startWith,
20-
skip,
2112
first,
13+
firstValueFrom,
14+
from,
15+
lastValueFrom,
16+
map,
17+
of,
18+
shareReplay,
19+
skip,
20+
startWith,
21+
switchMap,
22+
takeUntil,
23+
tap,
2224
} from 'rxjs';

src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.test.ts

Lines changed: 111 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@ import { aTimeout, elementUpdated, expect, fixture, html } from '@open-wc/testin
33
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
44
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
55
import { umbLocalizationRegistry } from './registry/localization.registry.js';
6+
import type { ManifestLocalization } from './extensions/localization.extension.js';
67

7-
const english = {
8+
const english: ManifestLocalization = {
89
type: 'localization',
910
alias: 'test.en',
1011
name: 'Test English',
12+
weight: 100,
1113
meta: {
1214
culture: 'en',
1315
localizations: {
@@ -27,7 +29,75 @@ const english = {
2729
},
2830
};
2931

30-
const danish = {
32+
const englishUs: ManifestLocalization = {
33+
type: 'localization',
34+
alias: 'test.en-us',
35+
name: 'Test English (US)',
36+
weight: 100,
37+
meta: {
38+
culture: 'en-us',
39+
localizations: {
40+
general: {
41+
close: 'Close US',
42+
overridden: 'Overridden',
43+
},
44+
},
45+
},
46+
};
47+
48+
// This is a factory function that returns the localization object.
49+
const asyncFactory = async (localizations: Record<string, any>, delay: number) => {
50+
await aTimeout(delay); // Simulate async loading
51+
return {
52+
// Simulate a JS module that exports a localization object.
53+
default: localizations,
54+
};
55+
};
56+
57+
// This is an async localization that overrides the previous one.
58+
const englishAsyncOverride: ManifestLocalization = {
59+
type: 'localization',
60+
alias: 'test.en.async-override',
61+
name: 'Test English Async Override',
62+
weight: -100,
63+
meta: {
64+
culture: 'en-us',
65+
},
66+
js: () =>
67+
asyncFactory(
68+
{
69+
general: {
70+
close: 'Close Async',
71+
overridden: 'Overridden Async',
72+
},
73+
},
74+
100,
75+
),
76+
};
77+
78+
// This is another async localization that loads later than the previous one and overrides it because of a lower weight.
79+
const english2AsyncOverride: ManifestLocalization = {
80+
type: 'localization',
81+
alias: 'test.en.async-override-2',
82+
name: 'Test English Async Override 2',
83+
weight: -200,
84+
meta: {
85+
culture: 'en-us',
86+
},
87+
js: () =>
88+
asyncFactory(
89+
{
90+
general: {
91+
close: 'Another Async Close',
92+
},
93+
},
94+
200, // This will load after the first async override
95+
// so it should override the close translation.
96+
// The overridden translation should not be overridden.
97+
),
98+
};
99+
100+
const danish: ManifestLocalization = {
31101
type: 'localization',
32102
alias: 'test.da',
33103
name: 'Test Danish',
@@ -53,8 +123,7 @@ describe('umb-localize', () => {
53123
});
54124

55125
describe('localization', () => {
56-
umbExtensionsRegistry.register(english);
57-
umbExtensionsRegistry.register(danish);
126+
umbExtensionsRegistry.registerMany([english, englishUs, danish]);
58127

59128
beforeEach(async () => {
60129
umbLocalizationRegistry.loadLanguage(english.meta.culture);
@@ -123,13 +192,50 @@ describe('umb-localize', () => {
123192
it('should change the value if the language is changed', async () => {
124193
expect(element.shadowRoot?.innerHTML).to.contain('Close');
125194

195+
// Change to Danish
126196
umbLocalizationRegistry.loadLanguage(danish.meta.culture);
127197
await aTimeout(0);
128198
await elementUpdated(element);
129-
130199
expect(element.shadowRoot?.innerHTML).to.contain('Luk');
131200
});
132201

202+
it('should fall back to the fallback language if the key is not found', async () => {
203+
expect(element.shadowRoot?.innerHTML).to.contain('Close');
204+
205+
// Change to US English
206+
umbLocalizationRegistry.loadLanguage(englishUs.meta.culture);
207+
await aTimeout(0);
208+
await elementUpdated(element);
209+
expect(element.shadowRoot?.innerHTML).to.contain('Close US');
210+
211+
element.key = 'general_overridden';
212+
await elementUpdated(element);
213+
expect(element.shadowRoot?.innerHTML).to.contain('Overridden');
214+
215+
element.key = 'general_logout';
216+
await elementUpdated(element);
217+
expect(element.shadowRoot?.innerHTML).to.contain('Log out');
218+
});
219+
220+
it('should accept a lazy loaded localization', async () => {
221+
umbExtensionsRegistry.registerMany([englishAsyncOverride, english2AsyncOverride]);
222+
umbLocalizationRegistry.loadLanguage(englishAsyncOverride.meta.culture);
223+
await aTimeout(200); // Wait for the async override to load
224+
225+
await elementUpdated(element);
226+
expect(element.shadowRoot?.innerHTML).to.contain(
227+
'Another Async Close',
228+
'(async) Should have overridden the close (from first language)',
229+
);
230+
231+
element.key = 'general_overridden';
232+
await elementUpdated(element);
233+
expect(element.shadowRoot?.innerHTML).to.contain(
234+
'Overridden Async',
235+
'(async) Should not have overridden the overridden (from first language)',
236+
);
237+
});
238+
133239
it('should use the slot if translation is not found', async () => {
134240
element.key = 'non-existing-key';
135241
await elementUpdated(element);

src/Umbraco.Web.UI.Client/src/packages/core/localization/registry/localization.registry.test.ts

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ const englishUk: ManifestLocalization = {
88
type: 'localization',
99
alias: 'test.en',
1010
name: 'Test English (UK)',
11+
weight: 100,
1112
meta: {
1213
culture: 'en',
14+
direction: 'ltr',
1315
localizations: {
1416
general: {
1517
color: 'Colour',
@@ -22,6 +24,7 @@ const english: ManifestLocalization = {
2224
type: 'localization',
2325
alias: 'test.en-us',
2426
name: 'Test English (US)',
27+
weight: 100,
2528
meta: {
2629
culture: 'en-us',
2730
direction: 'ltr',
@@ -46,14 +49,41 @@ const englishOverride: ManifestLocalization = {
4649
type: 'localization',
4750
alias: 'test.en.override',
4851
name: 'Test English',
52+
weight: 0,
4953
meta: {
5054
culture: 'en-us',
5155
localizations: {
5256
general: {
5357
close: 'Close 2',
58+
overridden: 'Overridden',
59+
},
60+
},
61+
},
62+
};
63+
64+
// This is a factory function that returns the localization object.
65+
const englishAsyncFactory = async () => {
66+
await aTimeout(100); // Simulate async loading
67+
return {
68+
// Simulate a JS module that exports a localization object.
69+
default: {
70+
general: {
71+
close: 'Close Async',
72+
overridden: 'Overridden Async',
5473
},
5574
},
75+
};
76+
};
77+
78+
const englishAsyncOverride: ManifestLocalization = {
79+
type: 'localization',
80+
alias: 'test.en.async-override',
81+
name: 'Test English Async Override',
82+
weight: -100,
83+
meta: {
84+
culture: 'en-us',
5685
},
86+
js: englishAsyncFactory,
5787
};
5888

5989
const danish: ManifestLocalization = {
@@ -87,10 +117,7 @@ const danishRegional: ManifestLocalization = {
87117
//#endregion
88118

89119
describe('UmbLocalizeController', () => {
90-
umbExtensionsRegistry.register(englishUk);
91-
umbExtensionsRegistry.register(english);
92-
umbExtensionsRegistry.register(danish);
93-
umbExtensionsRegistry.register(danishRegional);
120+
umbExtensionsRegistry.registerMany([englishUk, english, danish, danishRegional]);
94121

95122
let registry: UmbLocalizationRegistry;
96123

@@ -102,6 +129,16 @@ describe('UmbLocalizeController', () => {
102129

103130
afterEach(() => {
104131
registry.localizations.clear();
132+
registry.destroy();
133+
});
134+
135+
it('should register into the localization manager', async () => {
136+
expect(registry.localizations.size).to.equal(2, 'Should have registered the 2 original iso codes (en, en-us)');
137+
138+
// Register an additional language to test the registry.
139+
registry.loadLanguage(danish.meta.culture);
140+
await aTimeout(0);
141+
expect(registry.localizations.size).to.equal(3, 'Should have registered the 3rd language (da)');
105142
});
106143

107144
it('should set the document language and direction', async () => {
@@ -122,9 +159,48 @@ describe('UmbLocalizeController', () => {
122159

123160
await aTimeout(0);
124161

125-
const current = registry.localizations.get(english.meta.culture);
126-
expect(current).to.have.property('general_close', 'Close 2');
127-
expect(current).to.have.property('general_logout', 'Log out');
162+
const current = registry.localizations.get(englishOverride.meta.culture);
163+
expect(current).to.have.property(
164+
'general_close',
165+
'Close 2',
166+
'Should have overridden the close (from first language)',
167+
);
168+
expect(current).to.have.property('general_logout', 'Log out', 'Should not have overridden the logout');
169+
170+
umbExtensionsRegistry.unregister(englishOverride.alias);
171+
});
172+
173+
it('should load translations based on weight (lowest weight overrides)', async () => {
174+
// set weight to 200, so it will not override the existing translation
175+
const englishOverrideLowWeight = { ...englishOverride, weight: 200 } satisfies ManifestLocalization;
176+
umbExtensionsRegistry.register(englishOverrideLowWeight);
177+
await aTimeout(0);
178+
179+
let current = registry.localizations.get(englishOverrideLowWeight.meta.culture);
180+
expect(current).to.have.property(
181+
'general_close',
182+
'Close',
183+
'Should not have overridden the close (from first language)',
184+
);
185+
expect(current).to.have.property('general_overridden', 'Overridden', 'Should be able to register its own keys');
186+
187+
// Now register a new async override with a lower weight
188+
umbExtensionsRegistry.register(englishAsyncOverride);
189+
await aTimeout(200); // Wait for the async override to load
190+
current = registry.localizations.get(englishOverrideLowWeight.meta.culture);
191+
expect(current).to.have.property(
192+
'general_close',
193+
'Close Async',
194+
'(async) Should have overridden the close (from first language)',
195+
);
196+
expect(current).to.have.property(
197+
'general_overridden',
198+
'Overridden Async',
199+
'(async) Should have overridden the overridden',
200+
);
201+
202+
umbExtensionsRegistry.unregister(englishOverrideLowWeight.alias);
203+
umbExtensionsRegistry.unregister(englishAsyncOverride.alias);
128204
});
129205

130206
it('should be able to switch to the fallback language', async () => {

0 commit comments

Comments
 (0)