Skip to content

Commit 1f29d23

Browse files
authored
feat(framework): introduce the i18n decorator and the cldr option (#9897)
1 parent 65f75eb commit 1f29d23

File tree

94 files changed

+299
-459
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

94 files changed

+299
-459
lines changed

packages/ai/src/PromptInput.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
33
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
44
import event from "@ui5/webcomponents-base/dist/decorators/event.js";
55
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
6+
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
67
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
7-
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
88
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
99
import type ValueState from "@ui5/webcomponents-base/dist/types/ValueState.js";
1010
import "@ui5/webcomponents-icons/dist/paper-plane.js";
@@ -224,12 +224,9 @@ class PromptInput extends UI5Element {
224224
})
225225
valueStateMessage!: Array<HTMLElement>;
226226

227+
@i18n("@ui5/webcomponents-ai")
227228
static i18nBundle: I18nBundle;
228229

229-
static async onDefine() {
230-
PromptInput.i18nBundle = await getI18nBundle("@ui5/webcomponents-ai");
231-
}
232-
233230
_onkeydown(e: KeyboardEvent) {
234231
if (isEnter(e)) {
235232
this.fireEvent("submit");

packages/base/src/Boot.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type OpenUI5Support from "./features/OpenUI5Support.js";
1010
import type F6Navigation from "./features/F6Navigation.js";
1111
import type { PromiseResolve } from "./types.js";
1212
import { attachThemeRegistered } from "./theming/ThemeRegistered.js";
13+
import fixSafariActiveState from "./util/fixSafariActiveState.js";
1314

1415
let booted = false;
1516
let bootPromise: Promise<void>;
@@ -66,6 +67,7 @@ const boot = async (): Promise<void> => {
6667
openUI5Support && openUI5Support.attachListeners();
6768
insertFontFace();
6869
insertSystemCSSVars();
70+
fixSafariActiveState();
6971

7072
resolve();
7173

packages/base/src/FeaturesRegistry.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import type UI5Element from "./UI5Element.js";
44
abstract class ComponentFeature {
55
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-empty-function
66
constructor(...args: any[]) {}
7+
8+
/**
9+
* @deprecated assign the feature's "i18nBundle" static member directly from the component that uses the feature
10+
*/
711
static define?: () => Promise<void>;
812
static dependencies?: Array<typeof UI5Element>
913
}

packages/base/src/UI5Element.ts

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ import type {
3838
import { updateFormValue, setFormValue } from "./features/InputElementsFormSupport.js";
3939
import type { IFormInputElement } from "./features/InputElementsFormSupport.js";
4040
import { getComponentFeature, subscribeForFeatureLoad } from "./FeaturesRegistry.js";
41+
import { getI18nBundle } from "./i18nBundle.js";
42+
import { fetchCldr } from "./asset-registries/LocaleData.js";
43+
import getLocale from "./locale/getLocale.js";
4144

4245
const DEV_MODE = true;
4346
let autoId = 0;
@@ -1207,23 +1210,48 @@ abstract class UI5Element extends HTMLElement {
12071210
* Hook that will be called upon custom element definition
12081211
*
12091212
* @protected
1213+
* @deprecated use the "i18n" decorator for fetching message bundles and the "cldr" option in the "customElements" decorator for fetching CLDR
12101214
*/
12111215
static async onDefine(): Promise<void> {
12121216
return Promise.resolve();
12131217
}
12141218

1219+
static fetchI18nBundles() {
1220+
return Promise.all(Object.entries(this.getMetadata().getI18n()).map(pair => {
1221+
const { bundleName } = pair[1];
1222+
return getI18nBundle(bundleName);
1223+
}));
1224+
}
1225+
1226+
static fetchCLDR() {
1227+
if (this.getMetadata().needsCLDR()) {
1228+
return fetchCldr(getLocale().getLanguage(), getLocale().getRegion(), getLocale().getScript());
1229+
}
1230+
return Promise.resolve();
1231+
}
1232+
12151233
static asyncFinished: boolean;
1216-
static definePromise: Promise<[void, void]> | undefined;
1234+
static definePromise: Promise<void> | undefined;
12171235

12181236
/**
12191237
* Registers a UI5 Web Component in the browser window object
12201238
* @public
12211239
*/
1222-
static async define(): Promise<typeof UI5Element> {
1240+
static define(): typeof UI5Element {
12231241
this.definePromise = Promise.all([
1242+
this.fetchI18nBundles(),
1243+
this.fetchCLDR(),
12241244
boot(),
12251245
this.onDefine(),
1226-
]);
1246+
]).then(result => {
1247+
const [i18nBundles] = result;
1248+
Object.entries(this.getMetadata().getI18n()).forEach((pair, index) => {
1249+
const propertyName = pair[0];
1250+
const targetClass = pair[1].target;
1251+
(targetClass as Record<string, any>)[propertyName] = i18nBundles[index];
1252+
});
1253+
this.asyncFinished = true;
1254+
});
12271255

12281256
const tag = this.getMetadata().getTag();
12291257

@@ -1248,9 +1276,6 @@ abstract class UI5Element extends HTMLElement {
12481276
customElements.define(tag, this as unknown as CustomElementConstructor);
12491277
}
12501278

1251-
await this.definePromise;
1252-
this.asyncFinished = true;
1253-
12541279
return this;
12551280
}
12561281

packages/base/src/UI5ElementMetadata.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { camelToKebabCase, kebabToCamelCase } from "./util/StringHelper.js";
22
import { getSlottedNodes } from "./util/SlotsHelper.js";
33
import { getEffectiveScopingSuffixForTag } from "./CustomElementsScopeUtils.js";
4+
import type UI5Element from "./UI5Element.js";
45

56
type SlotInvalidation = {
67
properties: boolean | Array<string>,
@@ -30,6 +31,13 @@ type PropertyValue = boolean | number | string | object | undefined | null;
3031

3132
type EventData = Record<string, object>;
3233

34+
type I18nBundleAccessorValue = {
35+
bundleName: string,
36+
target: typeof UI5Element
37+
};
38+
39+
type I18nBundleAccessors = Record<string, I18nBundleAccessorValue>;
40+
3341
type Metadata = {
3442
tag?: string,
3543
managedSlots?: boolean,
@@ -39,9 +47,11 @@ type Metadata = {
3947
fastNavigation?: boolean,
4048
themeAware?: boolean,
4149
languageAware?: boolean,
50+
cldr?: boolean,
4251
formAssociated?: boolean,
4352
shadowRootOptions?: Partial<ShadowRootInit>
4453
features?: Array<string>
54+
i18n?: I18nBundleAccessors
4555
};
4656

4757
type State = Record<string, PropertyValue | Array<SlotValue>>;
@@ -236,6 +246,13 @@ class UI5ElementMetadata {
236246
return !!this.metadata.themeAware;
237247
}
238248

249+
/**
250+
* Determines whether this UI5 Element needs CLDR assets to be fetched to work correctly
251+
*/
252+
needsCLDR(): boolean {
253+
return !!this.metadata.cldr;
254+
}
255+
239256
getShadowRootOptions(): Partial<ShadowRootInit> {
240257
return this.metadata.shadowRootOptions || {};
241258
}
@@ -313,6 +330,13 @@ class UI5ElementMetadata {
313330

314331
throw new Error("Wrong format for invalidateOnChildChange: boolean or object is expected");
315332
}
333+
334+
getI18n(): I18nBundleAccessors {
335+
if (!this.metadata.i18n) {
336+
this.metadata.i18n = {};
337+
}
338+
return this.metadata.i18n;
339+
}
316340
}
317341

318342
const validateSingleSlot = (value: Node, slotData: Slot) => {

packages/base/src/decorators/customElement.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const customElement = (tagNameOrComponentSettings: string | {
1717
dependencies?: Array<typeof UI5Element>,
1818
languageAware?: boolean,
1919
themeAware?: boolean,
20+
cldr?: boolean,
2021
fastNavigation?: boolean,
2122
formAssociated?: boolean,
2223
shadowRootOptions?: Partial<ShadowRootInit>,
@@ -36,6 +37,7 @@ const customElement = (tagNameOrComponentSettings: string | {
3637
tag,
3738
languageAware,
3839
themeAware,
40+
cldr,
3941
fastNavigation,
4042
formAssociated,
4143
shadowRootOptions,
@@ -46,6 +48,9 @@ const customElement = (tagNameOrComponentSettings: string | {
4648
if (languageAware) {
4749
target.metadata.languageAware = languageAware;
4850
}
51+
if (cldr) {
52+
target.metadata.cldr = cldr;
53+
}
4954

5055
if (features) {
5156
target.metadata.features = features;

packages/base/src/decorators/i18n.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import type UI5Element from "../UI5Element.js";
2+
3+
type i18nDecorator = (target: typeof UI5Element, propertyName: string) => void;
4+
5+
/**
6+
* A decorator that converts a static class member into an accessor for the i18n bundle with a specified name
7+
*
8+
* @param { string } bundleName name of the i18n bundle to load
9+
* @returns { i18nDecorator }
10+
*
11+
* ```ts
12+
* class MyComponnet extends UI5Element {
13+
* @i18n('@ui5/webcomponents')
14+
* i18nBundle: I18nBundle;
15+
* }
16+
* ```
17+
*/
18+
const i18n = (bundleName: string): i18nDecorator => {
19+
return (target: typeof UI5Element, propertyName: string) => {
20+
if (!target.metadata.i18n) {
21+
target.metadata.i18n = {};
22+
}
23+
target.metadata.i18n[propertyName] = {
24+
bundleName,
25+
target,
26+
};
27+
};
28+
};
29+
30+
export default i18n;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { isIOS, isSafari } from "../Device.js";
2+
3+
let listenerAttached = false;
4+
5+
const fixSafariActiveState = () => {
6+
if (isSafari() && isIOS() && !listenerAttached) {
7+
// Safari on iOS does not use the :active state unless there is a touchstart event handler on the <body> element
8+
document.body.addEventListener("touchstart", () => {});
9+
listenerAttached = true;
10+
}
11+
};
12+
13+
export default fixSafariActiveState;

packages/compat/src/Table.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
44
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
55
import event from "@ui5/webcomponents-base/dist/decorators/event.js";
66
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
7+
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
78
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
89
import ResizeHandler from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
910
import type { ResizeObserverCallback } from "@ui5/webcomponents-base/dist/delegate/ResizeHandler.js";
@@ -30,7 +31,6 @@ import getNormalizedTarget from "@ui5/webcomponents-base/dist/util/getNormalized
3031
import getActiveElement from "@ui5/webcomponents-base/dist/util/getActiveElement.js";
3132
import { getLastTabbableElement, getTabbableElements } from "@ui5/webcomponents-base/dist/util/TabbableElements.js";
3233
import { getEffectiveAriaLabelText } from "@ui5/webcomponents-base/dist/util/AriaLabelHelper.js";
33-
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
3434
import debounce from "@ui5/webcomponents-base/dist/util/debounce.js";
3535
import BusyIndicator from "@ui5/webcomponents/dist/BusyIndicator.js";
3636
import CheckBox from "@ui5/webcomponents/dist/CheckBox.js";
@@ -430,10 +430,7 @@ class Table extends UI5Element {
430430
})
431431
columns!: Array<TableColumn>;
432432

433-
static async onDefine() {
434-
Table.i18nBundle = await getI18nBundle("@ui5/webcomponents");
435-
}
436-
433+
@i18n("@ui5/webcomponents")
437434
static i18nBundle: I18nBundle;
438435

439436
fnHandleF7: (e: CustomEvent) => void;

packages/compat/src/TableCell.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
22
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
33
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
4+
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
45
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
56
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
6-
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
77
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
88
import TableCellTemplate from "./generated/templates/TableCellTemplate.lit.js";
99

@@ -58,10 +58,8 @@ class TableCell extends UI5Element {
5858
@slot({ type: HTMLElement, "default": true })
5959
content?: Array<HTMLElement>;
6060

61+
@i18n("@ui5/webcomponents")
6162
static i18nBundle: I18nBundle;
62-
static async onDefine() {
63-
TableCell.i18nBundle = await getI18nBundle("@ui5/webcomponents");
64-
}
6563

6664
get cellContent(): Array<HTMLElement> {
6765
return this.getSlottedNodes<HTMLElement>("content");

packages/compat/src/TableGroupRow.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import UI5Element from "@ui5/webcomponents-base/dist/UI5Element.js";
22
import customElement from "@ui5/webcomponents-base/dist/decorators/customElement.js";
33
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
44
import event from "@ui5/webcomponents-base/dist/decorators/event.js";
5+
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
56
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
67
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
7-
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
88
import CheckBox from "@ui5/webcomponents/dist/CheckBox.js";
99
import type { ITableRow, TableColumnInfo } from "./Table.js";
1010
import TableGroupRowTemplate from "./generated/templates/TableGroupRowTemplate.lit.js";
@@ -70,6 +70,7 @@ class TableGroupRow extends UI5Element implements ITableRow {
7070
tabbableElements: Array<HTMLElement> = [];
7171
_columnsInfoString = "";
7272

73+
@i18n("@ui5/webcomponents")
7374
static i18nBundle: I18nBundle;
7475

7576
_colSpan?: number;
@@ -104,10 +105,6 @@ class TableGroupRow extends UI5Element implements ITableRow {
104105
_onfocusin(e: FocusEvent) {
105106
this.fireEvent("_focused", e);
106107
}
107-
108-
static async onDefine() {
109-
TableGroupRow.i18nBundle = await getI18nBundle("@ui5/webcomponents");
110-
}
111108
}
112109

113110
TableGroupRow.define();

packages/compat/src/TableRow.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import customElement from "@ui5/webcomponents-base/dist/decorators/customElement
33
import property from "@ui5/webcomponents-base/dist/decorators/property.js";
44
import event from "@ui5/webcomponents-base/dist/decorators/event.js";
55
import slot from "@ui5/webcomponents-base/dist/decorators/slot.js";
6+
import i18n from "@ui5/webcomponents-base/dist/decorators/i18n.js";
67
import type I18nBundle from "@ui5/webcomponents-base/dist/i18nBundle.js";
78
import type { PassiveEventListenerObject } from "@ui5/webcomponents-base/dist/types.js";
8-
import { getI18nBundle } from "@ui5/webcomponents-base/dist/i18nBundle.js";
99
import litRender from "@ui5/webcomponents-base/dist/renderer/LitRenderer.js";
1010
import {
1111
isSpace,
@@ -162,6 +162,7 @@ class TableRow extends UI5Element implements ITableRow {
162162
@slot({ type: HTMLElement, "default": true, individualSlots: true })
163163
cells!: Array<TableCell>;
164164

165+
@i18n("@ui5/webcomponents")
165166
static i18nBundle: I18nBundle;
166167

167168
visibleCells: Array<TableCell> = [];
@@ -431,10 +432,6 @@ class TableRow extends UI5Element implements ITableRow {
431432
getNormilzedTextContent(textContent: string): string {
432433
return textContent.replace(/[\n\r\t]/g, "").trim();
433434
}
434-
435-
static async onDefine() {
436-
TableRow.i18nBundle = await getI18nBundle("@ui5/webcomponents");
437-
}
438435
}
439436

440437
TableRow.define();

0 commit comments

Comments
 (0)