Skip to content

Commit 7b38975

Browse files
committed
feat(useI18nBundle): add new hook to retrieve current i18n bundle (#1104)
* chore(useI18nBundle): add useI18nBundle * deprecate old hook, update docs & add migration guide * add test for i18n hook * early exit test when running in react 16.8.0
1 parent 188e1a6 commit 7b38975

File tree

18 files changed

+256
-132
lines changed

18 files changed

+256
-132
lines changed

docs/2-MigrationGuide.stories.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,36 @@ or the [changelog](https://github.com/SAP/ui5-webcomponents-react/blob/master/CH
1414

1515
## Table of Contents
1616

17+
- [0.13.x to 0.14.0](#migrating-from-013x-to-0140)
1718
- [0.12.x to 0.13.0](#migrating-from-012x-to-0130)
1819
- [0.11.x to 0.12.0](#migrating-from-011x-to-0120)
1920
- [0.10.x to 0.11.0](#migrating-from-010x-to-0110)
2021
- [0.9.x to 0.10.0](#migrating-from-09x-to-0100)
2122
- [0.8.x to 0.9.0](#migrating-from-08x-to-090)
2223

24+
## Migrating from 0.13.x to 0.14.0
25+
26+
<br />
27+
28+
### Deleted Hooks
29+
30+
The `useI18nText` hook has been removed and replaced by the `useI18nBundle` hook. <br />
31+
Migration Path:
32+
33+
```js
34+
import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks';
35+
// ...
36+
const [pleaseWaitText, withParameters] = useI18nText('myApp', 'PLEASE_WAIT', ['CAROUSEL_DOT_TEXT', 5, 20]);
37+
38+
// becomes
39+
40+
import { useI18nBundle } from '@ui5/webcomponents-react-base/lib/hooks';
41+
// ...
42+
const i18nBundle = useI18nBundle('myApp');
43+
const pleaseWaitText = i18nBundle.getText('PLEASE_WAIT');
44+
const withParameters = i18nBundle.getText('CAROUSEL_DOT_TEXT', 5, 20);
45+
```
46+
2347
## Migrating from 0.12.x to 0.13.0
2448

2549
<br />

docs/5-Internationalization.stories.mdx

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -67,43 +67,39 @@ _Note:_ This is just asset registration, no data will be fetched at that point.
6767
Add the following import statement to the component where you want to use translated texts:
6868

6969
```js
70-
import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks';
70+
import { useI18nBundle } from '@ui5/webcomponents-react-base/lib/hooks';
7171
```
7272

73-
Now, you can use the `useI18nText` hook in your functional components in order to get your translated texts.
73+
Now, you can use the `useI18nBundle` hook in your functional components in order to access the i18nBundle and get your
74+
translated texts from there.
7475

7576
The hook has the following signature:
7677

77-
```js
78-
const translatedTextsArray = useI18nText((messageBundleId: string), (...textsToTranslate: (string | string[])[]));
78+
```ts
79+
const i18nBundle = useI18nBundle(messageBundleId: string): I18nBundle;
7980
```
8081

81-
In case you have texts without placeholder values you can use the simple `string` arguments, but if you have parameters
82-
in your message bundle you will have to use the `array` notation (_please see step 6_).
83-
You can mix `string` and `array` arguments in the same call.
84-
85-
Each parameter will be translated one by one and returned in an array in the same order.
82+
The `i18nBundle` is offering one public method `getText` which accepts a string as first parameter, followed by optional replacement parameters for your translation.
8683

8784
**Example:**
8885

8986
```jsx
9087
const MyTranslatedTextComponent = () => {
91-
const [pleaseWaitText, anotherText] = useI18nText('myApp', 'PLEASE_WAIT', 'ANOTHER_TEXT_TO_TRANSLATE');
88+
const i18nBundle = useI18nBundle('myApp');
9289

9390
return (
9491
<div>
95-
<span>{pleaseWaitText}</span>
96-
<p>{anotherText}</p>
92+
<span>{i18nBundle.getText('PLEASE_WAIT')}</span>
93+
<p>{i18nBundle.getText('ANOTHER_TEXT_TO_TRANSLATE')}</p>
9794
</div>
9895
);
9996
};
10097
```
10198

10299
**5. Use texts with placeholder values**
103100

104-
In case you have texts with placeholders in your message bundle, you can pass an array as text parameter to receive
105-
translated text with parameters. In this case, the first entry in the array is the translation key, followed by an
106-
arbitrary number of parameters which should be inserted into the translation.
101+
You can pass multiple additional values to `getText` for texts with placeholders.
102+
In this case, the first parameter is the translation key, followed by an arbitrary number of parameters which should be inserted into the translation.
107103

108104
**Example:**
109105

@@ -116,7 +112,8 @@ CAROUSEL_DOT_TEXT=Item {0} of {1} displayed
116112
Your hook call would now look like this:
117113

118114
```js
119-
const [carouselText, pleaseWaitText] = useI18nText('myApp', ['CAROUSEL_DOT_TEXT', 5, 20], 'PLEASE_WAIT');
115+
const i18nBundle = useI18nBundle('myApp');
116+
const carouselText = i18nBundle.getText('CAROUSEL_DOT_TEXT', 5, 20);
120117
```
121118

122119
This would resolve this text:<br />

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"@storybook/storybook-deployer": "^2.8.6",
5858
"@testing-library/jest-dom": "^5.11.4",
5959
"@testing-library/react": "^11.0.2",
60+
"@testing-library/react-hooks": "^3.7.0",
6061
"@types/jest": "^26.0.8",
6162
"@types/react": "^17.0.0",
6263
"@types/react-dom": "^17.0.0",
@@ -88,6 +89,7 @@
8889
"npm-run-all": "^4.1.5",
8990
"prettier": "^2.0.4",
9091
"react-app-polyfill": "^2.0.0",
92+
"react-test-renderer": "17.0.1",
9193
"rimraf": "^3.0.1",
9294
"rollup": "^2.23.0",
9395
"rollup-plugin-terser": "^7.0.2",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { act, renderHook } from '@testing-library/react-hooks';
2+
import { useI18nBundle } from './useI18nBundle';
3+
import { setLanguage } from '@ui5/webcomponents-base/dist/config/Language.js';
4+
import { version as reactVersion } from 'react/package.json';
5+
6+
describe('useI18nBundle', () => {
7+
test('Should load message bundle and update', async () => {
8+
if (reactVersion === '16.8.0') {
9+
// not testable with 16.8.0
10+
return;
11+
}
12+
const { result } = renderHook(() => useI18nBundle('@ui5/webcomponents-react'));
13+
14+
expect(result.current.getText('PLEASE_WAIT')).toBe('Please wait');
15+
});
16+
17+
test('Should update after changing the language', async () => {
18+
if (reactVersion === '16.8.0') {
19+
// not testable with 16.8.0
20+
return;
21+
}
22+
const { result, waitForNextUpdate } = renderHook(() => useI18nBundle('@ui5/webcomponents-react'));
23+
expect(result.current.getText('PLEASE_WAIT')).toBe('Please wait');
24+
25+
act(() => {
26+
setLanguage('de');
27+
});
28+
29+
await waitForNextUpdate();
30+
expect(result.current.getText('PLEASE_WAIT')).toBe('Bitte warten');
31+
32+
expect(result.all).toHaveLength(2);
33+
});
34+
});

packages/base/src/hooks/useI18nBundle.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import { getI18nBundleData } from '@ui5/webcomponents-base/dist/asset-registries/i18n';
12
import { fetchI18nBundle, getI18nBundle } from '@ui5/webcomponents-base/dist/i18nBundle';
3+
import { attachLanguageChange, detachLanguageChange } from '@ui5/webcomponents-base/dist/locale/languageChange';
4+
import { useIsomorphicLayoutEffect } from '@ui5/webcomponents-react-base/lib/hooks';
25
import { useEffect, useState } from 'react';
6+
import { deprecationNotice } from '@ui5/webcomponents-react-base/lib/Utils';
37

4-
type TextWithDefault = { key: string; defaultText: string };
8+
type TextWithDefault = { key: string; defaultText: string } | string;
59
type TextWithPlaceholders = [TextWithDefault, ...string[]];
610

711
interface I18nBundle {
8-
getText: (textObj: TextWithDefault, ...args: any) => string;
12+
getText: (textObj: TextWithDefault, ...args: any[]) => string;
913
}
1014

1115
const resolveTranslations = (bundle, texts) => {
@@ -22,6 +26,14 @@ export const useI18nText = (bundleName: string, ...texts: (TextWithDefault | Tex
2226
const i18nBundle: I18nBundle = getI18nBundle(bundleName);
2327
const [translations, setTranslations] = useState(resolveTranslations(i18nBundle, texts));
2428

29+
useEffect(() => {
30+
deprecationNotice(
31+
'useI18nText',
32+
`'useI18nText' is deprecated and will be removed in the next release. Please use 'useI18nBundle' instead.
33+
A Migration Guide can be found here: https://sap.github.io/ui5-webcomponents-react/?path=/docs/migration-guide--page#migrating-from-013x-to-0140`
34+
);
35+
}, []);
36+
2537
useEffect(() => {
2638
let didCancel = false;
2739
const fetchAndLoadBundle = async () => {
@@ -40,7 +52,44 @@ export const useI18nText = (bundleName: string, ...texts: (TextWithDefault | Tex
4052
return () => {
4153
didCancel = true;
4254
};
43-
}, [fetchI18nBundle, bundleName, texts]);
55+
}, [fetchI18nBundle, bundleName, ...texts]);
4456

4557
return translations;
4658
};
59+
60+
export const useI18nBundle = (bundleName: string): I18nBundle => {
61+
const [_, setUpdater] = useState(0);
62+
63+
useIsomorphicLayoutEffect(() => {
64+
let isMounted = true;
65+
const i18nBundleData = getI18nBundleData('@ui5/webcomponents-react');
66+
if (!i18nBundleData) {
67+
fetchI18nBundle(`${bundleName}`).then(() => {
68+
if (isMounted) {
69+
setUpdater((old) => old + 1);
70+
}
71+
});
72+
}
73+
return () => {
74+
isMounted = false;
75+
};
76+
}, [bundleName]);
77+
78+
useEffect(() => {
79+
let isMounted = true;
80+
const handler = () => {
81+
fetchI18nBundle(`${bundleName}`).then(() => {
82+
if (isMounted) {
83+
setUpdater((old) => old + 1);
84+
}
85+
});
86+
};
87+
attachLanguageChange(handler);
88+
return () => {
89+
isMounted = false;
90+
detachLanguageChange(handler);
91+
};
92+
}, []);
93+
94+
return getI18nBundle(bundleName);
95+
};

packages/base/src/lib/hooks.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
import { useConsolidatedRef } from '../hooks/useConsolidatedRef';
2-
import { useI18nText } from '../hooks/useI18nBundle';
2+
import { useI18nBundle, useI18nText } from '../hooks/useI18nBundle';
33
import { useIsomorphicLayoutEffect } from '../hooks/useIsomorphicLayoutEffect';
44
import { usePassThroughHtmlProps } from '../hooks/usePassThroughHtmlProps';
55
import { useViewportRange } from '../hooks/useViewportRange';
66

7-
export { useConsolidatedRef, usePassThroughHtmlProps, useViewportRange, useI18nText, useIsomorphicLayoutEffect };
7+
export {
8+
useConsolidatedRef,
9+
usePassThroughHtmlProps,
10+
useViewportRange,
11+
useI18nText,
12+
useIsomorphicLayoutEffect,
13+
useI18nBundle
14+
};

packages/main/src/components/AnalyticalCardHeader/index.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles';
2-
import { useI18nText, usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/hooks';
2+
import { useI18nBundle, usePassThroughHtmlProps } from '@ui5/webcomponents-react-base/lib/hooks';
33
import { StyleClassHelper } from '@ui5/webcomponents-react-base/lib/StyleClassHelper';
44
import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils';
55
import { DEVIATION, TARGET } from '@ui5/webcomponents-react/dist/assets/i18n/i18n-defaults';
@@ -171,7 +171,7 @@ export const AnalyticalCardHeader: FC<AnalyticalCardHeaderPropTypes> = forwardRe
171171

172172
const passThroughProps = usePassThroughHtmlProps(props, ['onHeaderPress']);
173173

174-
const [targetText, deviationText] = useI18nText('@ui5/webcomponents-react', TARGET, DEVIATION);
174+
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
175175

176176
return (
177177
<div
@@ -217,7 +217,7 @@ export const AnalyticalCardHeader: FC<AnalyticalCardHeaderPropTypes> = forwardRe
217217
className={classes.targetAndDeviationColumn}
218218
wrap={FlexBoxWrap.NoWrap}
219219
>
220-
<span>{targetText}</span>
220+
<span>{i18nBundle.getText(TARGET)}</span>
221221
<span className={classes.targetAndDeviationValue}>{target}</span>
222222
</FlexBox>
223223
)}
@@ -227,7 +227,7 @@ export const AnalyticalCardHeader: FC<AnalyticalCardHeaderPropTypes> = forwardRe
227227
className={classes.targetAndDeviationColumn}
228228
wrap={FlexBoxWrap.NoWrap}
229229
>
230-
<span>{deviationText}</span>
230+
<span>{i18nBundle.getText(DEVIATION)}</span>
231231
<span className={classes.targetAndDeviationValue}>{deviation}</span>
232232
</FlexBox>
233233
)}

packages/main/src/components/AnalyticalTable/ColumnHeader/ColumnHeaderModal.tsx

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import '@ui5/webcomponents-icons/dist/decline';
2-
import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks';
2+
import { useI18nBundle } from '@ui5/webcomponents-react-base/lib/hooks';
33
import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters';
44
import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils';
55
import {
@@ -45,14 +45,13 @@ export const ColumnHeaderModal = (props: ColumnHeaderModalProperties) => {
4545

4646
const { Filter } = column;
4747

48-
const [clearSortingText, sortAscendingText, sortDescendingText, groupText, ungroupText] = useI18nText(
49-
'@ui5/webcomponents-react',
50-
CLEAR_SORTING,
51-
SORT_ASCENDING,
52-
SORT_DESCENDING,
53-
GROUP,
54-
UNGROUP
55-
);
48+
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
49+
50+
const clearSortingText = i18nBundle.getText(CLEAR_SORTING);
51+
const sortAscendingText = i18nBundle.getText(SORT_ASCENDING);
52+
const sortDescendingText = i18nBundle.getText(SORT_DESCENDING);
53+
const groupText = i18nBundle.getText(GROUP);
54+
const ungroupText = i18nBundle.getText(UNGROUP);
5655

5756
const handleSort = useCallback(
5857
(e) => {

packages/main/src/components/AnalyticalTable/VerticalResizer.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks';
1+
import { useI18nBundle } from '@ui5/webcomponents-react-base/lib/hooks';
22
import { ThemingParameters } from '@ui5/webcomponents-react-base/lib/ThemingParameters';
33
import { DRAG_TO_RESIZE } from '@ui5/webcomponents-react/dist/assets/i18n/i18n-defaults';
44
import React, { MutableRefObject, useCallback, useEffect, useRef, useState } from 'react';
@@ -62,7 +62,7 @@ export const VerticalResizer = (props: VerticalResizerProps) => {
6262
const [isDragging, setIsDragging] = useState(false);
6363
const [mountTouchEvents, setMountTouchEvents] = useState(false);
6464

65-
const [dragToResizeText] = useI18nText('@ui5/webcomponents-react', DRAG_TO_RESIZE);
65+
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
6666

6767
const handleResizeStart = useCallback(
6868
(e) => {
@@ -153,7 +153,7 @@ export const VerticalResizer = (props: VerticalResizerProps) => {
153153
onMouseDown={handleResizeStart}
154154
onTouchStart={handleResizeStart}
155155
role="separator"
156-
title={dragToResizeText}
156+
title={i18nBundle.getText(DRAG_TO_RESIZE)}
157157
>
158158
{resizerPosition &&
159159
isDragging &&

packages/main/src/components/FilterBar/FilterDialog.tsx

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import '@ui5/webcomponents-icons/dist/search';
22
import { createComponentStyles } from '@ui5/webcomponents-react-base/lib/createComponentStyles';
3-
import { useI18nText } from '@ui5/webcomponents-react-base/lib/hooks';
3+
import { useI18nBundle } from '@ui5/webcomponents-react-base/lib/hooks';
44
import { enrichEventWithDetails } from '@ui5/webcomponents-react-base/lib/Utils';
55
import {
66
BASIC,
@@ -62,24 +62,15 @@ export const FilterDialog = (props) => {
6262
const dialogRefs = useRef({});
6363
const dialogRef = useRef<Ui5DialogDomRef>();
6464

65-
const [
66-
basicText,
67-
cancelText,
68-
clearText,
69-
restoreText,
70-
saveText,
71-
searchForFiltersText,
72-
showOnFilterBarText
73-
] = useI18nText(
74-
'@ui5/webcomponents-react',
75-
BASIC,
76-
CANCEL,
77-
CLEAR,
78-
RESTORE,
79-
SAVE,
80-
SEARCH_FOR_FILTERS,
81-
SHOW_ON_FILTER_BAR
82-
);
65+
const i18nBundle = useI18nBundle('@ui5/webcomponents-react');
66+
67+
const basicText = i18nBundle.getText(BASIC);
68+
const cancelText = i18nBundle.getText(CANCEL);
69+
const clearText = i18nBundle.getText(CLEAR);
70+
const restoreText = i18nBundle.getText(RESTORE);
71+
const saveText = i18nBundle.getText(SAVE);
72+
const searchForFiltersText = i18nBundle.getText(SEARCH_FOR_FILTERS);
73+
const showOnFilterBarText = i18nBundle.getText(SHOW_ON_FILTER_BAR);
8374

8475
useEffect(() => {
8576
if (open) {

0 commit comments

Comments
 (0)