@@ -20,6 +20,7 @@ import type {
20
20
import { umbLocalizationManager } from './localization.manager.js' ;
21
21
import type { LitElement } from '@umbraco-cms/backoffice/external/lit' ;
22
22
import type { UmbController , UmbControllerHost } from '@umbraco-cms/backoffice/controller-api' ;
23
+ import { escapeHTML } from '@umbraco-cms/backoffice/utils' ;
23
24
24
25
const LocalizationControllerAlias = Symbol ( ) ;
25
26
/**
@@ -109,38 +110,45 @@ export class UmbLocalizationController<LocalizationSetType extends UmbLocalizati
109
110
110
111
/**
111
112
* Outputs a translated term.
112
- * @param key
113
- * @param {...any } args
113
+ * @param {string } key - the localization key, the indicator of what localization entry you want to retrieve.
114
+ * @param {...any } args - the arguments to parse for this localization entry.
115
+ * @returns {string } - the translated term as a string.
114
116
*/
115
117
term < K extends keyof LocalizationSetType > ( key : K , ...args : FunctionParams < LocalizationSetType [ K ] > ) : string {
116
118
if ( ! this . #usedKeys. includes ( key ) ) {
117
119
this . #usedKeys. push ( key ) ;
118
120
}
119
121
120
122
const { primary, secondary } = this . getLocalizationData ( this . lang ( ) ) ;
123
+
124
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
121
125
let term : any ;
122
126
123
127
// Look for a matching term using regionCode, code, then the fallback
124
- if ( primary && primary [ key ] ) {
128
+ if ( primary ?. [ key ] ) {
125
129
term = primary [ key ] ;
126
- } else if ( secondary && secondary [ key ] ) {
130
+ } else if ( secondary ?. [ key ] ) {
127
131
term = secondary [ key ] ;
128
- } else if ( umbLocalizationManager . fallback && umbLocalizationManager . fallback [ key ] ) {
132
+ } else if ( umbLocalizationManager . fallback ?. [ key ] ) {
129
133
term = umbLocalizationManager . fallback [ key ] ;
130
134
} else {
131
135
return String ( key ) ;
132
136
}
133
137
138
+ // As translated texts can contain HTML, we will need to render with unsafeHTML.
139
+ // But arguments can come from user input, so they should be escaped.
140
+ const sanitizedArgs = args . map ( ( a ) => escapeHTML ( a ) ) ;
141
+
134
142
if ( typeof term === 'function' ) {
135
- return term ( ...args ) as string ;
143
+ return term ( ...sanitizedArgs ) as string ;
136
144
}
137
145
138
146
if ( typeof term === 'string' ) {
139
- if ( args . length > 0 ) {
147
+ if ( sanitizedArgs . length ) {
140
148
// Replace placeholders of format "%index%" and "{index}" with provided values
141
149
term = term . replace ( / ( % ( \d + ) % | \{ ( \d + ) \} ) / g, ( match , _p1 , p2 , p3 ) : string => {
142
150
const index = p2 || p3 ;
143
- return String ( args [ index ] || match ) ;
151
+ return typeof sanitizedArgs [ index ] !== 'undefined' ? String ( sanitizedArgs [ index ] ) : match ;
144
152
} ) ;
145
153
}
146
154
}
@@ -178,7 +186,18 @@ export class UmbLocalizationController<LocalizationSetType extends UmbLocalizati
178
186
return new Intl . RelativeTimeFormat ( this . lang ( ) , options ) . format ( value , unit ) ;
179
187
}
180
188
181
- string ( text : string ) : string {
189
+ /**
190
+ * Translates a string containing one or more terms. The terms should be prefixed with a `#` character.
191
+ * If the term is found in the localization set, it will be replaced with the localized term.
192
+ * If the term is not found, the original term will be returned.
193
+ * @param {string } text The text to translate.
194
+ * @returns {string } The translated text.
195
+ */
196
+ string ( text : unknown ) : string {
197
+ if ( typeof text !== 'string' ) {
198
+ return '' ;
199
+ }
200
+
182
201
// find all words starting with #
183
202
const regex = / # \w + / g;
184
203
0 commit comments