From c46598893cdd00eeb70d797ff2f2556e772898cc Mon Sep 17 00:00:00 2001 From: Micha Reiser Date: Thu, 30 Nov 2017 21:15:13 +0100 Subject: [PATCH] React-i18next Generic translate Fixes #16395 --- types/react-i18next/index.d.ts | 18 ++---- types/react-i18next/src/context.d.ts | 14 +++++ types/react-i18next/src/props.d.ts | 24 +++++++ types/react-i18next/src/translate.d.ts | 61 ++++++++++++++++-- .../test/react-i18next-tests.tsx | 63 ++++++++++++++++--- 5 files changed, 151 insertions(+), 29 deletions(-) create mode 100644 types/react-i18next/src/context.d.ts create mode 100644 types/react-i18next/src/props.d.ts diff --git a/types/react-i18next/index.d.ts b/types/react-i18next/index.d.ts index d7bb05a30f..9e731fe5e6 100644 --- a/types/react-i18next/index.d.ts +++ b/types/react-i18next/index.d.ts @@ -1,10 +1,10 @@ -// Type definitions for react-i18next 4.6 +// Type definitions for react-i18next 7.0 // Project: https://github.com/i18next/react-i18next // Definitions by: Giedrius Grabauskas // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.3 +// TypeScript Version: 2.4 -import { TranslationFunction } from "i18next"; +import { i18n as I18n, TranslationFunction } from "i18next"; import I18nextProvider from "./src/I18nextProvider"; import Interpolate from "./src/interpolate"; @@ -22,16 +22,6 @@ export { TranslationFunction }; -/** - * Extend your component's Prop interface with this one to get access to `this.props.t` - * - * Please note that if you use the `translateFuncName` option, you should create - * your own interface just like this one, but with your name of the translation function. - * - * interface MyComponentProps extends ReactI18next.InjectedTranslateProps {} - */ -export interface InjectedTranslateProps { - t?: TranslationFunction; -} +export { InjectedI18nProps, InjectedTranslateProps } from "./src/props"; export as namespace reactI18Next; diff --git a/types/react-i18next/src/context.d.ts b/types/react-i18next/src/context.d.ts new file mode 100644 index 0000000000..8e11a4d7a3 --- /dev/null +++ b/types/react-i18next/src/context.d.ts @@ -0,0 +1,14 @@ +import { i18n } from "i18next"; + +export interface ReactI18NextOptions { + wait?: boolean; + withRef?: boolean; + bindI18n?: string; + bindStore?: string; + translateFuncName?: string; + nsMode?: string; +} + +export function setDefaults(options: ReactI18NextOptions): void; + +export function setI18n(instance: i18n): void; diff --git a/types/react-i18next/src/props.d.ts b/types/react-i18next/src/props.d.ts new file mode 100644 index 0000000000..62a201049a --- /dev/null +++ b/types/react-i18next/src/props.d.ts @@ -0,0 +1,24 @@ +import {i18n as I18n, TranslationFunction} from "i18next"; + +/** + * Extend your component's Prop interface with this one to get access to `this.props.t` + * + * Please note that if you use the `translateFuncName` option, you should create + * your own interface just like this one, but with your name of the translation function. + * + * interface MyComponentProps extends ReactI18next.InjectedTranslateProps {} + * + * Then specify the name of the translate function as generic argument + * + * const translated = translate("view", { translateFuncName: "_" })(YourComponent); + */ +export interface InjectedTranslateProps { + t: TranslationFunction; +} + +/** + * Extend your component's Prop interface with this one to get access to `this.props.i18n` + */ +export interface InjectedI18nProps { + i18n: I18n +} diff --git a/types/react-i18next/src/translate.d.ts b/types/react-i18next/src/translate.d.ts index d41b4dc00a..0711d3895d 100644 --- a/types/react-i18next/src/translate.d.ts +++ b/types/react-i18next/src/translate.d.ts @@ -1,15 +1,64 @@ import * as React from "react"; -import { i18n } from "i18next"; +import { i18n as I18n, TranslationFunction } from "i18next"; +import { InjectedTranslateProps, InjectedI18nProps } from "./props"; +import { setDefaults, setI18n } from "./context"; -export interface TranslateOptions { +export interface TranslateOptions { withRef?: boolean; bindI18n?: string; bindStore?: string; - translateFuncName?: string; + translateFuncName?: TTranslateFuncName; wait?: boolean; nsMode?: string; - i18n?: i18n; + i18n?: I18n; } -// tslint:disable-next-line:ban-types -export default function translate(namespaces?: TKey[] | TKey, options?: TranslateOptions): (WrappedComponent: C) => C; +export interface TranslateHocProps { + i18n?: I18n; + initialI18nStore?: object; + initialLanguage?: string; +} + +// Diff / Omit taken from https://github.com/Microsoft/TypeScript/issues/12215#issuecomment-311923766 +type Diff = ({[P in T]: P } & {[P in U]: never } & { [x: string]: never })[T]; +type Omit = Pick>; + +type InjectedProps = InjectedI18nProps & InjectedTranslateProps; + +export interface WrapperComponentClass

extends React.ComponentClass

{ + new (props: P, context?: any): React.Component

& { getWrappedInstance(): React.Component }; +} + +// Injects props and removes them from the prop requirements. +// Adds the new properties t (or whatever the translation function is called) and i18n if needed. +export type InferableComponentEnhancerWithProps = +

(component: React.ComponentClass

| React.StatelessComponent

) => + React.ComponentClass & TranslateHocProps>; + +export type InferableComponentEnhancerWithPropsAndRef = +

(component: React.ComponentClass

| React.StatelessComponent

) => + WrapperComponentClass, P>; + +export interface Translate { + + (namespaces?: TNamespace | TNamespace[], options?: Omit & { withRef?: false }): + InferableComponentEnhancerWithProps<"t">; + + + (namespaces?: TNamespace | TNamespace[], options?: Omit & { withRef: true }): + InferableComponentEnhancerWithPropsAndRef<"t">; + + + (namespaces?: TNamespace | TNamespace[], options?: TranslateOptions & { withRef?: false }): + InferableComponentEnhancerWithProps; + + + (namespaces?: TNamespace | TNamespace[], options?: TranslateOptions & { withRef: true }): + InferableComponentEnhancerWithPropsAndRef; + + setDefaults: typeof setDefaults; + setI18n: typeof setI18n; +} + +declare const translate: Translate; +export default translate; diff --git a/types/react-i18next/test/react-i18next-tests.tsx b/types/react-i18next/test/react-i18next-tests.tsx index 6105ccf921..5ddb17c10c 100644 --- a/types/react-i18next/test/react-i18next-tests.tsx +++ b/types/react-i18next/test/react-i18next-tests.tsx @@ -1,33 +1,54 @@ import * as React from 'react'; import * as i18n from 'i18next'; import { translate, I18nextProvider, Interpolate, InjectedTranslateProps, TranslationFunction, loadNamespaces, Trans } from 'react-i18next'; +import { InjectedI18nProps } from 'react-i18next/src/props'; interface InnerAnotherComponentProps { - _?: TranslationFunction; + _: TranslationFunction; } class InnerAnotherComponent extends React.Component { render() { - const _ = this.props._!; + const _ = this.props._; return

{_('content.text', { /* options t options */ })}

; } } -const AnotherComponent = translate('view', { wait: true, translateFuncName: '_' })(InnerAnotherComponent); +const AnotherComponent = translate('view', { wait: true, translateFuncName: '_' })(InnerAnotherComponent); +const instanceWithoutRef = new AnotherComponent({}); +instanceWithoutRef.componentWillReceiveProps!({ + i18n, + initialI18nStore: { context: { text: "a message" } }, + initialLanguage: "en" +}, {}); + +translate.setDefaults({ wait: false }); +translate.setI18n(i18n); + +const AnotherComponentWithRef = translate("view" as Key, { translateFuncName: "_", withRef: true })(InnerAnotherComponent); +const instanceWithRef = new AnotherComponentWithRef({}); +const ref = instanceWithRef.getWrappedInstance(); +instanceWithRef.componentWillReceiveProps!({ + i18n, + initialI18nStore: { context: { text: "a message" } }, + initialLanguage: "en" +}, {}); class InnerYetAnotherComponent extends React.Component { render() { - const t = this.props.t!; + const t = this.props.t; return

{t('usingDefaultNS', { /* options t options */ })}

; } } const YetAnotherComponent = translate()(InnerYetAnotherComponent); -@translate(['view', 'nav'], { wait: true }) +const YetAnotherComponentWithRef = translate(undefined, { withRef: true })(InnerYetAnotherComponent); +new YetAnotherComponentWithRef({}).getWrappedInstance(); + class TranslatableView extends React.Component { render() { - const t = this.props.t!; + const t = this.props.t; const interpolateComponent = "a interpolated component"; return ( @@ -55,12 +76,14 @@ class TranslatableView extends React.Component { } } +const TranslatedView = translate(["view", "nav"] as Key[], { wait: true })(TranslatableView); + class App extends React.Component { render() { return (
- +
); @@ -81,12 +104,34 @@ loadNamespaces({ components: [App], i18n }).then(() => { }).catch(error => { }); type Key = "view" | "nav"; -@translate(['view', 'nav']) class GenericsTest extends React.Component { render() { return null; } } -@translate('view') +const TranslatedGenericsTest = translate(["view", "nav"] as Key[])(GenericsTest); +; + class GenericsTest2 extends React.Component { render() { return null; } } + +const TranslatedGenericsTest2 = translate("view" as Key)(GenericsTest2); +; + +class ComponentWithInjectedI18n extends React.Component { + render() { return null; } +} + +const TranslatedComponentWithInjectedI18n = translate()(ComponentWithInjectedI18n); +; + +function StatlessComponent(props: InjectedTranslateProps) { + return

{props.t("hy")}

; +} + +const TranslatedStatlessComponent = translate()(StatlessComponent); +; + +interface CustomTranslateFunctionProps { + _: TranslationFunction; +}