Skip to content

Commit a682e10

Browse files
committed
introduce React Hooks and expose useMessageSource hook
1 parent d5ccbcf commit a682e10

File tree

8 files changed

+196
-108
lines changed

8 files changed

+196
-108
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
},
3535
"peerDependencies": {
3636
"prop-types": "^15.5.10",
37-
"react": "^16.6.0"
37+
"react": "^16.8.0"
3838
},
3939
"devDependencies": {
4040
"@babel/core": "^7.3.4",

src/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
/**
22
* The Public API.
33
*/
4-
export { Provider, withMessages, propTypes } from './lib/messageSource';
4+
export { Provider } from './lib/MessageSourceContext';
55
export { FetchingProvider } from './lib/FetchingProvider';
6+
export { useMessageSource } from './lib/useMessageSource';
7+
export { withMessages, propTypes } from './lib/messageSource';

src/lib/FetchingProvider.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { Component } from 'react';
22
import PropTypes from 'prop-types';
33

4-
import { Provider } from './messageSource';
4+
import { Provider } from './MessageSourceContext';
55

66
const identity = x => x;
77

src/lib/MessageSourceContext.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import React from 'react';
2+
3+
/**
4+
* Initial Context value, an empty object.
5+
*/
6+
const empty = {};
7+
8+
/**
9+
* A React Context which holds the translations map.
10+
*/
11+
const MessageSourceContext = React.createContext(empty);
12+
MessageSourceContext.displayName = 'MessageSourceContext';
13+
14+
/**
15+
* The MessageSourceContext object.
16+
*/
17+
export { MessageSourceContext };
18+
19+
/**
20+
* Example usage:
21+
*
22+
* const translations = await fetch('/api/rest/texts?lang=en');
23+
* <MessageSource.Provider value={translations}>
24+
* <SomeOtherComponent />
25+
* ...
26+
* </MessageSource.Provider>
27+
*/
28+
export const { Provider } = MessageSourceContext;

src/lib/messageSource.js

Lines changed: 16 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -2,100 +2,37 @@ import React from 'react';
22
import PropTypes from 'prop-types';
33
import invariant from 'invariant';
44
import hoistNonReactStatics from 'hoist-non-react-statics';
5-
import { getMessageWithNamedParams, getMessageWithParams } from './messages';
6-
import { normalizeKeyPrefix } from './utils';
7-
8-
/**
9-
* A React Context which holds the translations map.
10-
*/
11-
const MessageSourceContext = React.createContext(null);
12-
MessageSourceContext.displayName = 'MessageSourceContext';
5+
import { useMessageSource } from './useMessageSource';
136

147
/**
158
* Creates a HOC which passes the MessageSourceApi to the given Component.
169
*/
1710
function enhanceWithMessages(keyPrefix, WrappedComponent) {
18-
const normalizedKeyPrefix = normalizeKeyPrefix(keyPrefix || '');
1911
const wrappedComponentName = WrappedComponent.displayName || WrappedComponent.name || 'Component';
2012

2113
/**
2214
* The enhancer HOC.
2315
*/
24-
class Enhancer extends React.Component {
25-
/**
26-
* Retrieves a text message.
27-
*
28-
* Example usage:
29-
* let name, lastName;
30-
* ...
31-
* const message = getMessage('message.key', name, lastName);
32-
*
33-
* @param key the key of the message.
34-
* @param params an optional parameters (param0, param1 ...).
35-
*/
36-
getMessage = (key, ...params) => {
37-
const textKey = normalizedKeyPrefix + key;
38-
const message = getMessageWithParams(this.context, textKey, ...params);
39-
if (message === textKey) {
40-
return getMessageWithParams(this.context, key, ...params);
41-
}
42-
43-
return message;
44-
};
45-
46-
/**
47-
* Retrieves a text message with named parameters.
48-
*
49-
* Example usage:
50-
* const parameters = {
51-
* name: 'John Doe',
52-
* }
53-
*
54-
* const message = getMessageWithNamedParams('message.key', parameters)
55-
*
56-
* @param key the key of the message.
57-
* @param namedParams a map of named parameters.
58-
*/
59-
getMessageWithNamedParams = (key, namedParams) => {
60-
const textKey = normalizedKeyPrefix + key;
61-
const message = getMessageWithNamedParams(this.context, textKey, namedParams);
62-
if (message === textKey) {
63-
return getMessageWithNamedParams(this.context, key, namedParams);
64-
}
65-
66-
return message;
67-
};
68-
69-
render() {
70-
if (process.env.NODE_ENV !== 'production') {
71-
/* eslint-disable react/prop-types */
72-
invariant(
73-
!this.props.getMessage,
74-
`[react-message-source]: [%s] already has a prop named [getMessage]. It will be overwritten.`,
75-
wrappedComponentName,
76-
);
77-
78-
invariant(
79-
!this.props.getMessageWithNamedParams,
80-
`[react-message-source]: [%s] already has a prop named [getMessageWithNamedParams]. It will be overwritten.`,
81-
wrappedComponentName,
82-
);
83-
/* eslint-enable react/prop-types */
84-
}
85-
86-
return (
87-
<WrappedComponent
88-
{...this.props}
89-
getMessage={this.getMessage}
90-
getMessageWithNamedParams={this.getMessageWithNamedParams}
91-
/>
16+
function Enhancer(props) {
17+
const messageSourceApi = useMessageSource(keyPrefix);
18+
if (process.env.NODE_ENV !== 'production') {
19+
const hasOwn = Object.prototype.hasOwnProperty;
20+
const propsToOverwrite = Object.keys(messageSourceApi)
21+
.filter(propToCheck => hasOwn.call(props, propToCheck))
22+
.join(', ');
23+
24+
invariant(
25+
!propsToOverwrite,
26+
`[react-message-source]: [%s] already has props named [%s]. They will be overwritten.`,
27+
wrappedComponentName,
28+
propsToOverwrite,
9229
);
9330
}
31+
32+
return <WrappedComponent {...props} {...messageSourceApi} />;
9433
}
9534

96-
Enhancer.contextType = MessageSourceContext;
9735
Enhancer.displayName = `WithMessages(${wrappedComponentName})`;
98-
9936
return hoistNonReactStatics(Enhancer, WrappedComponent);
10037
}
10138

@@ -113,17 +50,6 @@ function internalWithMessages(keyPrefixOrComponent) {
11350
return enhanceWithMessages(null, keyPrefixOrComponent);
11451
}
11552

116-
/**
117-
* Example usage:
118-
*
119-
* const translations = await fetch('/api/rest/texts?lang=en');
120-
* <MessageSource.Provider value={translations}>
121-
* <SomeOtherComponent />
122-
* ...
123-
* </MessageSource.Provider>
124-
*/
125-
export const { Provider } = MessageSourceContext;
126-
12753
/**
12854
* Example usages:
12955
*

src/lib/messageSource.test.js

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import React from 'react';
22
import TestRenderer from 'react-test-renderer';
3+
import { Provider as MessageSourceProvider } from './MessageSourceContext';
34
import * as MessageSource from './messageSource';
45

56
/* eslint-disable react/prop-types */
67

7-
describe('MessageSource', () => {
8+
describe('withMessages', () => {
89
const translations = {
910
'hello.world': 'Hello World',
1011
'greeting.normal': 'Hi',
@@ -34,9 +35,9 @@ describe('MessageSource', () => {
3435
const NestedHOC = MessageSource.withMessages(Nested);
3536

3637
const renderer = TestRenderer.create(
37-
<MessageSource.Provider value={translations}>
38+
<MessageSourceProvider value={translations}>
3839
<NestedHOC />
39-
</MessageSource.Provider>,
40+
</MessageSourceProvider>,
4041
);
4142

4243
const { root } = renderer;
@@ -54,9 +55,9 @@ describe('MessageSource', () => {
5455
const NestedHOC = MessageSource.withMessages(Nested);
5556

5657
const renderer = TestRenderer.create(
57-
<MessageSource.Provider value={translations}>
58+
<MessageSourceProvider value={translations}>
5859
<NestedHOC />
59-
</MessageSource.Provider>,
60+
</MessageSourceProvider>,
6061
);
6162

6263
const { root } = renderer;
@@ -74,9 +75,9 @@ describe('MessageSource', () => {
7475
const NestedHOC = MessageSource.withMessages()(Nested);
7576

7677
const renderer = TestRenderer.create(
77-
<MessageSource.Provider value={translations}>
78+
<MessageSourceProvider value={translations}>
7879
<NestedHOC />
79-
</MessageSource.Provider>,
80+
</MessageSourceProvider>,
8081
);
8182

8283
const { root } = renderer;
@@ -94,9 +95,9 @@ describe('MessageSource', () => {
9495
const NestedHOC = MessageSource.withMessages('hello')(Nested);
9596

9697
const renderer = TestRenderer.create(
97-
<MessageSource.Provider value={translations}>
98+
<MessageSourceProvider value={translations}>
9899
<NestedHOC />
99-
</MessageSource.Provider>,
100+
</MessageSourceProvider>,
100101
);
101102

102103
const { root } = renderer;
@@ -119,9 +120,9 @@ describe('MessageSource', () => {
119120
const NestedHOC = MessageSource.withMessages('hello')(Nested);
120121

121122
const renderer = TestRenderer.create(
122-
<MessageSource.Provider value={translations}>
123+
<MessageSourceProvider value={translations}>
123124
<NestedHOC />
124-
</MessageSource.Provider>,
125+
</MessageSourceProvider>,
125126
);
126127

127128
const { root } = renderer;
@@ -148,12 +149,12 @@ describe('MessageSource', () => {
148149
const NestedHOC = MessageSource.withMessages()(Nested);
149150

150151
const renderer = TestRenderer.create(
151-
<MessageSource.Provider value={levelOne}>
152+
<MessageSourceProvider value={levelOne}>
152153
<NestedHOC />
153-
<MessageSource.Provider value={levelTwo}>
154+
<MessageSourceProvider value={levelTwo}>
154155
<NestedHOC />
155-
</MessageSource.Provider>
156-
</MessageSource.Provider>,
156+
</MessageSourceProvider>
157+
</MessageSourceProvider>,
157158
);
158159

159160
const components = renderer.root.findAllByType(Nested);

src/lib/useMessageSource.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import React from 'react';
2+
import { MessageSourceContext } from './MessageSourceContext';
3+
import { getMessageWithNamedParams, getMessageWithParams } from './messages';
4+
import { normalizeKeyPrefix } from './utils';
5+
6+
/**
7+
* A Hook which which provides the MessageSourceApi.
8+
*
9+
* @param keyPrefix an optional prefix which will be prepended to the lookup key.
10+
*/
11+
export function useMessageSource(keyPrefix) {
12+
const textKeys = React.useContext(MessageSourceContext);
13+
return React.useMemo(() => {
14+
const keyPrefixToUse = normalizeKeyPrefix(keyPrefix || '');
15+
return {
16+
/**
17+
* Retrieves a text message.
18+
*
19+
* Example usage:
20+
* let name, lastName;
21+
* ...
22+
* const message = getMessage('message.key', name, lastName);
23+
*
24+
* @param key the key of the message.
25+
* @param params an optional parameters (param0, param1 ...).
26+
*/
27+
getMessage(key, ...params) {
28+
const textKey = keyPrefixToUse + key;
29+
const message = getMessageWithParams(textKeys, textKey, ...params);
30+
if (message === textKey) {
31+
// retry with key only (no prefix)
32+
return getMessageWithParams(textKeys, key, ...params);
33+
}
34+
35+
return message;
36+
},
37+
38+
/**
39+
* Retrieves a text message with named parameters.
40+
*
41+
* Example usage:
42+
* const parameters = {
43+
* name: 'John Doe',
44+
* }
45+
*
46+
* const message = getMessageWithNamedParams('message.key', parameters)
47+
*
48+
* @param key the key of the message.
49+
* @param namedParams a map of named parameters.
50+
*/
51+
getMessageWithNamedParams(key, namedParams) {
52+
const textKey = keyPrefixToUse + key;
53+
const message = getMessageWithNamedParams(textKeys, textKey, namedParams);
54+
if (message === textKey) {
55+
// retry with key only (no prefix)
56+
return getMessageWithNamedParams(textKeys, key, namedParams);
57+
}
58+
59+
return message;
60+
},
61+
};
62+
}, [textKeys, keyPrefix]);
63+
}

0 commit comments

Comments
 (0)