Skip to content

Commit

Permalink
Add react-i18n package with i18n React bindings (#28465)
Browse files Browse the repository at this point in the history
* Add react-i18n package with i18n React bindings

* Add documentation for withI18n params and return value
  • Loading branch information
jsnajdr committed Feb 11, 2021
1 parent f983876 commit f456ee9
Show file tree
Hide file tree
Showing 8 changed files with 279 additions and 0 deletions.
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1757,6 +1757,12 @@
"markdown_source": "../packages/project-management-automation/README.md",
"parent": "packages"
},
{
"title": "@wordpress/react-i18n",
"slug": "packages-react-i18n",
"markdown_source": "../packages/react-i18n/README.md",
"parent": "packages"
},
{
"title": "@wordpress/react-native-aztec",
"slug": "packages-react-native-aztec",
Expand Down
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"@wordpress/plugins": "file:packages/plugins",
"@wordpress/primitives": "file:packages/primitives",
"@wordpress/priority-queue": "file:packages/priority-queue",
"@wordpress/react-i18n": "file:packages/react-i18n",
"@wordpress/react-native-aztec": "file:packages/react-native-aztec",
"@wordpress/react-native-bridge": "file:packages/react-native-bridge",
"@wordpress/react-native-editor": "file:packages/react-native-editor",
Expand Down
86 changes: 86 additions & 0 deletions packages/react-i18n/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# React Bindings for I18n

React bindings for [`@wordpress/i18n`](../i18n).

## Installation

Install the module:

```sh
npm install @wordpress/react-i18n
```

_This package assumes that your code will run in an **ES2015+** environment. If you're using an environment that has limited or no support for ES2015+ such as lower versions of IE then using [core-js](https://github.com/zloirock/core-js) or [@babel/polyfill](https://babeljs.io/docs/en/next/babel-polyfill) will add support for these methods. Learn more about it in [Babel docs](https://babeljs.io/docs/en/next/caveats)._

## API

<!-- START TOKEN(Autogenerated API docs) -->

<a name="I18nProvider" href="#I18nProvider">#</a> **I18nProvider**

The `I18nProvider` should be mounted above any localized components:

_Usage_

```js
import { createI18n } from '@wordpress/react-i18n';
import { I18nProvider } from '@wordpress/react-i18n';
const i18n = createI18n();

ReactDom.render(
<I18nProvider i18n={ i18n }>
<App />
</I18nProvider>,
el
);
```

You can also instantiate the provider without the `i18n` prop. In that case it will use the
default `I18n` instance exported from `@wordpress/i18n`.

<a name="useI18n" href="#useI18n">#</a> **useI18n**

React hook providing access to i18n functions. It exposes the `__`, `_x`, `_n`, `_nx`,
`isRTL` and `hasTranslation` functions from [`@wordpress/i18n`](../i18n).
Refer to their documentation there.

_Usage_

```js
import { useI18n } from '@wordpress/react-i18n';

function MyComponent() {
const { __ } = useI18n();
return __( 'Hello, world!' );
}
```

<a name="withI18n" href="#withI18n">#</a> **withI18n**

React higher-order component that passes the i18n translate functions (the same set
as exposed by the `useI18n` hook) to the wrapped component as props.

_Usage_

```js
import { withI18n } from '@wordpress/react-i18n';

function MyComponent( { __ } ) {
return __( 'Hello, world!' );
}

export default withI18n( MyComponent );
```

_Parameters_

- _InnerComponent_ (unknown type): React component to be wrapped and receive the i18n functions like `__`

_Returns_

- (unknown type): The wrapped component


<!-- END TOKEN(Autogenerated API docs) -->

<br/><br/><p align="center"><img src="https://s.w.org/style/images/codeispoetry.png?1" alt="Code is Poetry." /></p>
35 changes: 35 additions & 0 deletions packages/react-i18n/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@wordpress/react-i18n",
"version": "1.0.0-alpha.1",
"description": "React bindings for @wordpress/i18n.",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
"keywords": [
"wordpress",
"gutenberg",
"i18n"
],
"homepage": "https://github.com/WordPress/gutenberg/tree/HEAD/packages/react-i18n/README.md",
"repository": {
"type": "git",
"url": "https://github.com/WordPress/gutenberg.git",
"directory": "packages/react-i18n"
},
"bugs": {
"url": "https://github.com/WordPress/gutenberg/issues"
},
"main": "build/index.js",
"module": "build-module/index.js",
"react-native": "src/index",
"types": "build-types",
"sideEffects": false,
"dependencies": {
"@babel/runtime": "^7.12.5",
"@wordpress/element": "file:../element",
"@wordpress/i18n": "file:../i18n",
"utility-types": "^3.10.0"
},
"publishConfig": {
"access": "public"
}
}
127 changes: 127 additions & 0 deletions packages/react-i18n/src/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/**
* WordPress dependencies
*/
import {
createContext,
useContext,
useEffect,
useMemo,
useReducer,
} from '@wordpress/element';
import { defaultI18n } from '@wordpress/i18n';
import type { I18n } from '@wordpress/i18n';
import type { ComponentType, PropsWithChildren } from 'react';
import type { Subtract } from 'utility-types';

interface I18nContextProps {
__: I18n[ '__' ];
_x: I18n[ '_x' ];
_n: I18n[ '_n' ];
_nx: I18n[ '_nx' ];
isRTL: I18n[ 'isRTL' ];
hasTranslation: I18n[ 'hasTranslation' ];
}

/**
* Utility to make a new context value
*/
function makeContextValue( i18n: I18n ): I18nContextProps {
return {
__: i18n.__.bind( i18n ),
_x: i18n._x.bind( i18n ),
_n: i18n._n.bind( i18n ),
_nx: i18n._nx.bind( i18n ),
isRTL: i18n.isRTL.bind( i18n ),
hasTranslation: i18n.hasTranslation.bind( i18n ),
};
}

const I18nContext = createContext( makeContextValue( defaultI18n ) );

type I18nProviderProps = PropsWithChildren< { i18n: I18n } >;

/**
* The `I18nProvider` should be mounted above any localized components:
*
* @example
* ```js
* import { createI18n } from '@wordpress/react-i18n';
* import { I18nProvider } from '@wordpress/react-i18n';
* const i18n = createI18n();
*
* ReactDom.render(
* <I18nProvider i18n={ i18n }>
* <App />
* </I18nProvider>,
* el
* );
* ```
*
* You can also instantiate the provider without the `i18n` prop. In that case it will use the
* default `I18n` instance exported from `@wordpress/i18n`.
*/
export function I18nProvider( props: I18nProviderProps ) {
const { children, i18n = defaultI18n } = props;
const [ update, forceUpdate ] = useReducer( () => [], [] );

// rerender translations whenever the i18n instance fires a change event
useEffect( () => i18n.subscribe( forceUpdate ), [ i18n ] );

const value = useMemo( () => makeContextValue( i18n ), [ i18n, update ] );

return (
<I18nContext.Provider value={ value }>
{ children }
</I18nContext.Provider>
);
}

/**
* React hook providing access to i18n functions. It exposes the `__`, `_x`, `_n`, `_nx`,
* `isRTL` and `hasTranslation` functions from [`@wordpress/i18n`](../i18n).
* Refer to their documentation there.
*
* @example
* ```js
* import { useI18n } from '@wordpress/react-i18n';
*
* function MyComponent() {
* const { __ } = useI18n();
* return __( 'Hello, world!' );
* }
* ```
*/
export const useI18n = () => useContext( I18nContext );

/**
* React higher-order component that passes the i18n translate functions (the same set
* as exposed by the `useI18n` hook) to the wrapped component as props.
*
* @example
* ```js
* import { withI18n } from '@wordpress/react-i18n';
*
* function MyComponent( { __ } ) {
* return __( 'Hello, world!' );
* }
*
* export default withI18n( MyComponent );
* ```
*
* @param InnerComponent React component to be wrapped and receive the i18n functions like `__`
* @return The wrapped component
*/
export function withI18n< P extends I18nContextProps >(
InnerComponent: ComponentType< P >
) {
const EnhancedComponent: ComponentType<
Subtract< P, I18nContextProps >
> = ( props ) => {
const i18nProps = useI18n();
return <InnerComponent { ...( props as P ) } { ...i18nProps } />;
};
const innerComponentName =
InnerComponent.displayName || InnerComponent.name || 'Component';
EnhancedComponent.displayName = `WithI18n(${ innerComponentName })`;
return EnhancedComponent;
}
9 changes: 9 additions & 0 deletions packages/react-i18n/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"declarationDir": "build-types"
},
"references": [ { "path": "../element" }, { "path": "../i18n" } ],
"include": [ "src/**/*" ]
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
{ "path": "packages/primitives" },
{ "path": "packages/priority-queue" },
{ "path": "packages/project-management-automation" },
{ "path": "packages/react-i18n" },
{ "path": "packages/token-list" },
{ "path": "packages/url" },
{ "path": "packages/warning" },
Expand Down

0 comments on commit f456ee9

Please sign in to comment.