Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
66 changes: 61 additions & 5 deletions TransWithoutContext.d.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,23 @@
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>;
type $NoInfer<T> = [T][T extends T ? 0 : never];

export type TransProps<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
Ns extends Namespace = _DefaultNamespace,
Expand All @@ -27,14 +41,56 @@ export type TransProps<
t?: TFunction<Ns, KPrefix>;
};

export function Trans<
Key extends ParseKeys<Ns, TOpt, KPrefix>,
export interface TransLegacy {
<
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;
}

export interface TransSelectorProps<
Key,
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;
> {
children?: TransChild | readonly TransChild[];
components?: readonly React.ReactElement[] | { readonly [tagName: string]: React.ReactElement };
count?: number;
context?: TContext;
defaults?: string;
i18n?: i18n;
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 interface TransSelector {
<
Target extends ConstrainTarget<TOpt>,
Key extends SelectorFn<GetSource<$NoInfer<Ns>, KPrefix>, ApplyTarget<Target, TOpt>, TOpt>,
const Ns extends Namespace = _DefaultNamespace,
KPrefix = undefined,
TContext extends string | undefined = undefined,
TOpt extends TOptions & { context?: TContext } = { context: TContext },
E = React.HTMLProps<HTMLDivElement>,
>(
props: TransSelectorProps<Key, Ns, KPrefix, TContext, TOpt> & E,
): React.ReactElement;
}

export const Trans: _EnableSelector extends true | 'optimize' ? TransSelector : TransLegacy;

export type ErrorCode =
| 'NO_I18NEXT_INSTANCE'
Expand Down
36 changes: 27 additions & 9 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,17 @@ type _DefaultNamespace = TypeOptions['defaultNS'];

export function useSSR(initialI18nStore: Resource, initialLanguage: string): void;

export interface UseTranslationOptions<KPrefix> {
type _EnableSelector = TypeOptions['enableSelector'];

export type UseTranslationOptions<KPrefix> = {
i18n?: i18n;
useSuspense?: boolean;
keyPrefix?: KPrefix;
bindI18n?: string | false;
nsMode?: 'fallback' | 'default';
lng?: string;
// other of these options might also work: https://github.com/i18next/i18next/blob/master/index.d.ts#L127
}
};

export type UseTranslationResponse<Ns extends Namespace, KPrefix> = [
t: TFunction<Ns, KPrefix>,
Expand All @@ -96,13 +98,29 @@ export type FallbackNs<Ns> = Ns extends undefined
? Ns
: _DefaultNamespace;

export function useTranslation<
const Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
const KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
>(
ns?: Ns,
options?: UseTranslationOptions<KPrefix>,
): UseTranslationResponse<FallbackNs<Ns>, KPrefix>;
export const useTranslation: _EnableSelector extends true | 'optimize'
? UseTranslationSelector
: UseTranslationLegacy;

interface UseTranslationLegacy {
<
const Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
const KPrefix extends KeyPrefix<FallbackNs<Ns>> = undefined,
>(
ns?: Ns,
options?: UseTranslationOptions<KPrefix>,
): UseTranslationResponse<FallbackNs<Ns>, KPrefix>;
}

interface UseTranslationSelector {
<
const Ns extends FlatNamespace | $Tuple<FlatNamespace> | undefined = undefined,
const KPrefix = undefined,
>(
ns?: Ns,
options?: UseTranslationOptions<KPrefix>,
): UseTranslationResponse<FallbackNs<Ns>, KPrefix>;
}

// Need to see usage to improve this
export function withSSR(): <Props>(WrappedComponent: React.ComponentType<Props>) => {
Expand Down
128 changes: 128 additions & 0 deletions test/typescript/selector-custom-types/Trans.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
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