@wordpress/i18n 6.0.0 → 6.0.1-next.46f643fa0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +20 -19
  2. package/build/create-i18n.js +60 -205
  3. package/build/create-i18n.js.map +1 -1
  4. package/build/default-i18n.js +31 -34
  5. package/build/default-i18n.js.map +1 -1
  6. package/build/index.js.map +1 -1
  7. package/build/sprintf.js +3 -4
  8. package/build/sprintf.js.map +1 -1
  9. package/build/types.js.map +1 -1
  10. package/build-module/create-i18n.js +60 -205
  11. package/build-module/create-i18n.js.map +1 -1
  12. package/build-module/default-i18n.js +31 -35
  13. package/build-module/default-i18n.js.map +1 -1
  14. package/build-module/index.js.map +1 -1
  15. package/build-module/sprintf.js +3 -4
  16. package/build-module/sprintf.js.map +1 -1
  17. package/build-module/types.js.map +1 -1
  18. package/build-types/create-i18n.d.ts +11 -115
  19. package/build-types/create-i18n.d.ts.map +1 -1
  20. package/build-types/default-i18n.d.ts +45 -47
  21. package/build-types/default-i18n.d.ts.map +1 -1
  22. package/build-types/index.d.ts +4 -3
  23. package/build-types/index.d.ts.map +1 -1
  24. package/build-types/sprintf.d.ts +1 -4
  25. package/build-types/sprintf.d.ts.map +1 -1
  26. package/build-types/types.d.ts +101 -0
  27. package/build-types/types.d.ts.map +1 -1
  28. package/package.json +3 -3
  29. package/src/create-i18n.ts +405 -0
  30. package/src/{default-i18n.js → default-i18n.ts} +37 -35
  31. package/src/index.ts +16 -0
  32. package/src/sprintf.ts +4 -10
  33. package/src/test/{create-i18n.js → create-i18n.ts} +63 -30
  34. package/src/test/{sprintf.js → sprintf.ts} +5 -1
  35. package/src/test/{subscribe-i18n.js → subscribe-i18n.ts} +3 -3
  36. package/src/types.ts +155 -0
  37. package/tsconfig.tsbuildinfo +1 -1
  38. package/src/create-i18n.js +0 -514
  39. /package/src/test/{default-i18n.js → default-i18n.ts} +0 -0
@@ -0,0 +1,405 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import type { TanninLocaleDomain } from 'tannin';
5
+ import Tannin from 'tannin';
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import type {
10
+ getFilterDomain,
11
+ I18n,
12
+ LocaleData,
13
+ SubscribeCallback,
14
+ TranslatableText,
15
+ UnsubscribeCallback,
16
+ } from './types';
17
+ /**
18
+ * WordPress dependencies
19
+ */
20
+ import type { Hooks } from '@wordpress/hooks';
21
+ /**
22
+ * Default locale data to use for Tannin domain when not otherwise provided.
23
+ * Assumes an English plural forms expression.
24
+ */
25
+ const DEFAULT_LOCALE_DATA: LocaleData = {
26
+ '': {
27
+ plural_forms( n: number ) {
28
+ return n === 1 ? 0 : 1;
29
+ },
30
+ },
31
+ };
32
+
33
+ /*
34
+ * Regular expression that matches i18n hooks like `i18n.gettext`, `i18n.ngettext`,
35
+ * `i18n.gettext_domain` or `i18n.ngettext_with_context` or `i18n.has_translation`.
36
+ */
37
+ const I18N_HOOK_REGEXP = /^i18n\.(n?gettext|has_translation)(_|$)/;
38
+
39
+ /**
40
+ * Create an i18n instance
41
+ *
42
+ * @param [initialData] Locale data configuration.
43
+ * @param [initialDomain] Domain for which configuration applies.
44
+ * @param [hooks] Hooks implementation.
45
+ *
46
+ * @return I18n instance.
47
+ */
48
+ export const createI18n = < TextDomain extends string >(
49
+ initialData?: LocaleData< TextDomain >,
50
+ initialDomain?: TextDomain,
51
+ hooks?: Hooks
52
+ ): I18n< TextDomain > => {
53
+ /**
54
+ * The underlying instance of Tannin to which exported functions interface.
55
+ */
56
+ const tannin = new Tannin( {} );
57
+
58
+ const listeners = new Set< () => void >();
59
+
60
+ const notifyListeners = () => {
61
+ listeners.forEach( ( listener ) => listener() );
62
+ };
63
+
64
+ /**
65
+ * Subscribe to changes of locale data.
66
+ *
67
+ * @param callback Subscription callback.
68
+ * @return Unsubscribe callback.
69
+ */
70
+ const subscribe = ( callback: SubscribeCallback ): UnsubscribeCallback => {
71
+ listeners.add( callback );
72
+ return () => listeners.delete( callback );
73
+ };
74
+
75
+ const getLocaleData: I18n< TextDomain >[ 'getLocaleData' ] = (
76
+ domain = 'default' as TextDomain
77
+ ) => tannin.data[ domain ] as LocaleData< TextDomain >;
78
+
79
+ /**
80
+ * @param [data]
81
+ * @param [domain]
82
+ */
83
+ const doSetLocaleData = (
84
+ data?: LocaleData,
85
+ domain: TextDomain = 'default' as TextDomain
86
+ ) => {
87
+ tannin.data[ domain ] = {
88
+ ...tannin.data[ domain ],
89
+ ...data,
90
+ } as TanninLocaleDomain;
91
+
92
+ // Populate default domain configuration (supported locale date which omits
93
+ // a plural forms expression).
94
+ tannin.data[ domain ][ '' ] = {
95
+ ...DEFAULT_LOCALE_DATA[ '' ],
96
+ ...tannin.data[ domain ]?.[ '' ],
97
+ };
98
+
99
+ // Clean up cached plural forms functions cache as it might be updated.
100
+ delete tannin.pluralForms[ domain ];
101
+ };
102
+
103
+ const setLocaleData: I18n< TextDomain >[ 'setLocaleData' ] = (
104
+ data,
105
+ domain
106
+ ) => {
107
+ doSetLocaleData( data, domain );
108
+ notifyListeners();
109
+ };
110
+
111
+ const addLocaleData: I18n< TextDomain >[ 'addLocaleData' ] = (
112
+ data,
113
+ domain = 'default' as TextDomain
114
+ ) => {
115
+ tannin.data[ domain ] = {
116
+ ...tannin.data[ domain ],
117
+ ...data,
118
+ // Populate default domain configuration (supported locale date which omits
119
+ // a plural forms expression).
120
+ '': {
121
+ ...DEFAULT_LOCALE_DATA[ '' ],
122
+ ...tannin.data[ domain ]?.[ '' ],
123
+ ...data?.[ '' ],
124
+ },
125
+ } as TanninLocaleDomain;
126
+
127
+ // Clean up cached plural forms functions cache as it might be updated.
128
+ delete tannin.pluralForms[ domain ];
129
+
130
+ notifyListeners();
131
+ };
132
+
133
+ const resetLocaleData: I18n< TextDomain >[ 'resetLocaleData' ] = (
134
+ data,
135
+ domain
136
+ ) => {
137
+ // Reset all current Tannin locale data.
138
+ tannin.data = {};
139
+
140
+ // Reset cached plural forms functions cache.
141
+ tannin.pluralForms = {};
142
+
143
+ setLocaleData( data, domain );
144
+ };
145
+
146
+ /**
147
+ * Wrapper for Tannin's `dcnpgettext`. Populates default locale data if not
148
+ * otherwise previously assigned.
149
+ *
150
+ * @param domain Domain to retrieve the translated text.
151
+ * @param context Context information for the translators.
152
+ * @param single Text to translate if non-plural. Used as
153
+ * fallback return value on a caught error.
154
+ * @param [plural] The text to be used if the number is
155
+ * plural.
156
+ * @param [number] The number to compare against to use
157
+ * either the singular or plural form.
158
+ *
159
+ * @return The translated string.
160
+ */
161
+ const dcnpgettext = (
162
+ domain = 'default' as TextDomain,
163
+ context: string | void,
164
+ single: string,
165
+ plural?: string,
166
+ number?: number
167
+ ): string => {
168
+ if ( ! tannin.data[ domain ] ) {
169
+ // Use `doSetLocaleData` to set silently, without notifying listeners.
170
+ doSetLocaleData( undefined, domain );
171
+ }
172
+
173
+ return tannin.dcnpgettext( domain, context, single, plural, number );
174
+ };
175
+
176
+ const getFilterDomain: getFilterDomain = ( domain ) => domain || 'default';
177
+
178
+ const __: I18n< TextDomain >[ '__' ] = ( text, domain ) => {
179
+ let translation = dcnpgettext( domain, undefined, text );
180
+ if ( ! hooks ) {
181
+ return translation as TranslatableText< typeof text >;
182
+ }
183
+
184
+ /**
185
+ * Filters text with its translation.
186
+ *
187
+ * @param translation Translated text.
188
+ * @param text Text to translate.
189
+ * @param domain Text domain. Unique identifier for retrieving translated strings.
190
+ */
191
+ translation = hooks.applyFilters(
192
+ 'i18n.gettext',
193
+ translation,
194
+ text,
195
+ domain
196
+ ) as TranslatableText< typeof text >;
197
+
198
+ return hooks.applyFilters(
199
+ 'i18n.gettext_' + getFilterDomain( domain ),
200
+ translation,
201
+ text,
202
+ domain
203
+ ) as TranslatableText< typeof text >;
204
+ };
205
+
206
+ const _x: I18n< TextDomain >[ '_x' ] = ( text, context, domain ) => {
207
+ let translation = dcnpgettext( domain, context, text );
208
+ if ( ! hooks ) {
209
+ return translation as TranslatableText< typeof text >;
210
+ }
211
+
212
+ /**
213
+ * Filters text with its translation based on context information.
214
+ *
215
+ * @param translation Translated text.
216
+ * @param text Text to translate.
217
+ * @param context Context information for the translators.
218
+ * @param domain Text domain. Unique identifier for retrieving translated strings.
219
+ */
220
+ translation = hooks.applyFilters(
221
+ 'i18n.gettext_with_context',
222
+ translation,
223
+ text,
224
+ context,
225
+ domain
226
+ ) as TranslatableText< typeof text >;
227
+
228
+ return hooks.applyFilters(
229
+ 'i18n.gettext_with_context_' + getFilterDomain( domain ),
230
+ translation,
231
+ text,
232
+ context,
233
+ domain
234
+ ) as TranslatableText< typeof text >;
235
+ };
236
+
237
+ const _n: I18n< TextDomain >[ '_n' ] = (
238
+ single,
239
+ plural,
240
+ number,
241
+ domain
242
+ ) => {
243
+ let translation = dcnpgettext(
244
+ domain,
245
+ undefined,
246
+ single,
247
+ plural,
248
+ number
249
+ );
250
+ if ( ! hooks ) {
251
+ return translation as TranslatableText<
252
+ typeof single | typeof plural
253
+ >;
254
+ }
255
+
256
+ /**
257
+ * Filters the singular or plural form of a string.
258
+ *
259
+ * @param translation Translated text.
260
+ * @param single The text to be used if the number is singular.
261
+ * @param plural The text to be used if the number is plural.
262
+ * @param number The number to compare against to use either the singular or plural form.
263
+ * @param domain Text domain. Unique identifier for retrieving translated strings.
264
+ */
265
+ translation = hooks.applyFilters(
266
+ 'i18n.ngettext',
267
+ translation,
268
+ single,
269
+ plural,
270
+ number,
271
+ domain
272
+ ) as TranslatableText< typeof single | typeof plural >;
273
+
274
+ return hooks.applyFilters(
275
+ 'i18n.ngettext_' + getFilterDomain( domain ),
276
+ translation,
277
+ single,
278
+ plural,
279
+ number,
280
+ domain
281
+ ) as TranslatableText< typeof single | typeof plural >;
282
+ };
283
+
284
+ const _nx: I18n< TextDomain >[ '_nx' ] = (
285
+ single,
286
+ plural,
287
+ number,
288
+ context,
289
+ domain
290
+ ) => {
291
+ let translation = dcnpgettext(
292
+ domain,
293
+ context,
294
+ single,
295
+ plural,
296
+ number
297
+ );
298
+ if ( ! hooks ) {
299
+ return translation as TranslatableText<
300
+ typeof single | typeof plural
301
+ >;
302
+ }
303
+
304
+ /**
305
+ * Filters the singular or plural form of a string with gettext context.
306
+ *
307
+ * @param translation Translated text.
308
+ * @param single The text to be used if the number is singular.
309
+ * @param plural The text to be used if the number is plural.
310
+ * @param number The number to compare against to use either the singular or plural form.
311
+ * @param context Context information for the translators.
312
+ * @param domain Text domain. Unique identifier for retrieving translated strings.
313
+ */
314
+ translation = hooks.applyFilters(
315
+ 'i18n.ngettext_with_context',
316
+ translation,
317
+ single,
318
+ plural,
319
+ number,
320
+ context,
321
+ domain
322
+ ) as TranslatableText< typeof single | typeof plural >;
323
+
324
+ return hooks.applyFilters(
325
+ 'i18n.ngettext_with_context_' + getFilterDomain( domain ),
326
+ translation,
327
+ single,
328
+ plural,
329
+ number,
330
+ context,
331
+ domain
332
+ ) as TranslatableText< typeof single | typeof plural >;
333
+ };
334
+
335
+ const isRTL: I18n< TextDomain >[ 'isRTL' ] = () => {
336
+ return 'rtl' === _x( 'ltr', 'text direction' );
337
+ };
338
+
339
+ const hasTranslation: I18n< TextDomain >[ 'hasTranslation' ] = (
340
+ single,
341
+ context,
342
+ domain
343
+ ) => {
344
+ const key = context ? context + '\u0004' + single : single;
345
+ let result = !! tannin.data?.[ domain ?? 'default' ]?.[ key ];
346
+ if ( hooks ) {
347
+ /**
348
+ * Filters the presence of a translation in the locale data.
349
+ *
350
+ * @param hasTranslation Whether the translation is present or not..
351
+ * @param single The singular form of the translated text (used as key in locale data)
352
+ * @param context Context information for the translators.
353
+ * @param domain Text domain. Unique identifier for retrieving translated strings.
354
+ */
355
+ result = hooks.applyFilters(
356
+ 'i18n.has_translation',
357
+ result,
358
+ single,
359
+ context,
360
+ domain
361
+ ) as boolean;
362
+
363
+ result = hooks.applyFilters(
364
+ 'i18n.has_translation_' + getFilterDomain( domain ),
365
+ result,
366
+ single,
367
+ context,
368
+ domain
369
+ ) as boolean;
370
+ }
371
+ return result;
372
+ };
373
+
374
+ if ( initialData ) {
375
+ setLocaleData( initialData, initialDomain );
376
+ }
377
+
378
+ if ( hooks ) {
379
+ /**
380
+ * @param hookName
381
+ */
382
+ const onHookAddedOrRemoved = ( hookName: string ) => {
383
+ if ( I18N_HOOK_REGEXP.test( hookName ) ) {
384
+ notifyListeners();
385
+ }
386
+ };
387
+
388
+ hooks.addAction( 'hookAdded', 'core/i18n', onHookAddedOrRemoved );
389
+ hooks.addAction( 'hookRemoved', 'core/i18n', onHookAddedOrRemoved );
390
+ }
391
+
392
+ return {
393
+ getLocaleData,
394
+ setLocaleData,
395
+ addLocaleData,
396
+ resetLocaleData,
397
+ subscribe,
398
+ __,
399
+ _x,
400
+ _n,
401
+ _nx,
402
+ isRTL,
403
+ hasTranslation,
404
+ };
405
+ };
@@ -7,6 +7,12 @@ import { createI18n } from './create-i18n';
7
7
  * WordPress dependencies
8
8
  */
9
9
  import { defaultHooks } from '@wordpress/hooks';
10
+ import {
11
+ LocaleData,
12
+ SubscribeCallback,
13
+ TranslatableText,
14
+ UnsubscribeCallback,
15
+ } from './types';
10
16
 
11
17
  const i18n = createI18n( undefined, undefined, defaultHooks );
12
18
 
@@ -20,19 +26,13 @@ export default i18n;
20
26
  * https://github.com/WordPress/gutenberg/pull/20318#issuecomment-590837722
21
27
  */
22
28
 
23
- /**
24
- * @typedef {import('./create-i18n').LocaleData} LocaleData
25
- * @typedef {import('./create-i18n').SubscribeCallback} SubscribeCallback
26
- * @typedef {import('./create-i18n').UnsubscribeCallback} UnsubscribeCallback
27
- */
28
-
29
29
  /**
30
30
  * Returns locale data by domain in a Jed-formatted JSON object shape.
31
31
  *
32
32
  * @see http://messageformat.github.io/Jed/
33
33
  *
34
- * @param {string} [domain] Domain for which to get the data.
35
- * @return {LocaleData} Locale data.
34
+ * @param { string | undefined } [domain] Domain for which to get the data.
35
+ * @return { LocaleData } Locale data.
36
36
  */
37
37
  export const getLocaleData = i18n.getLocaleData.bind( i18n );
38
38
 
@@ -42,8 +42,8 @@ export const getLocaleData = i18n.getLocaleData.bind( i18n );
42
42
  *
43
43
  * @see http://messageformat.github.io/Jed/
44
44
  *
45
- * @param {LocaleData} [data] Locale data configuration.
46
- * @param {string} [domain] Domain for which configuration applies.
45
+ * @param {LocaleData } [data] Locale data configuration.
46
+ * @param {string | undefined} [domain] Domain for which configuration applies.
47
47
  */
48
48
  export const setLocaleData = i18n.setLocaleData.bind( i18n );
49
49
 
@@ -53,8 +53,8 @@ export const setLocaleData = i18n.setLocaleData.bind( i18n );
53
53
  *
54
54
  * @see http://messageformat.github.io/Jed/
55
55
  *
56
- * @param {LocaleData} [data] Locale data configuration.
57
- * @param {string} [domain] Domain for which configuration applies.
56
+ * @param {LocaleData} [data] Locale data configuration.
57
+ * @param {string | undefined} [domain] Domain for which configuration applies.
58
58
  */
59
59
  export const resetLocaleData = i18n.resetLocaleData.bind( i18n );
60
60
 
@@ -73,10 +73,10 @@ export const subscribe = i18n.subscribe.bind( i18n );
73
73
  *
74
74
  * @template {string} Text
75
75
  *
76
- * @param {Text} text Text to translate.
77
- * @param {string} [domain] Domain to retrieve the translated text.
76
+ * @param {Text} text Text to translate.
77
+ * @param {string | undefined} domain Domain to retrieve the translated text.
78
78
  *
79
- * @return {import('./types').TranslatableText< Text >} Translated text.
79
+ * @return {TranslatableText<Text>} Translated text.
80
80
  */
81
81
  export const __ = i18n.__.bind( i18n );
82
82
 
@@ -87,11 +87,11 @@ export const __ = i18n.__.bind( i18n );
87
87
  *
88
88
  * @template {string} Text
89
89
  *
90
- * @param {Text} text Text to translate.
91
- * @param {string} context Context information for the translators.
92
- * @param {string} [domain] Domain to retrieve the translated text.
90
+ * @param {Text} text Text to translate.
91
+ * @param {string} context Context information for the translators.
92
+ * @param {string | undefined} domain Domain to retrieve the translated text.
93
93
  *
94
- * @return {import('./types').TranslatableText< Text >} Translated context string without pipe.
94
+ * @return {TranslatableText<Text>} Translated context string without pipe.
95
95
  */
96
96
  export const _x = i18n._x.bind( i18n );
97
97
 
@@ -104,13 +104,13 @@ export const _x = i18n._x.bind( i18n );
104
104
  * @template {string} Single
105
105
  * @template {string} Plural
106
106
  *
107
- * @param {Single} single The text to be used if the number is singular.
108
- * @param {Plural} plural The text to be used if the number is plural.
109
- * @param {number} number The number to compare against to use either the
110
- * singular or plural form.
111
- * @param {string} [domain] Domain to retrieve the translated text.
107
+ * @param {Single} single The text to be used if the number is singular.
108
+ * @param {Plural} plural The text to be used if the number is plural.
109
+ * @param {number} number The number to compare against to use either the
110
+ * singular or plural form.
111
+ * @param {string | undefined} domain Domain to retrieve the translated text.
112
112
  *
113
- * @return {import('./types').TranslatableText< Single | Plural >} The translated singular or plural form.
113
+ * @return {TranslatableText<Single | Plural>} The translated singular or plural form.
114
114
  */
115
115
  export const _n = i18n._n.bind( i18n );
116
116
 
@@ -122,15 +122,16 @@ export const _n = i18n._n.bind( i18n );
122
122
  *
123
123
  * @template {string} Single
124
124
  * @template {string} Plural
125
+ * @param {Single} single The text to be used if the number is singular.
125
126
  *
126
- * @param {Single} single The text to be used if the number is singular.
127
- * @param {Plural} plural The text to be used if the number is plural.
128
- * @param {number} number The number to compare against to use either the
129
- * singular or plural form.
130
- * @param {string} context Context information for the translators.
131
- * @param {string} [domain] Domain to retrieve the translated text.
127
+ * @param {Single} single The text to be used if the number is singular.
128
+ * @param {Plural} plural The text to be used if the number is plural.
129
+ * @param {number} number The number to compare against to use either the
130
+ * singular or plural form.
131
+ * @param {string} context Context information for the translators.
132
+ * @param {string | undefined} [domain] Domain to retrieve the translated text.
132
133
  *
133
- * @return {import('./types').TranslatableText< Single | Plural >} The translated singular or plural form.
134
+ * @return {TranslatableText<Single | Plural>} The translated singular or plural form.
134
135
  */
135
136
  export const _nx = i18n._nx.bind( i18n );
136
137
 
@@ -149,9 +150,10 @@ export const isRTL = i18n.isRTL.bind( i18n );
149
150
  /**
150
151
  * Check if there is a translation for a given string (in singular form).
151
152
  *
152
- * @param {string} single Singular form of the string to look up.
153
- * @param {string} [context] Context information for the translators.
154
- * @param {string} [domain] Domain to retrieve the translated text.
153
+ * @param {string} single Singular form of the string to look up.
154
+ * @param {string} context Context information for the translators.
155
+ * @param {string} domain Domain to retrieve the translated text.
156
+ *
155
157
  * @return {boolean} Whether the translation exists or not.
156
158
  */
157
159
  export const hasTranslation = i18n.hasTranslation.bind( i18n );
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ export { sprintf } from './sprintf';
2
+ export * from './create-i18n';
3
+ export type * from './types';
4
+ export {
5
+ default as defaultI18n,
6
+ setLocaleData,
7
+ resetLocaleData,
8
+ getLocaleData,
9
+ subscribe,
10
+ __,
11
+ _x,
12
+ _n,
13
+ _nx,
14
+ isRTL,
15
+ hasTranslation,
16
+ } from './default-i18n';
package/src/sprintf.ts CHANGED
@@ -4,16 +4,11 @@
4
4
  // Disable reason: `eslint-plugin-import` doesn't support `exports` (https://github.com/import-js/eslint-plugin-import/issues/1810)
5
5
  // eslint-disable-next-line import/no-unresolved
6
6
  import _sprintf from '@tannin/sprintf';
7
- import type { SprintfArgs } from '@tannin/sprintf/types';
8
7
 
9
8
  /**
10
9
  * Internal dependencies
11
10
  */
12
- import type { TranslatableText } from './types';
13
-
14
- type DistributeSprintfArgs< T extends string > = T extends any
15
- ? SprintfArgs< T >
16
- : never;
11
+ import type { DistributeSprintfArgs, TranslatableText } from './types';
17
12
 
18
13
  export function sprintf< T extends string >(
19
14
  format: T | TranslatableText< T >,
@@ -27,13 +22,12 @@ export function sprintf< T extends string >(
27
22
  /**
28
23
  * Returns a formatted string.
29
24
  *
30
- * @template {string} T
31
- * @param {T | TranslatableText<T>} format The format of the string to generate.
32
- * @param {DistributeSprintfArgs<T>} args Arguments to apply to the format.
25
+ * @param format The format of the string to generate.
26
+ * @param args Arguments to apply to the format.
33
27
  *
34
28
  * @see https://www.npmjs.com/package/@tannin/sprintf
35
29
  *
36
- * @return {string} The formatted string.
30
+ * @return The formatted string.
37
31
  */
38
32
  export function sprintf< T extends string >(
39
33
  format: T | TranslatableText< T >,