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 <leekelleher@gmail.com>
This commit is contained in:
@@ -1,22 +1,24 @@
|
||||
export {
|
||||
BehaviorSubject,
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
Subject,
|
||||
Observable,
|
||||
BehaviorSubject,
|
||||
Subscription,
|
||||
map,
|
||||
distinctUntilChanged,
|
||||
catchError,
|
||||
combineLatest,
|
||||
shareReplay,
|
||||
takeUntil,
|
||||
debounceTime,
|
||||
tap,
|
||||
of,
|
||||
lastValueFrom,
|
||||
firstValueFrom,
|
||||
switchMap,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
startWith,
|
||||
skip,
|
||||
first,
|
||||
firstValueFrom,
|
||||
from,
|
||||
lastValueFrom,
|
||||
map,
|
||||
of,
|
||||
shareReplay,
|
||||
skip,
|
||||
startWith,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs';
|
||||
|
||||
@@ -3,11 +3,13 @@ import { aTimeout, elementUpdated, expect, fixture, html } from '@open-wc/testin
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbLocalizationController } from '@umbraco-cms/backoffice/localization-api';
|
||||
import { umbLocalizationRegistry } from './registry/localization.registry.js';
|
||||
import type { ManifestLocalization } from './extensions/localization.extension.js';
|
||||
|
||||
const english = {
|
||||
const english: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.en',
|
||||
name: 'Test English',
|
||||
weight: 100,
|
||||
meta: {
|
||||
culture: 'en',
|
||||
localizations: {
|
||||
@@ -27,7 +29,75 @@ const english = {
|
||||
},
|
||||
};
|
||||
|
||||
const danish = {
|
||||
const englishUs: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.en-us',
|
||||
name: 'Test English (US)',
|
||||
weight: 100,
|
||||
meta: {
|
||||
culture: 'en-us',
|
||||
localizations: {
|
||||
general: {
|
||||
close: 'Close US',
|
||||
overridden: 'Overridden',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// This is a factory function that returns the localization object.
|
||||
const asyncFactory = async (localizations: Record<string, any>, delay: number) => {
|
||||
await aTimeout(delay); // Simulate async loading
|
||||
return {
|
||||
// Simulate a JS module that exports a localization object.
|
||||
default: localizations,
|
||||
};
|
||||
};
|
||||
|
||||
// This is an async localization that overrides the previous one.
|
||||
const englishAsyncOverride: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.en.async-override',
|
||||
name: 'Test English Async Override',
|
||||
weight: -100,
|
||||
meta: {
|
||||
culture: 'en-us',
|
||||
},
|
||||
js: () =>
|
||||
asyncFactory(
|
||||
{
|
||||
general: {
|
||||
close: 'Close Async',
|
||||
overridden: 'Overridden Async',
|
||||
},
|
||||
},
|
||||
100,
|
||||
),
|
||||
};
|
||||
|
||||
// This is another async localization that loads later than the previous one and overrides it because of a lower weight.
|
||||
const english2AsyncOverride: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.en.async-override-2',
|
||||
name: 'Test English Async Override 2',
|
||||
weight: -200,
|
||||
meta: {
|
||||
culture: 'en-us',
|
||||
},
|
||||
js: () =>
|
||||
asyncFactory(
|
||||
{
|
||||
general: {
|
||||
close: 'Another Async Close',
|
||||
},
|
||||
},
|
||||
200, // This will load after the first async override
|
||||
// so it should override the close translation.
|
||||
// The overridden translation should not be overridden.
|
||||
),
|
||||
};
|
||||
|
||||
const danish: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.da',
|
||||
name: 'Test Danish',
|
||||
@@ -53,8 +123,7 @@ describe('umb-localize', () => {
|
||||
});
|
||||
|
||||
describe('localization', () => {
|
||||
umbExtensionsRegistry.register(english);
|
||||
umbExtensionsRegistry.register(danish);
|
||||
umbExtensionsRegistry.registerMany([english, englishUs, danish]);
|
||||
|
||||
beforeEach(async () => {
|
||||
umbLocalizationRegistry.loadLanguage(english.meta.culture);
|
||||
@@ -123,13 +192,50 @@ describe('umb-localize', () => {
|
||||
it('should change the value if the language is changed', async () => {
|
||||
expect(element.shadowRoot?.innerHTML).to.contain('Close');
|
||||
|
||||
// Change to Danish
|
||||
umbLocalizationRegistry.loadLanguage(danish.meta.culture);
|
||||
await aTimeout(0);
|
||||
await elementUpdated(element);
|
||||
|
||||
expect(element.shadowRoot?.innerHTML).to.contain('Luk');
|
||||
});
|
||||
|
||||
it('should fall back to the fallback language if the key is not found', async () => {
|
||||
expect(element.shadowRoot?.innerHTML).to.contain('Close');
|
||||
|
||||
// Change to US English
|
||||
umbLocalizationRegistry.loadLanguage(englishUs.meta.culture);
|
||||
await aTimeout(0);
|
||||
await elementUpdated(element);
|
||||
expect(element.shadowRoot?.innerHTML).to.contain('Close US');
|
||||
|
||||
element.key = 'general_overridden';
|
||||
await elementUpdated(element);
|
||||
expect(element.shadowRoot?.innerHTML).to.contain('Overridden');
|
||||
|
||||
element.key = 'general_logout';
|
||||
await elementUpdated(element);
|
||||
expect(element.shadowRoot?.innerHTML).to.contain('Log out');
|
||||
});
|
||||
|
||||
it('should accept a lazy loaded localization', async () => {
|
||||
umbExtensionsRegistry.registerMany([englishAsyncOverride, english2AsyncOverride]);
|
||||
umbLocalizationRegistry.loadLanguage(englishAsyncOverride.meta.culture);
|
||||
await aTimeout(200); // Wait for the async override to load
|
||||
|
||||
await elementUpdated(element);
|
||||
expect(element.shadowRoot?.innerHTML).to.contain(
|
||||
'Another Async Close',
|
||||
'(async) Should have overridden the close (from first language)',
|
||||
);
|
||||
|
||||
element.key = 'general_overridden';
|
||||
await elementUpdated(element);
|
||||
expect(element.shadowRoot?.innerHTML).to.contain(
|
||||
'Overridden Async',
|
||||
'(async) Should not have overridden the overridden (from first language)',
|
||||
);
|
||||
});
|
||||
|
||||
it('should use the slot if translation is not found', async () => {
|
||||
element.key = 'non-existing-key';
|
||||
await elementUpdated(element);
|
||||
|
||||
@@ -8,8 +8,10 @@ const englishUk: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.en',
|
||||
name: 'Test English (UK)',
|
||||
weight: 100,
|
||||
meta: {
|
||||
culture: 'en',
|
||||
direction: 'ltr',
|
||||
localizations: {
|
||||
general: {
|
||||
color: 'Colour',
|
||||
@@ -22,6 +24,7 @@ const english: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.en-us',
|
||||
name: 'Test English (US)',
|
||||
weight: 100,
|
||||
meta: {
|
||||
culture: 'en-us',
|
||||
direction: 'ltr',
|
||||
@@ -46,16 +49,43 @@ const englishOverride: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.en.override',
|
||||
name: 'Test English',
|
||||
weight: 0,
|
||||
meta: {
|
||||
culture: 'en-us',
|
||||
localizations: {
|
||||
general: {
|
||||
close: 'Close 2',
|
||||
overridden: 'Overridden',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// This is a factory function that returns the localization object.
|
||||
const englishAsyncFactory = async () => {
|
||||
await aTimeout(100); // Simulate async loading
|
||||
return {
|
||||
// Simulate a JS module that exports a localization object.
|
||||
default: {
|
||||
general: {
|
||||
close: 'Close Async',
|
||||
overridden: 'Overridden Async',
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const englishAsyncOverride: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.en.async-override',
|
||||
name: 'Test English Async Override',
|
||||
weight: -100,
|
||||
meta: {
|
||||
culture: 'en-us',
|
||||
},
|
||||
js: englishAsyncFactory,
|
||||
};
|
||||
|
||||
const danish: ManifestLocalization = {
|
||||
type: 'localization',
|
||||
alias: 'test.da',
|
||||
@@ -87,10 +117,7 @@ const danishRegional: ManifestLocalization = {
|
||||
//#endregion
|
||||
|
||||
describe('UmbLocalizeController', () => {
|
||||
umbExtensionsRegistry.register(englishUk);
|
||||
umbExtensionsRegistry.register(english);
|
||||
umbExtensionsRegistry.register(danish);
|
||||
umbExtensionsRegistry.register(danishRegional);
|
||||
umbExtensionsRegistry.registerMany([englishUk, english, danish, danishRegional]);
|
||||
|
||||
let registry: UmbLocalizationRegistry;
|
||||
|
||||
@@ -102,6 +129,16 @@ describe('UmbLocalizeController', () => {
|
||||
|
||||
afterEach(() => {
|
||||
registry.localizations.clear();
|
||||
registry.destroy();
|
||||
});
|
||||
|
||||
it('should register into the localization manager', async () => {
|
||||
expect(registry.localizations.size).to.equal(2, 'Should have registered the 2 original iso codes (en, en-us)');
|
||||
|
||||
// Register an additional language to test the registry.
|
||||
registry.loadLanguage(danish.meta.culture);
|
||||
await aTimeout(0);
|
||||
expect(registry.localizations.size).to.equal(3, 'Should have registered the 3rd language (da)');
|
||||
});
|
||||
|
||||
it('should set the document language and direction', async () => {
|
||||
@@ -122,9 +159,48 @@ describe('UmbLocalizeController', () => {
|
||||
|
||||
await aTimeout(0);
|
||||
|
||||
const current = registry.localizations.get(english.meta.culture);
|
||||
expect(current).to.have.property('general_close', 'Close 2');
|
||||
expect(current).to.have.property('general_logout', 'Log out');
|
||||
const current = registry.localizations.get(englishOverride.meta.culture);
|
||||
expect(current).to.have.property(
|
||||
'general_close',
|
||||
'Close 2',
|
||||
'Should have overridden the close (from first language)',
|
||||
);
|
||||
expect(current).to.have.property('general_logout', 'Log out', 'Should not have overridden the logout');
|
||||
|
||||
umbExtensionsRegistry.unregister(englishOverride.alias);
|
||||
});
|
||||
|
||||
it('should load translations based on weight (lowest weight overrides)', async () => {
|
||||
// set weight to 200, so it will not override the existing translation
|
||||
const englishOverrideLowWeight = { ...englishOverride, weight: 200 } satisfies ManifestLocalization;
|
||||
umbExtensionsRegistry.register(englishOverrideLowWeight);
|
||||
await aTimeout(0);
|
||||
|
||||
let current = registry.localizations.get(englishOverrideLowWeight.meta.culture);
|
||||
expect(current).to.have.property(
|
||||
'general_close',
|
||||
'Close',
|
||||
'Should not have overridden the close (from first language)',
|
||||
);
|
||||
expect(current).to.have.property('general_overridden', 'Overridden', 'Should be able to register its own keys');
|
||||
|
||||
// Now register a new async override with a lower weight
|
||||
umbExtensionsRegistry.register(englishAsyncOverride);
|
||||
await aTimeout(200); // Wait for the async override to load
|
||||
current = registry.localizations.get(englishOverrideLowWeight.meta.culture);
|
||||
expect(current).to.have.property(
|
||||
'general_close',
|
||||
'Close Async',
|
||||
'(async) Should have overridden the close (from first language)',
|
||||
);
|
||||
expect(current).to.have.property(
|
||||
'general_overridden',
|
||||
'Overridden Async',
|
||||
'(async) Should have overridden the overridden',
|
||||
);
|
||||
|
||||
umbExtensionsRegistry.unregister(englishOverrideLowWeight.alias);
|
||||
umbExtensionsRegistry.unregister(englishAsyncOverride.alias);
|
||||
});
|
||||
|
||||
it('should be able to switch to the fallback language', async () => {
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
import type { ManifestLocalization } from '../extensions/localization.extension.js';
|
||||
import {
|
||||
type UmbLocalizationSetBase,
|
||||
type UmbLocalizationDictionary,
|
||||
type UmbLocalizationFlatDictionary,
|
||||
UMB_DEFAULT_LOCALIZATION_CULTURE,
|
||||
} from '@umbraco-cms/backoffice/localization-api';
|
||||
import { umbLocalizationManager } from '@umbraco-cms/backoffice/localization-api';
|
||||
import type { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import { combineLatest } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
catchError,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
from,
|
||||
map,
|
||||
of,
|
||||
switchMap,
|
||||
} from '@umbraco-cms/backoffice/external/rxjs';
|
||||
import { hasDefaultExport, loadManifestPlainJs } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import { umbLocalizationManager, UMB_DEFAULT_LOCALIZATION_CULTURE } from '@umbraco-cms/backoffice/localization-api';
|
||||
import { UmbStringState } from '@umbraco-cms/backoffice/observable-api';
|
||||
import type { UmbBackofficeExtensionRegistry } from '@umbraco-cms/backoffice/extension-registry';
|
||||
import type {
|
||||
UmbLocalizationSetBase,
|
||||
UmbLocalizationDictionary,
|
||||
UmbLocalizationFlatDictionary,
|
||||
} from '@umbraco-cms/backoffice/localization-api';
|
||||
import type { Subscription } from '@umbraco-cms/backoffice/external/rxjs';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param innerDictionary
|
||||
* @param dictionaryName
|
||||
* @param dictionary
|
||||
* Adds or updates a dictionary in the inner dictionary.
|
||||
* @param {UmbLocalizationFlatDictionary} innerDictionary The inner dictionary to add or update the dictionary in.
|
||||
* @param {string} dictionaryName The name of the dictionary to add or update.
|
||||
* @param {UmbLocalizationDictionary['value']} dictionary The dictionary to add or update.
|
||||
*/
|
||||
function addOrUpdateDictionary(
|
||||
innerDictionary: UmbLocalizationFlatDictionary,
|
||||
@@ -34,63 +42,85 @@ export class UmbLocalizationRegistry {
|
||||
);
|
||||
readonly currentLanguage = this.#currentLanguage.asObservable();
|
||||
|
||||
#loadedExtAliases: Array<string> = [];
|
||||
|
||||
/**
|
||||
* Get the current registered translations.
|
||||
* @returns {Map<string, UmbLocalizationSetBase>} Returns the registered translations
|
||||
*/
|
||||
get localizations() {
|
||||
get localizations(): Map<string, UmbLocalizationSetBase> {
|
||||
return umbLocalizationManager.localizations;
|
||||
}
|
||||
|
||||
#subscription: Subscription;
|
||||
|
||||
constructor(extensionRegistry: UmbBackofficeExtensionRegistry) {
|
||||
combineLatest([this.currentLanguage, extensionRegistry.byType('localization')]).subscribe(
|
||||
async ([currentLanguage, extensions]) => {
|
||||
const locale = new Intl.Locale(currentLanguage);
|
||||
const currentLanguageExtensions = extensions.filter(
|
||||
(ext) =>
|
||||
ext.meta.culture.toLowerCase() === locale.baseName.toLowerCase() ||
|
||||
ext.meta.culture.toLowerCase() === locale.language.toLowerCase(),
|
||||
);
|
||||
// Store the locale in a variable to use when setting the document language and direction
|
||||
let locale: Intl.Locale | undefined = undefined;
|
||||
|
||||
// If there are no extensions for the current language, return early
|
||||
if (!currentLanguageExtensions.length) return;
|
||||
|
||||
// Register the new translations only if they have not been registered before
|
||||
const diff = currentLanguageExtensions.filter((ext) => !this.#loadedExtAliases.includes(ext.alias));
|
||||
|
||||
// Load all localizations
|
||||
const translations = await Promise.all(currentLanguageExtensions.map(this.#loadExtension));
|
||||
|
||||
// If there are no translations, return early
|
||||
if (!translations.length) return;
|
||||
|
||||
if (diff.length) {
|
||||
const filteredTranslations = translations.filter((t) =>
|
||||
diff.some((ext) => ext.meta.culture.toLowerCase() === t.$code),
|
||||
this.#subscription = this.currentLanguage
|
||||
.pipe(
|
||||
// Ensure the current language is not empty
|
||||
filter((currentLanguage) => !!currentLanguage),
|
||||
// Use distinctUntilChanged to avoid unnecessary re-renders when the language hasn't changed
|
||||
distinctUntilChanged(),
|
||||
// Switch to the extensions registry to get the current language and the extensions for that language
|
||||
// Note: This also cancels the previous subscription if the language changes
|
||||
switchMap((currentLanguage) => {
|
||||
return extensionRegistry.byType('localization').pipe(
|
||||
// Filter the extensions to only those that match the current language
|
||||
map((extensions) => {
|
||||
locale = new Intl.Locale(currentLanguage);
|
||||
return extensions.filter(
|
||||
(ext) =>
|
||||
ext.meta.culture.toLowerCase() === locale!.baseName.toLowerCase() ||
|
||||
ext.meta.culture.toLowerCase() === locale!.language.toLowerCase(),
|
||||
);
|
||||
}),
|
||||
);
|
||||
umbLocalizationManager.registerManyLocalizations(filteredTranslations);
|
||||
}
|
||||
}),
|
||||
// Ensure we only process extensions that are registered
|
||||
filter((extensions) => extensions.length > 0),
|
||||
// Ensure we only process extensions that have not been loaded before
|
||||
distinctUntilChanged((prev, curr) => {
|
||||
const prevAliases = prev.map((ext) => ext.alias).sort();
|
||||
const currAliases = curr.map((ext) => ext.alias).sort();
|
||||
return this.#arraysEqual(prevAliases, currAliases);
|
||||
}),
|
||||
// With switchMap, if a new language is selected before the previous translations finish loading,
|
||||
// the previous promise is canceled (unsubscribed), and only the latest one is processed.
|
||||
// This prevents race conditions and stale state.
|
||||
switchMap((extensions) =>
|
||||
from(
|
||||
(async () => {
|
||||
// Load all localizations
|
||||
const translations = await Promise.all(extensions.map(this.#loadExtension));
|
||||
|
||||
// Set the document language
|
||||
const newLang = locale.baseName.toLowerCase();
|
||||
if (document.documentElement.lang.toLowerCase() !== newLang) {
|
||||
document.documentElement.lang = newLang;
|
||||
}
|
||||
// If there are no translations, return early
|
||||
if (!translations.length) return;
|
||||
|
||||
// Set the document direction to the direction of the primary language
|
||||
const newDir = translations[0].$dir ?? 'ltr';
|
||||
if (document.documentElement.dir !== newDir) {
|
||||
document.documentElement.dir = newDir;
|
||||
}
|
||||
},
|
||||
);
|
||||
// Sort translations by their original extension weight (highest-to-lowest)
|
||||
// This ensures that the translations with the lowest weight override the others
|
||||
translations.sort((a, b) => b.$weight - a.$weight);
|
||||
|
||||
// Load the translations into the localization manager
|
||||
umbLocalizationManager.registerManyLocalizations(translations);
|
||||
|
||||
// Set the browser language and direction based on the translations
|
||||
this.#setBrowserLanguage(locale!, translations);
|
||||
})(),
|
||||
),
|
||||
),
|
||||
// Catch any errors that occur while loading the translations
|
||||
// This is important to ensure that the observable does not error out and stop the subscription
|
||||
catchError((error) => {
|
||||
console.error('Error loading translations:', error);
|
||||
return of([]);
|
||||
}),
|
||||
)
|
||||
// Subscribe to the observable to trigger the loading of translations
|
||||
.subscribe();
|
||||
}
|
||||
|
||||
#loadExtension = async (extension: ManifestLocalization) => {
|
||||
this.#loadedExtAliases.push(extension.alias);
|
||||
|
||||
const innerDictionary: UmbLocalizationFlatDictionary = {};
|
||||
|
||||
// If extension contains a dictionary, add it to the inner dictionary.
|
||||
@@ -115,16 +145,63 @@ export class UmbLocalizationRegistry {
|
||||
return {
|
||||
$code: extension.meta.culture.toLowerCase(),
|
||||
$dir: extension.meta.direction ?? 'ltr',
|
||||
$weight: extension.weight ?? 100,
|
||||
...innerDictionary,
|
||||
} satisfies UmbLocalizationSetBase;
|
||||
} satisfies UmbLocalizationSetBase & { $weight: number };
|
||||
};
|
||||
|
||||
#setBrowserLanguage(locale: Intl.Locale, translations: UmbLocalizationSetBase[]) {
|
||||
// Set the document language
|
||||
const newLang = locale.baseName.toLowerCase();
|
||||
if (document.documentElement.lang.toLowerCase() !== newLang) {
|
||||
document.documentElement.lang = newLang;
|
||||
}
|
||||
|
||||
// We need to find the direction of the new language, so we look for the best match
|
||||
// If the new language is not found, we default to 'ltr'
|
||||
const reverseTranslations = translations.slice().reverse();
|
||||
|
||||
// Look for a direct match first
|
||||
const directMatch = reverseTranslations.find((t) => t.$code.toLowerCase() === newLang);
|
||||
if (directMatch) {
|
||||
document.documentElement.dir = directMatch.$dir;
|
||||
return;
|
||||
}
|
||||
|
||||
// If no direct match, look for a match with the language code only
|
||||
const langOnlyDirectMatch = reverseTranslations.find(
|
||||
(t) => t.$code.toLowerCase() === locale.language.toLowerCase(),
|
||||
);
|
||||
if (langOnlyDirectMatch) {
|
||||
document.documentElement.dir = langOnlyDirectMatch.$dir;
|
||||
return;
|
||||
}
|
||||
|
||||
// If no match is found, default to 'ltr'
|
||||
if (document.documentElement.dir !== 'ltr') {
|
||||
document.documentElement.dir = 'ltr';
|
||||
}
|
||||
}
|
||||
|
||||
#arraysEqual(a: string[], b: string[]) {
|
||||
if (a.length !== b.length) return false;
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load a language from the extension registry.
|
||||
* @param {string} locale The locale to load.
|
||||
*/
|
||||
loadLanguage(locale: string) {
|
||||
this.#currentLanguage.setValue(locale.toLowerCase());
|
||||
const canonicalLocale = Intl.getCanonicalLocales(locale)[0];
|
||||
this.#currentLanguage.setValue(canonicalLocale);
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.#subscription.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user