Skip to content

Commit

Permalink
Allow for Custom DefaultNS typing (#1328)
Browse files Browse the repository at this point in the history
* Allow for Custom DefaultNS typing

* Add small usage example

* Fix warns when running jest

* Use new CustomTypeOptions type

* Update typescript and add tests for custom types

* Add deprecation warning
  • Loading branch information
JCQuintas authored Jun 14, 2021
1 parent dcfd70b commit 626cd8a
Show file tree
Hide file tree
Showing 10 changed files with 241 additions and 15 deletions.
6 changes: 3 additions & 3 deletions package-lock.json

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

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"rollup-plugin-terser": "^5.1.1",
"sinon": "^7.2.3",
"tslint": "^5.13.1",
"typescript": "^3.6.4",
"typescript": "^4.3.2",
"yargs": "13.3.0"
},
"peerDependencies": {
Expand All @@ -103,13 +103,14 @@
"build": "npm run clean && npm run build:cjs && npm run build:es && npm run build:umd && npm run build:amd && npm run copy",
"preversion": "npm run build && git push",
"postversion": "git push && git push --tags",
"pretest": "npm run test:typescript && npm run test:typescript:noninterop",
"pretest": "npm run test:typescript && npm run test:typescript:noninterop && npm run test:typescript:customtypes",
"test": "cross-env BABEL_ENV=development jest --no-cache",
"test:watch": "cross-env BABEL_ENV=development jest --no-cache --watch",
"test:coverage": "cross-env BABEL_ENV=development jest --no-cache --coverage",
"test:lint": "eslint ./src ./test",
"test:typescript": "tslint --project tsconfig.json",
"test:typescript:noninterop": "tslint --project tsconfig.nonEsModuleInterop.json",
"test:typescript:customtypes": "tslint --project ./test/typescript/custom-types/tsconfig.json",
"contributors:add": "all-contributors add",
"contributors:generate": "all-contributors generate",
"prettier": "prettier --write \"{,**/}*.{ts,tsx,js,json,md}\""
Expand All @@ -126,6 +127,9 @@
"testMatch": [
"**/test/?(*.)(spec|test).js?(x)"
],
"modulePathIgnorePatterns": [
"<rootDir>/example/"
],
"collectCoverageFrom": [
"**/src/*.{js,jsx}",
"*.macro.js",
Expand Down
66 changes: 66 additions & 0 deletions test/typescript/custom-types/Trans.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import * as React from 'react';
import { Trans } from 'react-i18next';

function defaultNamespaceUsage() {
return <Trans i18nKey="foo">foo</Trans>;
}

function namedDefaultNamespaceUsage() {
return (
<Trans ns="custom" i18nKey="foo">
foo
</Trans>
);
}

function alternateNamespaceUsage() {
return (
<Trans ns="alternate" i18nKey="baz">
foo
</Trans>
);
}

function arrayNamespace() {
return (
<Trans ns={['alternate', 'custom']} i18nKey={['alternate:baz', 'custom:bar']}>
foo
</Trans>
);
}

function expectErrorWhenNamespaceDoesNotExist() {
return (
// @ts-expect-error
<Trans ns="fake" i18nKey="foo">
foo
</Trans>
);
}

function expectErrorWhenKeyNotInNamespace() {
return (
// @ts-expect-error
<Trans ns="custom" i18nKey="fake">
foo
</Trans>
);
}

function expectErrorWhenUsingArrayNamespaceAndUnscopedKey() {
return (
// @ts-expect-error
<Trans ns={['custom']} i18nKey={['foo']}>
foo
</Trans>
);
}

function expectErrorWhenUsingArrayNamespaceAndWrongKey() {
return (
// @ts-expect-error
<Trans ns={['custom']} i18nKey={['custom:fake']}>
foo
</Trans>
);
}
47 changes: 47 additions & 0 deletions test/typescript/custom-types/Translation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import * as React from 'react';
import { Translation } from 'react-i18next';

function defaultNamespaceUsage() {
return <Translation>{(t) => <>{t('foo')}</>}</Translation>;
}

function namedDefaultNamespaceUsage() {
return <Translation ns="custom">{(t) => <>{t('foo')}</>}</Translation>;
}

function alternateNamespaceUsage() {
return <Translation ns="alternate">{(t) => <>{t('baz')}</>}</Translation>;
}

function arrayNamespace() {
return (
<Translation ns={['alternate', 'custom']}>
{(t) => (
<>
{t('alternate:baz')}
{t('custom:foo')}
</>
)}
</Translation>
);
}

function expectErrorWhenNamespaceDoesNotExist() {
// @ts-expect-error
return <Translation ns="fake">{(t) => <>{t('foo')}</>}</Translation>;
}

function expectErrorWhenKeyNotInNamespace() {
// @ts-expect-error
return <Translation ns="custom">{(t) => <>{t('fake')}</>}</Translation>;
}

function expectErrorWhenUsingArrayNamespaceAndUnscopedKey() {
// @ts-expect-error
return <Translation ns={['custom']}>{(t) => <>{t('foo')}</>}</Translation>;
}

function expectErrorWhenUsingArrayNamespaceAndWrongKey() {
// @ts-expect-error
return <Translation ns={['custom']}>{(t) => <>{t('custom:fake')}</>}</Translation>;
}
16 changes: 16 additions & 0 deletions test/typescript/custom-types/custom-types.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'react-i18next';

declare module 'react-i18next' {
interface CustomTypeOptions {
defaultNS: 'custom';
resources: {
custom: {
foo: 'foo';
bar: 'bar';
};
alternate: {
baz: 'baz';
};
};
}
}
5 changes: 5 additions & 0 deletions test/typescript/custom-types/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"extends": "../../../tsconfig.json",
"include": ["./**/*"],
"exclude": []
}
52 changes: 52 additions & 0 deletions test/typescript/custom-types/useTranslation.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';

function defaultNamespaceUsage() {
const { t } = useTranslation();

return <>{t('foo')}</>;
}

function namedDefaultNamespaceUsage() {
const [t] = useTranslation('custom');
return <>{t('bar')}</>;
}

function alternateNamespaceUsage() {
const [t] = useTranslation('alternate');
return <>{t('baz')}</>;
}

function arrayNamespace() {
const [t] = useTranslation(['alternate', 'custom']);
return (
<>
{t('alternate:baz')}
{t('custom:foo')}
</>
);
}

function expectErrorWhenNamespaceDoesNotExist() {
// @ts-expect-error
const [t] = useTranslation('fake');
return <>{t('foo')}</>;
}

function expectErrorWhenKeyNotInNamespace() {
const [t] = useTranslation('custom');
// @ts-expect-error
return <>{t('fake')}</>;
}

function expectErrorWhenUsingArrayNamespaceAndUnscopedKey() {
const [t] = useTranslation(['custom']);
// @ts-expect-error
return <>{t('foo')}</>;
}

function expectErrorWhenUsingArrayNamespaceAndWrongKey() {
const [t] = useTranslation(['custom']);
// @ts-expect-error
return <>{t('custom:fake')}</>;
}
8 changes: 4 additions & 4 deletions test/typescript/withTranslation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { withTranslation, WithTranslation } from 'react-i18next';
import { default as myI18n } from './i18n';

/**
* @see https://react.i18next.com/latest/trans-component
* see https://react.i18next.com/latest/trans-component
*/

interface MyComponentProps extends WithTranslation {
Expand Down Expand Up @@ -34,7 +34,7 @@ function defaultUsageWithDefaultProps() {
}

/**
* @see https://react.i18next.com/latest/withtranslation-hoc#withtranslation-params
* see https://react.i18next.com/latest/withtranslation-hoc#withtranslation-params
*/
function withNs() {
const ExtendedComponent = withTranslation('ns')(MyComponent);
Expand All @@ -47,15 +47,15 @@ function withNsArray() {
}

/**
* @see https://react.i18next.com/latest/withtranslation-hoc#overriding-the-i-18-next-instance
* see https://react.i18next.com/latest/withtranslation-hoc#overriding-the-i-18-next-instance
*/
function withI18nOverride() {
const ExtendedComponent = withTranslation('ns')(MyComponent);
return <ExtendedComponent bar="baz" i18n={myI18n} />;
}

/**
* @see https://react.i18next.com/latest/withtranslation-hoc#not-using-suspense
* see https://react.i18next.com/latest/withtranslation-hoc#not-using-suspense
*/
function withSuspense() {
const ExtendedComponent = withTranslation('ns')(MyComponent);
Expand Down
46 changes: 41 additions & 5 deletions ts4.1/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,45 @@ type Subtract<T extends K, K> = Omit<T, keyof K>;

/**
* This interface can be augmented by users to add types to `react-i18next` default resources.
*
* @deprecated use the `resources` key of `CustomTypeOptions` instead
*/
export interface Resources {}
/**
* This interface can be augmented by users to add types to `react-i18next`. It accepts a `defaultNS` and `resources` properties.
*
* Usage:
* ```ts
* // react-i18next.d.ts
* import 'react-i18next';
* declare module 'react-i18next' {
* interface CustomTypeOptions {
* defaultNS: 'custom';
* resources: {
* custom: {
* foo: 'foo';
* };
* };
* }
* }
* ```
*/
export interface CustomTypeOptions {}

type MergeBy<T, K> = Omit<T, keyof K> & K;

type Fallback<F, T = keyof Resources> = [T] extends [never] ? F : T;
type TypeOptions = MergeBy<
{
defaultNS: 'translation';
resources: Resources;
},
CustomTypeOptions
>;

type DefaultResources = TypeOptions['resources'];
type DefaultNamespace<T = TypeOptions['defaultNS']> = T extends Fallback<string> ? T : string;

type Fallback<F, T = keyof DefaultResources> = [T] extends [never] ? F : T;

export type Namespace<F = Fallback<string>> = F | F[];

Expand Down Expand Up @@ -90,13 +125,16 @@ type NormalizeMultiReturn<T, V> = V extends `${infer N}:${infer R}`
: never
: never;

export type TFuncKey<N extends Namespace = DefaultNamespace, T = Resources> = N extends (keyof T)[]
export type TFuncKey<
N extends Namespace = DefaultNamespace,
T = DefaultResources
> = N extends (keyof T)[]
? NormalizeMulti<T, N[number]>
: N extends keyof T
? Normalize<T[N]>
: string;

export type TFuncReturn<N, TKeys, TDefaultResult, T = Resources> = N extends (keyof T)[]
export type TFuncReturn<N, TKeys, TDefaultResult, T = DefaultResources> = N extends (keyof T)[]
? NormalizeMultiReturn<T, TKeys>
: N extends keyof T
? NormalizeReturn<T[N], TKeys>
Expand Down Expand Up @@ -158,8 +196,6 @@ type UseTranslationResponse<N extends Namespace> = [TFunction<N>, i18n, boolean]
ready: boolean;
};

type DefaultNamespace<T = 'translation'> = T extends Fallback<string> ? T : string;

export function useTranslation<N extends Namespace = DefaultNamespace>(
ns?: N,
options?: UseTranslationOptions,
Expand Down
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"allowSyntheticDefaultImports": true
},
"include": ["./src/**/*", "./test/**/*"],
"exclude": ["test/typescript/nonEsModuleInterop/**/*.ts"]
"exclude": ["test/typescript/nonEsModuleInterop/**/*.ts", "test/typescript/custom-types"]
}

0 comments on commit 626cd8a

Please sign in to comment.