Skip to content

Commit

Permalink
i18n-calypso: add useTranslate hook (#31043)
Browse files Browse the repository at this point in the history
* i18n-calypso: add useTranslate React hook

* post-share/no-connection-notice: use the useTranslate hook

* Optimize the useTranslate hook a bit

Use lazy creation of the initial state, create the change handler inside the
`useEffect` callback. Both changes prevent creation of unneeded function objects
on every hook call.

* Document the useTranslate hook

* Add unit test for the useTranslate hook

* Returns just the translate function from the hook (not array) and expose translate.localeSlug property

* Use the ReactDOM rendered and JSDOM in tests

* Add unit test for useTranslate reactive rerender

* Add test for reactivity when locale does not change, fix implementation
  • Loading branch information
jsnajdr authored Mar 5, 2019
1 parent 3db02c4 commit ad3e868
Show file tree
Hide file tree
Showing 6 changed files with 178 additions and 13 deletions.
26 changes: 15 additions & 11 deletions client/blocks/post-share/no-connections-notice.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,26 @@
* External dependencies
*/
import React from 'react';
import { localize } from 'i18n-calypso';
import { useTranslate } from 'i18n-calypso';

/**
* Internal dependencies
*/
import Notice from 'components/notice';
import NoticeAction from 'components/notice/notice-action';

const NoConnectionsNotice = ( { siteSlug, translate } ) => (
<Notice
status="is-warning"
showDismiss={ false }
text={ translate( 'Connect an account to get started.' ) }
>
<NoticeAction href={ `/sharing/${ siteSlug }` }>{ translate( 'Settings' ) }</NoticeAction>
</Notice>
);
const NoConnectionsNotice = ( { siteSlug } ) => {
const translate = useTranslate();

export default localize( NoConnectionsNotice );
return (
<Notice
status="is-warning"
showDismiss={ false }
text={ translate( 'Connect an account to get started.' ) }
>
<NoticeAction href={ `/sharing/${ siteSlug }` }>{ translate( 'Settings' ) }</NoticeAction>
</Notice>
);
};

export default NoConnectionsNotice;
36 changes: 35 additions & 1 deletion packages/i18n-calypso/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,41 @@ render(
);
```

## React Hook

The `useTranslate` hook is a modern alternative to the `localize` higher-order component that
exposes the `translate` method to React components as a return value of a React hook. The
resulting component is also reactive, i.e., it gets rerendered when the `i18n` locale changes
and the state emitter emits a `change` event.

The `useTranslate` hook returns the `translate` function:
```jsx
const translate = useTranslate();
```
The function can be called to return a localized value of a string, and it also exposes a
`localeSlug` property whose value is a string with the current locale slug.

### Usage

```jsx
import React from 'react';
import { useTranslate } from 'i18n-calypso';

function Greeting( { className } ) {
const translate = useTranslate();
debug( 'using translate with locale:', translate.localeSlug );
return (
<h1 className={ className }>
{ translate( 'Hello!' ) }
</h1>
);
}

export default Greeting;
```

Unlike the `localize` HOC, the component doesn't need to be wrapped and receives the `translate`
function from the hook call rather than a prop.

## Some Background

Expand All @@ -323,4 +358,3 @@ just the hash is used for lookup, resulting in a shorter file.
The generator of the jed file would usually try to choose the smallest hash length at which no hash collisions occur. In the above example a hash length of 1 (`d` short for `d2306dd8970ff616631a3501791297f31475e416`) is enough because there is only one string.

Note that when generating the jed file, all possible strings need to be taken into consideration for the collision calculation, as otherwise an untranslated source string would be provided with the wrong translation.

2 changes: 1 addition & 1 deletion packages/i18n-calypso/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"lodash": "^4.7.11",
"lru": "^3.1.0",
"moment-timezone": "^0.5.23",
"react": "^16.6.3",
"react": "^16.8.3",
"xgettext-js": "^2.0.0"
},
"devDependencies": {
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n-calypso/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
import I18N from './i18n';
import localizeFactory from './localize';
import useTranslateFactory from './use-translate';

const i18n = new I18N();
export { I18N };
Expand All @@ -26,3 +27,4 @@ export const on = i18n.on.bind( i18n );
export const off = i18n.off.bind( i18n );
export const emit = i18n.emit.bind( i18n );
export const localize = localizeFactory( i18n );
export const useTranslate = useTranslateFactory( i18n );
24 changes: 24 additions & 0 deletions packages/i18n-calypso/src/use-translate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* External dependencies
*/
import React from 'react';

export default function( i18n ) {
function bindTranslate() {
const translate = i18n.translate.bind( i18n );
Object.defineProperty( translate, 'localeSlug', { get: i18n.getLocaleSlug.bind( i18n ) } );
return translate;
}

return function useTranslate() {
const [ translate, setTranslate ] = React.useState( bindTranslate );

React.useEffect(() => {
const onChange = () => setTranslate( bindTranslate );
i18n.on( 'change', onChange );
return () => i18n.off( 'change', onChange );
}, []);

return translate;
};
}
101 changes: 101 additions & 0 deletions packages/i18n-calypso/test/use-translate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @jest-environment jsdom
*/
/**
* External dependencies
*/
import React from 'react';
import ReactDOM from 'react-dom';
import { act } from 'react-dom/test-utils';

/**
* Internal dependencies
*/
import i18n, { useTranslate } from '../src';

function Label() {
const translate = useTranslate();
return translate( 'hook (%(lang)s)', { args: { lang: translate.localeSlug } } );
}

describe( 'useTranslate()', () => {
let container;

beforeEach( () => {
// reset to default locale
i18n.setLocale();

// create container
container = document.createElement( 'div' );
document.body.appendChild( container );
} );

afterEach( () => {
// tear down the container
ReactDOM.unmountComponentAtNode( container );
document.body.removeChild( container );
container = null;
} );

test( 'renders a translated string', () => {
// set some locale data
i18n.setLocale( {
'': { localeSlug: 'cs' },
'hook (%(lang)s)': [ 'háček (%(lang)s)' ],
} );

// render the Label component
act( () => {
ReactDOM.render( <Label />, container );
} );

// check that it's translated
expect( container.textContent ).toBe( 'háček (cs)' );
} );

test( 'rerenders after locale change', () => {
// render with the default locale
act( () => {
ReactDOM.render( <Label />, container );
} );

expect( container.textContent ).toBe( 'hook (en)' );

// change locale and ensure that React UI is rerendered
act( () => {
i18n.setLocale( {
'': { localeSlug: 'cs' },
'hook (%(lang)s)': [ 'háček (%(lang)s)' ],
} );
} );

expect( container.textContent ).toBe( 'háček (cs)' );
} );

test( 'rerenders after update of current locale translations', () => {
// set some locale data
i18n.setLocale( {
'': { localeSlug: 'cs' },
'hook (%(lang)s)': [ 'háček (%(lang)s)' ],
} );

// render the Label component
act( () => {
ReactDOM.render( <Label />, container );
} );

// check that it's translated
expect( container.textContent ).toBe( 'háček (cs)' );

// update the translations for the current locale
act( () => {
i18n.setLocale( {
'': { localeSlug: 'cs' },
'hook (%(lang)s)': [ 'hák (%(lang)s)' ],
} );
} );

// check that the rendered translation is updated
expect( container.textContent ).toBe( 'hák (cs)' );
} );
} );

0 comments on commit ad3e868

Please sign in to comment.