From 1cba2b03fd035e029a54fddb7494bea4610817ac Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:49:21 +0200 Subject: [PATCH 01/12] move lit properties to UmbLitElement we cannot have lit properties on the UmbElementMixin since we are not guaranteed it's a Lit element --- src/libs/element-api/element.mixin.ts | 5 ----- src/shared/lit-element/lit-element.element.ts | 8 ++++++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/libs/element-api/element.mixin.ts b/src/libs/element-api/element.mixin.ts index 5f84628e5c..2dee24bd1c 100644 --- a/src/libs/element-api/element.mixin.ts +++ b/src/libs/element-api/element.mixin.ts @@ -10,7 +10,6 @@ import { UmbContextProviderController, } from '@umbraco-cms/backoffice/context-api'; import { ObserverCallback, UmbObserverController } from '@umbraco-cms/backoffice/observable-api'; -import { property } from '@umbraco-cms/backoffice/external/lit'; export declare class UmbElement extends UmbControllerHostElement { /** @@ -34,10 +33,6 @@ export declare class UmbElement extends UmbControllerHostElement { export const UmbElementMixin = (superClass: T) => { class UmbElementMixinClass extends UmbControllerHostElementMixin(superClass) implements UmbElement { - // Make `dir` and `lang` reactive properties so they react to language changes: - @property() dir = ''; - @property() lang = ''; - localize: UmbLocalizeController = new UmbLocalizeController(this); /** diff --git a/src/shared/lit-element/lit-element.element.ts b/src/shared/lit-element/lit-element.element.ts index 43266c07aa..d776a26981 100644 --- a/src/shared/lit-element/lit-element.element.ts +++ b/src/shared/lit-element/lit-element.element.ts @@ -1,4 +1,8 @@ -import { LitElement } from '@umbraco-cms/backoffice/external/lit'; +import { LitElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; -export class UmbLitElement extends UmbElementMixin(LitElement) {} +export class UmbLitElement extends UmbElementMixin(LitElement) { + // Make `dir` and `lang` reactive properties so they react to language changes: + @property() dir = ''; + @property() lang = ''; +} From 972082d8947c459a15d55cdd3bd470bc2f3c3b83 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:51:34 +0200 Subject: [PATCH 02/12] load the initial language first from the 'lang' attribute, then from the document, and lastly default 'en-us' --- src/apps/app/app.element.ts | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/apps/app/app.element.ts b/src/apps/app/app.element.ts index c304290c78..651f71c9aa 100644 --- a/src/apps/app/app.element.ts +++ b/src/apps/app/app.element.ts @@ -23,19 +23,6 @@ export class UmbAppElement extends UmbLitElement { @property({ type: String }) serverUrl = window.location.origin; - /** - * The default culture to use for localization. - * - * When the current user is resolved, the culture will be set to the user's culture. - * - * @attr - * @remarks This is the default culture to use for localization, not the current culture. - * @example "en-us" - * @example "en" - */ - @property({ type: String, attribute: 'default-culture' }) - culture: string = 'en-us'; - /** * The base path of the backoffice. * @@ -89,7 +76,8 @@ export class UmbAppElement extends UmbLitElement { } #setLanguage() { - umbTranslationRegistry.loadLanguage(this.culture); + const initialLanguage = this.lang || document.documentElement.lang || 'en-us'; + umbTranslationRegistry.loadLanguage(initialLanguage); } #listenForLanguageChange(authContext: UmbAuthContext) { From ffce72d793c58ee30b52959e746aae3539f33ed6 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:51:47 +0200 Subject: [PATCH 03/12] fix import order --- src/apps/app/app.element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/apps/app/app.element.ts b/src/apps/app/app.element.ts index 651f71c9aa..2bfef5c26f 100644 --- a/src/apps/app/app.element.ts +++ b/src/apps/app/app.element.ts @@ -1,5 +1,5 @@ -import { umbTranslationRegistry } from '@umbraco-cms/backoffice/localization'; import type { UmbAppErrorElement } from './app-error.element.js'; +import { umbTranslationRegistry } from '@umbraco-cms/backoffice/localization'; import { UMB_AUTH, UmbAuthFlow, UmbAuthContext } from '@umbraco-cms/backoffice/auth'; import { UMB_APP, UmbAppContext } from '@umbraco-cms/backoffice/context'; import { css, html, customElement, property } from '@umbraco-cms/backoffice/external/lit'; From 313a05dd671e87c5a910a76920441c2f5424b5c0 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 10:55:32 +0200 Subject: [PATCH 04/12] add docs to UmbLitElement --- src/shared/lit-element/lit-element.element.ts | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/src/shared/lit-element/lit-element.element.ts b/src/shared/lit-element/lit-element.element.ts index d776a26981..edceb90c3b 100644 --- a/src/shared/lit-element/lit-element.element.ts +++ b/src/shared/lit-element/lit-element.element.ts @@ -1,8 +1,31 @@ import { LitElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; +/** + * The base class for all Umbraco LitElement elements. + * + * @abstract + * @remarks This class is a wrapper around the LitElement class. + * @remarks The `dir` and `lang` properties are defined here as reactive properties so they react to language changes. + */ export class UmbLitElement extends UmbElementMixin(LitElement) { - // Make `dir` and `lang` reactive properties so they react to language changes: - @property() dir = ''; + /** + * The direction of the element. + * + * @attr + * @remarks This is the direction of the element, not the direction of the backoffice. + * @example 'ltr' + * @example 'rtl' + */ + @property() dir: 'rtl' | 'ltr' | '' = ''; + + /** + * The language of the element. + * + * @attr + * @remarks This is the language of the element, not the language of the backoffice. + * @example 'en-us' + * @example 'en' + */ @property() lang = ''; } From 2f926c19b25fd761a85a445d33a8bc7bae72f7df Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:22:38 +0200 Subject: [PATCH 05/12] set default dir --- index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.html b/index.html index 9f1236019b..94d127f213 100644 --- a/index.html +++ b/index.html @@ -1,5 +1,5 @@ - + From 2ea2846f687b82c54c2c08c1ea9b08287c16fc71 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:22:56 +0200 Subject: [PATCH 06/12] optimise translation registry to only load a minimum amount of times --- src/external/rxjs/index.ts | 1 + .../registry/translation.registry.ts | 105 +++++++++++------- 2 files changed, 63 insertions(+), 43 deletions(-) diff --git a/src/external/rxjs/index.ts b/src/external/rxjs/index.ts index 14f961250f..abc0543f3c 100644 --- a/src/external/rxjs/index.ts +++ b/src/external/rxjs/index.ts @@ -15,4 +15,5 @@ export { lastValueFrom, firstValueFrom, switchMap, + filter, } from 'rxjs'; diff --git a/src/packages/core/localization/registry/translation.registry.ts b/src/packages/core/localization/registry/translation.registry.ts index d625d693c9..d561501868 100644 --- a/src/packages/core/localization/registry/translation.registry.ts +++ b/src/packages/core/localization/registry/translation.registry.ts @@ -7,7 +7,14 @@ import { } from '@umbraco-cms/backoffice/localization-api'; import { hasDefaultExport, loadExtension } from '@umbraco-cms/backoffice/extension-api'; import { UmbBackofficeExtensionRegistry, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { Subject, combineLatest, map, distinctUntilChanged, Observable } from '@umbraco-cms/backoffice/external/rxjs'; +import { + Subject, + combineLatest, + map, + distinctUntilChanged, + Observable, + filter, +} from '@umbraco-cms/backoffice/external/rxjs'; export class UmbTranslationRegistry { /** @@ -18,61 +25,72 @@ export class UmbTranslationRegistry { } #currentLanguage = new Subject(); - #currentLanguageUnique: Observable = this.#currentLanguage.pipe( - map((x) => x.toLowerCase()), - distinctUntilChanged() - ); constructor(extensionRegistry: UmbBackofficeExtensionRegistry) { - combineLatest([this.#currentLanguageUnique, extensionRegistry.extensionsOfType('translations')]).subscribe( - async ([userCulture, extensions]) => { - const locale = new Intl.Locale(userCulture); - const translations = await Promise.all( - extensions - .filter( - (x) => - x.meta.culture.toLowerCase() === locale.baseName.toLowerCase() || - x.meta.culture.toLowerCase() === locale.language.toLowerCase() - ) - .map(async (extension) => { - const innerDictionary: UmbTranslationsFlatDictionary = {}; + const currentLanguage$: Observable = this.#currentLanguage.pipe( + map((x) => x.toLowerCase()), + distinctUntilChanged() + ); + + const currentExtensions$ = extensionRegistry.extensionsOfType('translations').pipe( + filter((x) => x.length > 0), + distinctUntilChanged((prev, curr) => prev.length !== curr.length) + ); - // If extension contains a dictionary, add it to the inner dictionary. - if (extension.meta.translations) { - for (const [dictionaryName, dictionary] of Object.entries(extension.meta.translations)) { - this.#addOrUpdateDictionary(innerDictionary, dictionaryName, dictionary); - } + combineLatest([currentLanguage$, currentExtensions$]).subscribe(async ([userCulture, extensions]) => { + const locale = new Intl.Locale(userCulture); + const translations = await Promise.all( + extensions + .filter( + (x) => + x.meta.culture.toLowerCase() === locale.baseName.toLowerCase() || + x.meta.culture.toLowerCase() === locale.language.toLowerCase() + ) + .map(async (extension) => { + const innerDictionary: UmbTranslationsFlatDictionary = {}; + + // If extension contains a dictionary, add it to the inner dictionary. + if (extension.meta.translations) { + for (const [dictionaryName, dictionary] of Object.entries(extension.meta.translations)) { + this.#addOrUpdateDictionary(innerDictionary, dictionaryName, dictionary); } + } - // If extension contains a js file, load it and add the default dictionary to the inner dictionary. - const loadedExtension = await loadExtension(extension); + // If extension contains a js file, load it and add the default dictionary to the inner dictionary. + const loadedExtension = await loadExtension(extension); - if (loadedExtension && hasDefaultExport(loadedExtension)) { - for (const [dictionaryName, dictionary] of Object.entries(loadedExtension.default)) { - this.#addOrUpdateDictionary(innerDictionary, dictionaryName, dictionary); - } + if (loadedExtension && hasDefaultExport(loadedExtension)) { + for (const [dictionaryName, dictionary] of Object.entries(loadedExtension.default)) { + this.#addOrUpdateDictionary(innerDictionary, dictionaryName, dictionary); } + } - // Notify subscribers that the inner dictionary has changed. - return { - $code: extension.meta.culture.toLowerCase(), - $dir: extension.meta.direction ?? 'ltr', - ...innerDictionary, - } satisfies TranslationSet; - }) - ); + // Notify subscribers that the inner dictionary has changed. + return { + $code: extension.meta.culture.toLowerCase(), + $dir: extension.meta.direction ?? 'ltr', + ...innerDictionary, + } satisfies TranslationSet; + }) + ); - if (translations.length) { - registerTranslation(...translations); + if (translations.length) { + console.log('Registering translations for', locale.baseName.toLowerCase(), translations); + registerTranslation(...translations); - // Set the document language - document.documentElement.lang = locale.baseName.toLowerCase(); + // Set the document language + const newLang = locale.baseName.toLowerCase(); + if (document.documentElement.lang.toLowerCase() !== newLang) { + document.documentElement.lang = newLang; + } - // Set the document direction to the direction of the primary language - document.documentElement.dir = translations[0].$dir ?? 'ltr'; + // 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; } } - ); + }); } /** @@ -80,6 +98,7 @@ export class UmbTranslationRegistry { * @param locale The locale to load. */ loadLanguage(locale: string) { + console.log('Loading language', locale); this.#currentLanguage.next(locale); } From 9ace8c178982e8e777deed5fee9f4f3df99c0025 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:24:08 +0200 Subject: [PATCH 07/12] remove debug info --- src/packages/core/localization/registry/translation.registry.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/packages/core/localization/registry/translation.registry.ts b/src/packages/core/localization/registry/translation.registry.ts index d561501868..361e25f432 100644 --- a/src/packages/core/localization/registry/translation.registry.ts +++ b/src/packages/core/localization/registry/translation.registry.ts @@ -75,7 +75,6 @@ export class UmbTranslationRegistry { ); if (translations.length) { - console.log('Registering translations for', locale.baseName.toLowerCase(), translations); registerTranslation(...translations); // Set the document language @@ -98,7 +97,6 @@ export class UmbTranslationRegistry { * @param locale The locale to load. */ loadLanguage(locale: string) { - console.log('Loading language', locale); this.#currentLanguage.next(locale); } From 74e3b80c768b118a40dcfb11e252e33f73a3b63f Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 11:26:27 +0200 Subject: [PATCH 08/12] add TODO --- src/shared/lit-element/lit-element.element.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/shared/lit-element/lit-element.element.ts b/src/shared/lit-element/lit-element.element.ts index edceb90c3b..f8ca45d25d 100644 --- a/src/shared/lit-element/lit-element.element.ts +++ b/src/shared/lit-element/lit-element.element.ts @@ -1,6 +1,8 @@ import { LitElement, property } from '@umbraco-cms/backoffice/external/lit'; import { UmbElementMixin } from '@umbraco-cms/backoffice/element-api'; +// TODO: Currently we don't check if the `lang` is registered in the backoffice. We should do that. We can do that by checking if the `lang` is in the `languages` array of the `language` resource and potentially make sure that UmbTranslationRegistry only loads the translations and some other mechanism reloads to another language (currently it does both) + /** * The base class for all Umbraco LitElement elements. * From 0e1988322c7f35474caf1353e9208d7f54dfe519 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:13:17 +0200 Subject: [PATCH 09/12] add proper comparator --- .../localization/registry/translation.registry.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/packages/core/localization/registry/translation.registry.ts b/src/packages/core/localization/registry/translation.registry.ts index 361e25f432..59eef6729d 100644 --- a/src/packages/core/localization/registry/translation.registry.ts +++ b/src/packages/core/localization/registry/translation.registry.ts @@ -7,14 +7,7 @@ import { } from '@umbraco-cms/backoffice/localization-api'; import { hasDefaultExport, loadExtension } from '@umbraco-cms/backoffice/extension-api'; import { UmbBackofficeExtensionRegistry, umbExtensionsRegistry } from '@umbraco-cms/backoffice/extension-registry'; -import { - Subject, - combineLatest, - map, - distinctUntilChanged, - Observable, - filter, -} from '@umbraco-cms/backoffice/external/rxjs'; +import { Subject, combineLatest, map, distinctUntilChanged, filter } from '@umbraco-cms/backoffice/external/rxjs'; export class UmbTranslationRegistry { /** @@ -27,14 +20,14 @@ export class UmbTranslationRegistry { #currentLanguage = new Subject(); constructor(extensionRegistry: UmbBackofficeExtensionRegistry) { - const currentLanguage$: Observable = this.#currentLanguage.pipe( + const currentLanguage$ = this.#currentLanguage.pipe( map((x) => x.toLowerCase()), distinctUntilChanged() ); const currentExtensions$ = extensionRegistry.extensionsOfType('translations').pipe( filter((x) => x.length > 0), - distinctUntilChanged((prev, curr) => prev.length !== curr.length) + distinctUntilChanged((prev, curr) => prev.length === curr.length && prev.every((x) => curr.includes(x))) ); combineLatest([currentLanguage$, currentExtensions$]).subscribe(async ([userCulture, extensions]) => { From 8f56085bad193b884b82cf83ac5fdb1e0ef723f6 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:13:41 +0200 Subject: [PATCH 10/12] make sure that registerMany registers all extensions at the same time in order not to call next() a million times --- .../registry/extension.registry.ts | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/libs/extension-api/registry/extension.registry.ts b/src/libs/extension-api/registry/extension.registry.ts index be5109d2e5..435bc85ed5 100644 --- a/src/libs/extension-api/registry/extension.registry.ts +++ b/src/libs/extension-api/registry/extension.registry.ts @@ -111,34 +111,17 @@ export class UmbExtensionRegistry< } register(manifest: ManifestTypes | ManifestKind): void { - if (!manifest.type) { - console.error(`Extension is missing type`, manifest); + const isValid = this.checkExtension(manifest); + if (!isValid) { return; } - if (!manifest.alias) { - console.error(`Extension is missing alias`, manifest); - return; - } - - if (manifest.type === 'kind') { - this.defineKind(manifest as ManifestKind); - return; - } - - const extensionsValues = this._extensions.getValue(); - const extension = extensionsValues.find((extension) => extension.alias === (manifest as ManifestTypes).alias); - - if (extension) { - console.error(`Extension with alias ${(manifest as ManifestTypes).alias} is already registered`); - return; - } - - this._extensions.next([...extensionsValues, manifest as ManifestTypes]); + this._extensions.next([...this._extensions.getValue(), manifest as ManifestTypes]); } registerMany(manifests: Array>): void { - manifests.forEach((manifest) => this.register(manifest)); + const validManifests = manifests.filter(this.checkExtension.bind(this)); + this._extensions.next([...this._extensions.getValue(), ...(validManifests as Array)]); } unregisterMany(aliases: Array): void { @@ -172,6 +155,33 @@ export class UmbExtensionRegistry< } */ + private checkExtension(manifest: ManifestTypes | ManifestKind): boolean { + if (!manifest.type) { + console.error(`Extension is missing type`, manifest); + return false; + } + + if (!manifest.alias) { + console.error(`Extension is missing alias`, manifest); + return false; + } + + if (manifest.type === 'kind') { + this.defineKind(manifest as ManifestKind); + return false; + } + + const extensionsValues = this._extensions.getValue(); + const extension = extensionsValues.find((extension) => extension.alias === (manifest as ManifestTypes).alias); + + if (extension) { + console.error(`Extension with alias ${(manifest as ManifestTypes).alias} is already registered`); + return false; + } + + return true; + } + private _kindsOfType | string>(type: Key) { return this.kinds.pipe( map((kinds) => kinds.filter((kind) => kind.matchType === type)), From 6f07663e8f78147d0cfce0c4b25fb3bfc4978c86 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:15:22 +0200 Subject: [PATCH 11/12] we call update() manualle through registerTranslation() at the moment, so no need to observe the 'lang' attribute this might change in the future if we separate loading of languages from changing of languages --- src/libs/localization-api/manager.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/localization-api/manager.ts b/src/libs/localization-api/manager.ts index df1f101c78..97508ab1d7 100644 --- a/src/libs/localization-api/manager.ts +++ b/src/libs/localization-api/manager.ts @@ -26,17 +26,17 @@ export interface DefaultTranslationSet extends TranslationSet { } export const connectedElements = new Set(); -const documentElementObserver = new MutationObserver(update); +// const documentElementObserver = new MutationObserver(update); export const translations: Map = new Map(); export let documentDirection = document.documentElement.dir || 'ltr'; export let documentLanguage = document.documentElement.lang || navigator.language; export let fallback: TranslationSet; // Watch for changes on -documentElementObserver.observe(document.documentElement, { - attributes: true, - attributeFilter: ['dir', 'lang'], -}); +// documentElementObserver.observe(document.documentElement, { +// attributes: true, +// attributeFilter: ['lang'], +// }); /** Registers one or more translations */ export function registerTranslation(...translation: TranslationSet[]) { From ea158b37d4aeab838dc2b22bdee2f95708b7e517 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 11 Aug 2023 12:29:14 +0200 Subject: [PATCH 12/12] Revert "we call update() manualle through registerTranslation() at the moment, so no need to observe the 'lang' attribute" This reverts commit 6f07663e8f78147d0cfce0c4b25fb3bfc4978c86. --- src/libs/localization-api/manager.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/libs/localization-api/manager.ts b/src/libs/localization-api/manager.ts index 97508ab1d7..df1f101c78 100644 --- a/src/libs/localization-api/manager.ts +++ b/src/libs/localization-api/manager.ts @@ -26,17 +26,17 @@ export interface DefaultTranslationSet extends TranslationSet { } export const connectedElements = new Set(); -// const documentElementObserver = new MutationObserver(update); +const documentElementObserver = new MutationObserver(update); export const translations: Map = new Map(); export let documentDirection = document.documentElement.dir || 'ltr'; export let documentLanguage = document.documentElement.lang || navigator.language; export let fallback: TranslationSet; // Watch for changes on -// documentElementObserver.observe(document.documentElement, { -// attributes: true, -// attributeFilter: ['lang'], -// }); +documentElementObserver.observe(document.documentElement, { + attributes: true, + attributeFilter: ['dir', 'lang'], +}); /** Registers one or more translations */ export function registerTranslation(...translation: TranslationSet[]) {