From 90195bf469e87630f2a1d2b7159110ea9110791e Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 07:51:59 +0100 Subject: [PATCH 1/9] feat: adds `termOrDefault` to be able to safely fall back to a value if the translation does not exist --- .../localization.controller.test.ts | 68 ++++++++++ .../localization.controller.ts | 122 +++++++++++++----- 2 files changed, 157 insertions(+), 33 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts index e01bc818c6da..807c28e75492 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.test.ts @@ -318,6 +318,74 @@ describe('UmbLocalizationController', () => { }); }); + describe('termOrDefault', () => { + it('should return the translation when the key exists', () => { + expect(controller.termOrDefault('close', 'X')).to.equal('Close'); + expect(controller.termOrDefault('logout', 'Sign out')).to.equal('Log out'); + }); + + it('should return the default value when the key does not exist', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((controller.termOrDefault as any)('nonExistentKey', 'Default Value')).to.equal('Default Value'); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((controller.termOrDefault as any)('anotherMissingKey', 'Fallback')).to.equal('Fallback'); + }); + + it('should work with function-based translations and arguments', () => { + expect(controller.termOrDefault('numUsersSelected', 'No selection', 0)).to.equal('No users selected'); + expect(controller.termOrDefault('numUsersSelected', 'No selection', 1)).to.equal('One user selected'); + expect(controller.termOrDefault('numUsersSelected', 'No selection', 5)).to.equal('5 users selected'); + }); + + it('should work with string-based translations and placeholder arguments', () => { + expect(controller.termOrDefault('withInlineToken', 'N/A', 'Hello', 'World')).to.equal('Hello World'); + expect(controller.termOrDefault('withInlineTokenLegacy', 'N/A', 'Foo', 'Bar')).to.equal('Foo Bar'); + }); + + it('should use default value for missing key even with arguments', () => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((controller.termOrDefault as any)('missingKey', 'Default', 'arg1', 'arg2')).to.equal('Default'); + }); + + it('should handle the three-tier fallback before using defaultValue', async () => { + // Switch to Danish regional + document.documentElement.lang = danishRegional.$code; + await aTimeout(0); + + // Primary (da-dk) has 'close' + expect(controller.termOrDefault('close', 'X')).to.equal('Luk'); + + // Secondary (da) has 'notOnRegional', not on da-dk + expect(controller.termOrDefault('notOnRegional', 'Not found')).to.equal('Not on regional'); + + // Fallback (en) has 'logout', not on da-dk or da + expect(controller.termOrDefault('logout', 'Sign out')).to.equal('Log out'); + + // Non-existent key should use default + // eslint-disable-next-line @typescript-eslint/no-explicit-any + expect((controller.termOrDefault as any)('completelyMissing', 'Fallback Value')).to.equal('Fallback Value'); + }); + + it('should update when language changes', async () => { + expect(controller.termOrDefault('close', 'X')).to.equal('Close'); + + // Switch to Danish + document.documentElement.lang = danishRegional.$code; + await aTimeout(0); + + expect(controller.termOrDefault('close', 'X')).to.equal('Luk'); + }); + + it('should override a term if new localization is registered', () => { + expect(controller.termOrDefault('close', 'X')).to.equal('Close'); + + // Register override + umbLocalizationManager.registerLocalization(englishOverride); + + expect(controller.termOrDefault('close', 'X')).to.equal('Close 2'); + }); + }); + describe('string', () => { it('should replace words prefixed with a # with translated value', async () => { const str = '#close'; diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts index 6872a3cd4026..9d48e213e9d5 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts @@ -110,41 +110,40 @@ export class UmbLocalizationController(key: K, ...args: FunctionParams): string { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + #lookupTerm(key: K): any { if (!this.#usedKeys.includes(key)) { this.#usedKeys.push(key); } const { primary, secondary } = this.#getLocalizationData(this.lang()); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let term: any; - // Look for a matching term using regionCode, code, then the fallback if (primary?.[key]) { - term = primary[key]; + return primary[key]; } else if (secondary?.[key]) { - term = secondary[key]; + return secondary[key]; } else if (umbLocalizationManager.fallback?.[key]) { - term = umbLocalizationManager.fallback[key]; - } else { - return String(key); + return umbLocalizationManager.fallback[key]; } + return null; + } + + /** + * Processes a localization entry (string or function) with the provided arguments. + * @param {any} term - the localization entry to process. + * @param {unknown[]} args - the arguments to apply to the term. + * @returns {string} - the processed term as a string. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + #processTerm(term: any, args: unknown[]): string { if (typeof term === 'function') { return term(...args) as string; } @@ -152,7 +151,7 @@ export class UmbLocalizationController { + return term.replace(/(%(\d+)%|\{(\d+)\})/g, (match, _p1, p2, p3): string => { const index = p2 || p3; return typeof args[index] !== 'undefined' ? String(args[index]) : match; }); @@ -162,6 +161,63 @@ export class UmbLocalizationController(key: K, ...args: FunctionParams): string { + const term = this.#lookupTerm(key); + + if (term === null) { + return String(key); + } + + return this.#processTerm(term, args); + } + + /** + * Returns the localized term for the given key, or the default value if not found. + * This method follows the same resolution order as term() (primary → secondary → fallback), + * but returns the provided defaultValue instead of the key when no translation is found. + * @param {string} key - the localization key, the indicator of what localization entry you want to retrieve. + * @param {string} defaultValue - the value to return if the key is not found in any localization set. + * @param {unknown[]} args - the arguments to parse for this localization entry. + * @returns {string} - the translated term or the default value as a string. + * @example + * Retrieving a term with fallback: + * ```ts + * this.localize.termOrDefault('general_close', 'X'); + * ``` + * Retrieving a term with fallback and arguments: + * ```ts + * this.localize.termOrDefault('general_greeting', 'Hello!', userName); + * ``` + */ + termOrDefault( + key: K, + defaultValue: string, + ...args: FunctionParams + ): string { + const term = this.#lookupTerm(key); + + if (term === null) { + return defaultValue; + } + + return this.#processTerm(term, args); + } + /** * Outputs a localized date in the specified format. * @param {Date} dateToFormat - the date to format. @@ -255,10 +311,10 @@ export class UmbLocalizationController { - const key = match.slice(1); - if (!this.#usedKeys.includes(key)) { - this.#usedKeys.push(key); - } + const key = match.slice(1) as keyof LocalizationSetType; + + const term = this.#lookupTerm(key); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const localized = this.term(key, ...args); // we didn't find a localized string, so we return the original string with the # - return localized === key ? match : localized; + if (term === null) { + return match; + } + + return this.#processTerm(term, args); }); return localizedText; From 805ec3272406c7bf65a86771a78c3b092d17ed60 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:18:35 +0100 Subject: [PATCH 2/9] feat: accepts 'null' as fallback --- .../localization-api/localization.controller.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts index 9d48e213e9d5..08c8b76a6b71 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts @@ -191,9 +191,9 @@ export class UmbLocalizationController( + termOrDefault( key: K, - defaultValue: string, + defaultValue: D, ...args: FunctionParams - ): string { + ): string | D { const term = this.#lookupTerm(key); if (term === null) { From 082eaab4702ed6c15a386457ff2bfedb2f7ada97 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:20:34 +0100 Subject: [PATCH 3/9] feat: uses 'termOrDefault' to do a safe null-check and uses 'willUpdate' to contain number of re-renders --- .../localization/localize-number.element.ts | 28 ++++++-- .../localize-relative-time.element.ts | 31 +++++++-- .../core/localization/localize.element.ts | 67 +++++++++++++------ 3 files changed, 97 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-number.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-number.element.ts index 636cf32d3c72..d6cc3af8e6eb 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-number.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-number.element.ts @@ -1,4 +1,5 @@ -import { css, customElement, html, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValues } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; /** @@ -21,16 +22,33 @@ export class UmbLocalizeNumberElement extends UmbLitElement { * @attr * @example options={ style: 'currency', currency: 'EUR' } */ - @property() + @property({ type: Object }) options?: Intl.NumberFormatOptions; @state() - protected get text(): string { - return this.localize.number(this.number, this.options); + private _text: string | null | undefined = undefined; + + /** + * Computes the localized number when properties change or when the localization controller triggers an update. + * This lifecycle method runs before render and caches the result to avoid repeated computations. + * @param {PropertyValues} changedProperties - The properties that changed since the last update. + */ + protected override willUpdate(changedProperties: PropertyValues): void { + // Update when properties change OR when localization controller triggers update + if (changedProperties.has('number') || changedProperties.has('options') || changedProperties.size === 0) { + this._text = this.number ? this.localize.number(this.number, this.options) : null; + } } override render() { - return this.number ? html`${unsafeHTML(this.text)}` : html``; + // undefined = not yet computed (loading), don't show fallback + // null = no number provided, show fallback + // string = number formatted, show it + if (this._text === undefined) { + return nothing; + } + + return this._text !== null ? html`${unsafeHTML(this._text)}` : html``; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts index 093f43c07af9..0877da0c4f48 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts @@ -1,4 +1,5 @@ -import { css, customElement, html, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValues } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; /** @@ -33,12 +34,34 @@ export class UmbLocalizeRelativeTimeElement extends UmbLitElement { unit: Intl.RelativeTimeFormatUnit = 'seconds'; @state() - protected get text(): string { - return this.localize.relativeTime(this.time, this.unit, this.options); + private _text: string | null | undefined = undefined; + + /** + * Computes the localized relative time when properties change or when the localization controller triggers an update. + * This lifecycle method runs before render and caches the result to avoid repeated computations. + * @param {PropertyValues} changedProperties - The properties that changed since the last update. + */ + protected override willUpdate(changedProperties: PropertyValues): void { + // Update when properties change OR when localization controller triggers update + if ( + changedProperties.has('time') || + changedProperties.has('unit') || + changedProperties.has('options') || + changedProperties.size === 0 + ) { + this._text = this.time ? this.localize.relativeTime(this.time, this.unit, this.options) : null; + } } override render() { - return this.time ? html`${unsafeHTML(this.text)}` : html``; + // undefined = not yet computed (loading), don't show fallback + // null = no time provided, show fallback + // string = time formatted, show it + if (this._text === undefined) { + return nothing; + } + + return this._text !== null ? html`${unsafeHTML(this._text)}` : html``; } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts index ce2df299d3ba..42355512eb04 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts @@ -1,4 +1,5 @@ -import { css, customElement, html, property, state, unsafeHTML, when } from '@umbraco-cms/backoffice/external/lit'; +import type { PropertyValues } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, nothing, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; import { escapeHTML } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -21,43 +22,69 @@ export class UmbLocalizeElement extends UmbLitElement { * The values to forward to the localization function (must be JSON compatible). * @attr * @example args="[1,2,3]" - * @type {any[] | undefined} + * @type {unknown[] | undefined} */ @property({ type: Array }) args?: unknown[]; /** - * If true, the key will be rendered instead of the localized value if the key is not found. + * If true, the key will be rendered instead of the fallback value if the key is not found. * @attr */ @property({ type: Boolean }) debug = false; @state() - protected get text(): string { - // As translated texts can contain HTML, we will need to render with unsafeHTML. - // But arguments can come from user input, so they should be escaped. - const escapedArgs = (this.args ?? []).map((a) => escapeHTML(a)); + private _text: string | null | undefined = undefined; - const localizedValue = this.localize.term(this.key, ...escapedArgs); + /** + * Computes the localized text when properties change or when the localization controller triggers an update. + * This lifecycle method runs before render and caches the result to avoid repeated computations. + * @param {PropertyValues} changedProperties - The properties that changed since the last update. + */ + protected override willUpdate(changedProperties: PropertyValues): void { + // Update when properties change OR when localization controller triggers update + if (changedProperties.has('key') || changedProperties.has('args') || changedProperties.size === 0) { + // As translated texts can contain HTML, we will need to render with unsafeHTML. + // But arguments can come from user input, so they should be escaped. + const escapedArgs = (this.args ?? []).map((a) => escapeHTML(a)); - // If the value is the same as the key, it means the key was not found. - if (localizedValue === this.key) { - (this.getHostElement() as HTMLElement).setAttribute('data-localize-missing', this.key); - return ''; - } + this._text = this.localize.termOrDefault(this.key, null, ...escapedArgs); - (this.getHostElement() as HTMLElement).removeAttribute('data-localize-missing'); + if (this._text !== null) { + this._text = this._text.trim(); + } + } + } - return localizedValue.trim(); + /** + * Updates the data-localize-missing attribute after the element has rendered. + * This attribute is set when the localization key is not found, allowing for debugging and styling. + * @param {PropertyValues} changedProperties - The properties that changed since the last update. + */ + protected override updated(changedProperties: PropertyValues): void { + if (changedProperties.has('_text')) { + if (this._text === null) { + this.setAttribute('data-localize-missing', this.key); + } else { + this.removeAttribute('data-localize-missing'); + } + } } override render() { - return when( - this.text, - (text) => unsafeHTML(text), - () => (this.debug ? html`${this.key}` : html``), - ); + // undefined = not yet computed (loading), don't show fallback + // null = key not found, show fallback + // string = translation found, show it + if (this._text === undefined) { + return nothing; + } + + return this._text !== null + ? unsafeHTML(this._text) + : this.debug + ? html`${this.key}` + : html``; } static override styles = [ From 837f573f6ca1339250e0bead53114124c40081ce Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:44:38 +0100 Subject: [PATCH 4/9] feat: uses null-check to determine if key is set --- .../core/localization/localize.element.ts | 53 +++++-------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts index 42355512eb04..f376919f7aa4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts @@ -1,5 +1,4 @@ -import type { PropertyValues } from '@umbraco-cms/backoffice/external/lit'; -import { css, customElement, html, nothing, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; import { escapeHTML } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -35,51 +34,25 @@ export class UmbLocalizeElement extends UmbLitElement { debug = false; @state() - private _text: string | null | undefined = undefined; + protected get _text(): string | null { + // As translated texts can contain HTML, we will need to render with unsafeHTML. + // But arguments can come from user input, so they should be escaped. + const escapedArgs = (this.args ?? []).map((a) => escapeHTML(a)); - /** - * Computes the localized text when properties change or when the localization controller triggers an update. - * This lifecycle method runs before render and caches the result to avoid repeated computations. - * @param {PropertyValues} changedProperties - The properties that changed since the last update. - */ - protected override willUpdate(changedProperties: PropertyValues): void { - // Update when properties change OR when localization controller triggers update - if (changedProperties.has('key') || changedProperties.has('args') || changedProperties.size === 0) { - // As translated texts can contain HTML, we will need to render with unsafeHTML. - // But arguments can come from user input, so they should be escaped. - const escapedArgs = (this.args ?? []).map((a) => escapeHTML(a)); - - this._text = this.localize.termOrDefault(this.key, null, ...escapedArgs); + const localizedValue = this.localize.termOrDefault(this.key, null, ...escapedArgs); - if (this._text !== null) { - this._text = this._text.trim(); - } + // Update the data attribute based on whether the key was found + if (localizedValue === null) { + (this.getHostElement() as HTMLElement).setAttribute('data-localize-missing', this.key); + return null; } - } - /** - * Updates the data-localize-missing attribute after the element has rendered. - * This attribute is set when the localization key is not found, allowing for debugging and styling. - * @param {PropertyValues} changedProperties - The properties that changed since the last update. - */ - protected override updated(changedProperties: PropertyValues): void { - if (changedProperties.has('_text')) { - if (this._text === null) { - this.setAttribute('data-localize-missing', this.key); - } else { - this.removeAttribute('data-localize-missing'); - } - } + (this.getHostElement() as HTMLElement).removeAttribute('data-localize-missing'); + + return localizedValue.trim(); } override render() { - // undefined = not yet computed (loading), don't show fallback - // null = key not found, show fallback - // string = translation found, show it - if (this._text === undefined) { - return nothing; - } - return this._text !== null ? unsafeHTML(this._text) : this.debug From 0df8e9444abf9823822d17c3992cdc4d4a0d3756 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 08:46:42 +0100 Subject: [PATCH 5/9] chore: accidental rename of variable --- .../src/packages/core/localization/localize.element.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts index f376919f7aa4..0a3911c3a055 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts @@ -34,7 +34,7 @@ export class UmbLocalizeElement extends UmbLitElement { debug = false; @state() - protected get _text(): string | null { + protected get text(): string | null { // As translated texts can contain HTML, we will need to render with unsafeHTML. // But arguments can come from user input, so they should be escaped. const escapedArgs = (this.args ?? []).map((a) => escapeHTML(a)); @@ -53,8 +53,8 @@ export class UmbLocalizeElement extends UmbLitElement { } override render() { - return this._text !== null - ? unsafeHTML(this._text) + return this.text !== null + ? unsafeHTML(this.text) : this.debug ? html`${this.key}` : html``; From da49ad266a85fa0a9e01d43fff39859f7c8e4045 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:11:09 +0100 Subject: [PATCH 6/9] uses `when()` to evaluate --- .../packages/core/localization/localize.element.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts index 0a3911c3a055..fcdd886cf881 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize.element.ts @@ -1,4 +1,4 @@ -import { css, customElement, html, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state, unsafeHTML, when } from '@umbraco-cms/backoffice/external/lit'; import { escapeHTML } from '@umbraco-cms/backoffice/utils'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; @@ -53,11 +53,11 @@ export class UmbLocalizeElement extends UmbLitElement { } override render() { - return this.text !== null - ? unsafeHTML(this.text) - : this.debug - ? html`${this.key}` - : html``; + return when( + this.text, + (text) => unsafeHTML(text), + () => (this.debug ? html`${this.key}` : html``), + ); } static override styles = [ From 630bfd863270765a0fd49ed3175c934add07ad92 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:16:31 +0100 Subject: [PATCH 7/9] revert commits --- .../localization/localize-number.element.ts | 30 +++++----------- .../localize-relative-time.element.ts | 35 +++++-------------- 2 files changed, 16 insertions(+), 49 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-number.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-number.element.ts index d6cc3af8e6eb..125bab27ce2e 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-number.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-number.element.ts @@ -1,5 +1,4 @@ -import type { PropertyValues } from '@umbraco-cms/backoffice/external/lit'; -import { css, customElement, html, nothing, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state, unsafeHTML, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; /** @@ -26,29 +25,16 @@ export class UmbLocalizeNumberElement extends UmbLitElement { options?: Intl.NumberFormatOptions; @state() - private _text: string | null | undefined = undefined; - - /** - * Computes the localized number when properties change or when the localization controller triggers an update. - * This lifecycle method runs before render and caches the result to avoid repeated computations. - * @param {PropertyValues} changedProperties - The properties that changed since the last update. - */ - protected override willUpdate(changedProperties: PropertyValues): void { - // Update when properties change OR when localization controller triggers update - if (changedProperties.has('number') || changedProperties.has('options') || changedProperties.size === 0) { - this._text = this.number ? this.localize.number(this.number, this.options) : null; - } + protected get text(): string | null { + return this.number ? this.localize.number(this.number, this.options) : null; } override render() { - // undefined = not yet computed (loading), don't show fallback - // null = no number provided, show fallback - // string = number formatted, show it - if (this._text === undefined) { - return nothing; - } - - return this._text !== null ? html`${unsafeHTML(this._text)}` : html``; + return when( + this.text, + (text) => unsafeHTML(text), + () => html``, + ); } static override styles = [ diff --git a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts index 0877da0c4f48..ad1aac4e2b4c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/core/localization/localize-relative-time.element.ts @@ -1,5 +1,4 @@ -import type { PropertyValues } from '@umbraco-cms/backoffice/external/lit'; -import { css, customElement, html, nothing, property, state, unsafeHTML } from '@umbraco-cms/backoffice/external/lit'; +import { css, customElement, html, property, state, unsafeHTML, when } from '@umbraco-cms/backoffice/external/lit'; import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element'; /** @@ -34,34 +33,16 @@ export class UmbLocalizeRelativeTimeElement extends UmbLitElement { unit: Intl.RelativeTimeFormatUnit = 'seconds'; @state() - private _text: string | null | undefined = undefined; - - /** - * Computes the localized relative time when properties change or when the localization controller triggers an update. - * This lifecycle method runs before render and caches the result to avoid repeated computations. - * @param {PropertyValues} changedProperties - The properties that changed since the last update. - */ - protected override willUpdate(changedProperties: PropertyValues): void { - // Update when properties change OR when localization controller triggers update - if ( - changedProperties.has('time') || - changedProperties.has('unit') || - changedProperties.has('options') || - changedProperties.size === 0 - ) { - this._text = this.time ? this.localize.relativeTime(this.time, this.unit, this.options) : null; - } + protected get text(): string | null { + return this.time ? this.localize.relativeTime(this.time, this.unit, this.options) : null; } override render() { - // undefined = not yet computed (loading), don't show fallback - // null = no time provided, show fallback - // string = time formatted, show it - if (this._text === undefined) { - return nothing; - } - - return this._text !== null ? html`${unsafeHTML(this._text)}` : html``; + return when( + this.text, + (text) => unsafeHTML(text), + () => html``, + ); } static override styles = [ From ba59ec25ceeab6573861dabd6f10d503935ce37f Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:31:09 +0100 Subject: [PATCH 8/9] fix: improves the fallback mechanism --- .../modal/document-notifications-modal.element.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/notifications/modal/document-notifications-modal.element.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/notifications/modal/document-notifications-modal.element.ts index 061c60669c1a..bf3a6c30fc70 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/notifications/modal/document-notifications-modal.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/entity-actions/notifications/modal/document-notifications-modal.element.ts @@ -82,11 +82,7 @@ export class UmbDocumentNotificationsModalElement extends UmbModalBaseElement< (setting) => setting.actionId, (setting) => { const localizationKey = `actions_${setting.alias}`; - let localization = this.localize.term(localizationKey); - if (localization === localizationKey) { - // Fallback to alias if no localization is found - localization = setting.alias; - } + const localization = this.localize.termOrDefault(localizationKey, setting.alias); return html` this.#updateSubscription(setting.actionId)} From 4a22e5b477a1563759ce48baf20413bc22039beb Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Tue, 25 Nov 2025 09:33:02 +0100 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../src/libs/localization-api/localization.controller.ts | 2 +- .../src/packages/core/localization/localize-number.element.ts | 2 +- .../core/localization/localize-relative-time.element.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts index 08c8b76a6b71..1ed3cf85f0a0 100644 --- a/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts +++ b/src/Umbraco.Web.UI.Client/src/libs/localization-api/localization.controller.ts @@ -158,7 +158,7 @@ export class UmbLocalizationController