Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@
coverage
node_modules
package-lock.json
/types
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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": [
Expand All @@ -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"
},
Expand All @@ -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",
Expand Down
105 changes: 83 additions & 22 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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<Translator | undefined>} */
// @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(...);
*
* <TranslateProvider translator={translator}>
* <App />
* </TranslateProvider>
* ```
*
* @param {{ translator: Translator, children: React.ReactNode }} props
*/
export function TranslateProvider({ translator, children }) {
return (
<Provider value={translator}>
<TranslateContext.Provider value={translator}>
{children}
</Provider>
</TranslateContext.Provider>
);
}
/* istanbul ignore next */
Expand All @@ -22,32 +42,73 @@ if (process.env.NODE_ENV !== 'production') {
};
}

export const translate = () => (Component) => (props) => (
<Consumer>
{(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<TProps>) =>
* React.ComponentType<TProps & { t: Translator['t'] }>}
*/
export function translate() {
return (Component) => (props) => {
const { t } = useTranslator();

return (
<Component
{...props} // eslint-disable-line react/jsx-props-no-spreading
t={translator.t}
t={t}
/>
)}
</Consumer>
);

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
* <Interpolate
* i18nKey="welcome"
* name={(
* <strong>{name}</strong>
* )}
* />
* ```
*
* 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 (
<Consumer>
{(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))
)}
</Consumer>
// 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 = {
Expand Down
21 changes: 21 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -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"
]
}