From f456ee9b32a3ecf6b4c41e345a23e9f0e568b964 Mon Sep 17 00:00:00 2001 From: Jarda Snajdr Date: Thu, 11 Feb 2021 12:36:27 +0100 Subject: [PATCH] Add react-i18n package with i18n React bindings (#28465) * Add react-i18n package with i18n React bindings * Add documentation for withI18n params and return value --- docs/manifest.json | 6 ++ package-lock.json | 14 ++++ package.json | 1 + packages/react-i18n/README.md | 86 ++++++++++++++++++++ packages/react-i18n/package.json | 35 ++++++++ packages/react-i18n/src/index.tsx | 127 ++++++++++++++++++++++++++++++ packages/react-i18n/tsconfig.json | 9 +++ tsconfig.json | 1 + 8 files changed, 279 insertions(+) create mode 100644 packages/react-i18n/README.md create mode 100644 packages/react-i18n/package.json create mode 100644 packages/react-i18n/src/index.tsx create mode 100644 packages/react-i18n/tsconfig.json diff --git a/docs/manifest.json b/docs/manifest.json index e571ec2b03ea8..24243f9766e52 100644 --- a/docs/manifest.json +++ b/docs/manifest.json @@ -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", diff --git a/package-lock.json b/package-lock.json index e09b803017955..9e0f3ed441371 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12758,6 +12758,15 @@ "@babel/runtime": "^7.12.5" } }, + "@wordpress/react-i18n": { + "version": "file:packages/react-i18n", + "requires": { + "@babel/runtime": "^7.12.5", + "@wordpress/element": "file:packages/element", + "@wordpress/i18n": "file:packages/i18n", + "utility-types": "^3.10.0" + } + }, "@wordpress/react-native-aztec": { "version": "file:packages/react-native-aztec", "requires": { @@ -54560,6 +54569,11 @@ "integrity": "sha1-ihagXURWV6Oupe7MWxKk+lN5dyw=", "dev": true }, + "utility-types": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", + "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", diff --git a/package.json b/package.json index fe197cbdc4f9e..773e119a1835c 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/packages/react-i18n/README.md b/packages/react-i18n/README.md new file mode 100644 index 0000000000000..f0065bbd03dd2 --- /dev/null +++ b/packages/react-i18n/README.md @@ -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 + + + +# **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( + + + , + 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`. + +# **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!' ); +} +``` + +# **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 + + + + +

Code is Poetry.

diff --git a/packages/react-i18n/package.json b/packages/react-i18n/package.json new file mode 100644 index 0000000000000..56ddaeefbf6d1 --- /dev/null +++ b/packages/react-i18n/package.json @@ -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" + } +} diff --git a/packages/react-i18n/src/index.tsx b/packages/react-i18n/src/index.tsx new file mode 100644 index 0000000000000..980b11edc88b7 --- /dev/null +++ b/packages/react-i18n/src/index.tsx @@ -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( + * + * + * , + * 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 ( + + { children } + + ); +} + +/** + * 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 ; + }; + const innerComponentName = + InnerComponent.displayName || InnerComponent.name || 'Component'; + EnhancedComponent.displayName = `WithI18n(${ innerComponentName })`; + return EnhancedComponent; +} diff --git a/packages/react-i18n/tsconfig.json b/packages/react-i18n/tsconfig.json new file mode 100644 index 0000000000000..2a29fe3367623 --- /dev/null +++ b/packages/react-i18n/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "declarationDir": "build-types" + }, + "references": [ { "path": "../element" }, { "path": "../i18n" } ], + "include": [ "src/**/*" ] +} diff --git a/tsconfig.json b/tsconfig.json index 9197c90e3f36c..06ef384950a43 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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" },