Skip to content

feat: adds support for new "selector" API #1852

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
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
86 changes: 77 additions & 9 deletions TransWithoutContext.d.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,80 @@
import type { i18n, ParseKeys, Namespace, TypeOptions, TOptions, TFunction } from 'i18next';
import type {
i18n,
ApplyTarget,
ConstrainTarget,
GetSource,
ParseKeys,
Namespace,
SelectorFn,
TypeOptions,
TOptions,
TFunction,
} from 'i18next';
import * as React from 'react';

type _DefaultNamespace = TypeOptions['defaultNS'];
type _EnableSelector = TypeOptions['enableSelector'];

type TransChild = React.ReactNode | Record<string, unknown>;
export type TransProps<
Key extends ParseKeys<Ns, TOpt, KPrefix>,

/**
* This type functionally replaces {@link TransProps}, and should replace
* it when cut over from the string-based to the selector API
* (tentatively v27).
*
* So if you depend on this type directly, just be aware that it will be
* renamed to `TransProps` in a future major release.
*/
interface TransPropsInterface<
Key,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
E = React.HTMLProps<HTMLDivElement>,
> = E & {
> {
children?: TransChild | readonly TransChild[];
components?: readonly React.ReactElement[] | { readonly [tagName: string]: React.ReactElement };
count?: number;
context?: TContext;
defaults?: string;
i18n?: i18n;
i18nKey?: Key | Key[];
i18nKey?: Key;
ns?: Ns;
parent?: string | React.ComponentType<any> | null; // used in React.createElement if not null
tOptions?: TOpt;
values?: {};
shouldUnescape?: boolean;
t?: TFunction<Ns, KPrefix>;
};
}

export function Trans<
export type GetTransProps<
Target extends ConstrainTarget<TOpt>,
Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
E = React.HTMLProps<HTMLDivElement>,
>(props: TransProps<Key, Ns, KPrefix, TContext, TOpt, E>): React.ReactElement;
> = E &
TransPropsInterface<
_EnableSelector extends true
? SelectorFn<GetSource<NoInfer<Ns>, KPrefix>, ApplyTarget<Target, TOpt>, TOpt>
: Key | Key[],
Ns,
KPrefix,
TContext,
TOpt
>;

export declare function Trans<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
const Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
E = React.HTMLProps<HTMLDivElement>,
Target extends ConstrainTarget<TOpt> = ConstrainTarget<TOpt>,
>(props: GetTransProps<Target, Key, Ns, KPrefix, TContext, TOpt, E>): React.ReactElement;

export type ErrorCode =
| 'NO_I18NEXT_INSTANCE'
Expand Down Expand Up @@ -71,3 +111,31 @@ export type ErrorMeta = {
* ```
*/
export type ErrorArgs = readonly [string, ErrorMeta | undefined, ...any[]];

/**
* This type left here in case anyone in userland depends on it.
* It's no longer used internally, and can/should be removed when
* we cut over to the selector API (tentatively v27)
*/
export type TransProps<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
E = React.HTMLProps<HTMLDivElement>,
> = E & {
children?: TransChild | readonly TransChild[];
components?: readonly React.ReactElement[] | { readonly [tagName: string]: React.ReactElement };
count?: number;
context?: TContext;
defaults?: string;
i18n?: i18n;
i18nKey?: Key | Key[];
ns?: Ns;
parent?: string | React.ComponentType<any> | null; // used in React.createElement if not null
tOptions?: TOpt;
values?: {};
shouldUnescape?: boolean;
t?: TFunction<Ns, KPrefix>;
};
125 changes: 125 additions & 0 deletions test/typescript/selector-custom-types/Trans.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { describe, it, expectTypeOf } from 'vitest';
import * as React from 'react';
import { Trans, useTranslation } from 'react-i18next';

describe('<Trans />', () => {
describe('default namespace', () => {
it('standard usage', () => {
<Trans i18nKey={($) => (expectTypeOf($.foo).toMatchTypeOf<'foo'>, $.foo)} />;
});

it(`raises a TypeError given a key that doesn't exist`, () => {
// @ts-expect-error
<Trans i18nKey={($) => $.Nope} />;
});
});

describe('named namespace', () => {
it('standard usage', () => {
<Trans ns="custom" i18nKey={($) => (expectTypeOf($.foo).toEqualTypeOf<'foo'>, $.foo)} />;
});

it(`raises a TypeError given a namespace that doesn't exist`, () => {
expectTypeOf<React.ComponentProps<typeof Trans>>()
.toHaveProperty('ns')
.extract<'Nope'>()
// @ts-expect-error
.toMatchTypeOf<'Nope'>();
});
});

describe('array namespace', () => {
it('should work with array namespace', () => (
<>
<Trans
ns={['alternate', 'custom']}
i18nKey={($) => (expectTypeOf($.baz).toEqualTypeOf<'baz'>(), $.baz)}
/>
<Trans
ns={['alternate', 'custom']}
i18nKey={($) => (expectTypeOf($.alternate.baz).toEqualTypeOf<'baz'>(), $.baz)}
/>
<Trans
ns={['alternate', 'custom']}
i18nKey={($) => (expectTypeOf($.custom.bar).toEqualTypeOf<'bar'>(), $.custom.bar)}
/>
<Trans
ns={['custom', 'alternate']}
i18nKey={($) => (expectTypeOf($.alternate.baz).toEqualTypeOf<'baz'>(), $.alternate.baz)}
/>
<Trans
ns={['custom', 'alternate']}
i18nKey={($) => (expectTypeOf($.bar).toEqualTypeOf<'bar'>(), $.bar)}
/>
<Trans
ns={['custom', 'alternate']}
i18nKey={($) => (expectTypeOf($.custom.bar).toEqualTypeOf<'bar'>(), $.bar)}
/>
<Trans
ns={['custom', 'alternate']}
i18nKey={($) => (
expectTypeOf($.alternate.foobar.deep.deeper.deeeeeper).toEqualTypeOf<'foobar'>(),
$.alternate.foobar.deep.deeper.deeeeeper
)}
/>
</>
));

it(`raises a TypeError given a key that's not present inside any namespace`, () => {
// @ts-expect-error
<Trans ns={['alternate', 'custom']} i18nKey={($) => $.bar} />;
// @ts-expect-error
<Trans ns={['alternate', 'custom']} i18nKey={($) => $.custom.baz} />;
});
});

describe('usage with `t` function', () => {
it('should work when providing `t` function', () => {
const { t } = useTranslation('alternate');
<Trans
t={t}
i18nKey={($) => (expectTypeOf($.foobar.barfoo).toEqualTypeOf<'barfoo'>(), $.foobar.barfoo)}
/>;
});

it('should work when providing `t` function with a prefix', () => {
const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' });
<Trans
t={t}
i18nKey={($) => (
expectTypeOf($.deeper.deeeeeper).toEqualTypeOf<'foobar'>(), $.deeper.deeeeeper
)}
/>;
});

it('raises a TypeError given a key-prefixed `t` function and an invalid key', () => {
const { t } = useTranslation('alternate', { keyPrefix: 'foobar.deep' });
// @ts-expect-error
<Trans t={t} i18nKey={($) => $.xxx} />;
});
});

describe('interpolation', () => {
it('should work with text and interpolation', () => {
expectTypeOf(Trans).toBeCallableWith({
children: <>foo {{ var: '' }}</>,
});
});

it('should work with Interpolation in HTMLElement', () => {
expectTypeOf(Trans).toBeCallableWith({
children: (
<>
foo <strong>{{ var: '' }}</strong>
</>
),
});
});

it('should work with text and interpolation as children of an HTMLElement', () => {
expectTypeOf(Trans).toBeCallableWith({
children: <span>foo {{ var: '' }}</span>,
});
});
});
});
Loading
Loading