diff --git a/.gitignore b/.gitignore index e6617ff..06c52bd 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ coverage node_modules package-lock.json +/types diff --git a/package.json b/package.json index b04ae8e..0683223 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@babel/preset-react": "^7.8.3", "@babel/register": "^7.8.6", "@rollup/plugin-babel": "^5.0.3", + "@types/react": "^17.0.14", "@u-wave/react-translate-example": "file:example", "@u-wave/translate": "^1.1.0", "babel-plugin-istanbul": "^6.0.0", @@ -31,7 +32,8 @@ "react": "^18.0.0", "react-dom": "^18.0.0", "react-test-renderer": "^18.0.0", - "rollup": "^2.0.6" + "rollup": "^2.0.6", + "typescript": "^4.3.5" }, "homepage": "https://github.com/u-wave/react-translate#readme", "keywords": [ @@ -43,6 +45,7 @@ "license": "MIT", "main": "dist/react-translate.js", "module": "dist/react-translate.es.js", + "typings": "types/index.d.ts", "peerDependencies": { "react": "^17.0.0 || ^18.0.0" }, @@ -51,7 +54,7 @@ "url": "git+https://github.com/u-wave/react-translate.git" }, "scripts": { - "prepare": "rollup -c", + "prepare": "tsc && rollup -c", "lint": "eslint --cache .", "test": "npm run lint && npm run tests-only", "tests-only": "nyc mocha --require @babel/register", diff --git a/src/index.js b/src/index.js index ab180ab..fed3e41 100644 --- a/src/index.js +++ b/src/index.js @@ -1,14 +1,34 @@ import React from 'react'; import PropTypes from 'prop-types'; -const TranslateContext = React.createContext(); -const { Provider, Consumer } = TranslateContext; +/** @typedef {{ + * t: (key: string, data: object) => string, + * parts: (key: string, data: object) => any[], + * }} Translator */ +/** @type {React.Context} */ +// @ts-ignore TS2322: the assigned type is narrower than the identifier's so this is fine +const TranslateContext = React.createContext(undefined); + +/** + * Make a translator instance available to the context. Children of this `TranslateProvider` element + * can access the translator instance using `useTranslate()` or `Interpolate` as listed below. + * + * ```js + * const translator = new Translator(...); + * + * + * + * + * ``` + * + * @param {{ translator: Translator, children: React.ReactNode }} props + */ export function TranslateProvider({ translator, children }) { return ( - + {children} - + ); } /* istanbul ignore next */ @@ -22,32 +42,73 @@ if (process.env.NODE_ENV !== 'production') { }; } -export const translate = () => (Component) => (props) => ( - - {(translator) => ( +/** + * Get the `@u-wave/translate` instance from the context. Destructuring the `t` function is the + * recommended way to use this instance. This can be used in place of the `translate()` HOC in + * function components to avoid introducing additional nesting and PropTypes requirements. + * + * @returns {Translator} + */ +export function useTranslator() { + const context = React.useContext(TranslateContext); + if (context === undefined) { + throw new Error('useTranslator() can only be used within a TranslateContext'); + } + return context; +} + +/** + * Get the translate function from the context. This is a higher-order component, only intended + * for use in class components. If you can, use `useTranslator()` instead. + * + * The translate function is passed in as the `t` prop. + * + * @template {object} TProps + * @returns {(Component: React.ComponentType) => + * React.ComponentType} + */ +export function translate() { + return (Component) => (props) => { + const { t } = useTranslator(); + + return ( - )} - -); - -export const useTranslator = () => React.useContext(TranslateContext); + ); + }; +} +/** + * Translate the key given in the `i18nKey` prop. The other props are used as the interpolation + * data. Unlike `useTranslate()`, this component can interpolate other React elements: + * + * ```js + * {name} + * )} + * /> + * ``` + * + * Here, the `name` prop is a React element, and it will be rendered correctly. + * + * @param {{ [key: string]: unknown, i18nKey: string }} props + */ export function Interpolate({ i18nKey, ...props }) { + const translator = useTranslator(); + return ( - - {(translator) => ( - // Manually use createElement so we're not passing an array as children to React. - // Passing the array would require us to add keys to each interpolated element - // but we know that the shape will stay the same so it's safe to spread it and act - // as if they were all written as separate children by the user. - React.createElement(React.Fragment, {}, ...translator.parts(i18nKey, props)) - )} - + // Manually use createElement so we're not passing an array as children to React. + // Passing the array would require us to add keys to each interpolated element + // but we know that the shape will stay the same so it's safe to spread it and act + // as if they were all written as separate children by the user. + React.createElement(React.Fragment, {}, ...translator.parts(i18nKey, props)) ); } + /* istanbul ignore next */ if (process.env.NODE_ENV !== 'production') { Interpolate.propTypes = { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2e006d7 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "jsx": "preserve", + "esModuleInterop": true, + "strict": false, + "strictBindCallApply": true, + "strictNullChecks": true, + "noImplicitThis": true, + "allowJs": true, + "checkJs": true, + "outDir": "types", + "declaration": true, + "emitDeclarationOnly": true + }, + "include": [ + "src/**/*.js" + ], + "exclude": [ + "node_modules" + ] +}