Skip to content

Commit

Permalink
fix issue with key prefixes (#2105)
Browse files Browse the repository at this point in the history
Co-authored-by: Alina Saalfeld <alina.saalfeld@binary-butterfly.de>
  • Loading branch information
Gotos and Alina Saalfeld committed Jan 3, 2024
1 parent 0424076 commit 32c69e1
Show file tree
Hide file tree
Showing 4 changed files with 105 additions and 71 deletions.
19 changes: 18 additions & 1 deletion test/typescript/array-access/i18next.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import 'i18next';

declare module 'i18next' {
interface CustomTypeOptions {
defaultNS: 'main';
defaultNS: 'prefix';
resources: {
main: {
arrayOfStrings: ['zero', 'one'];
Expand All @@ -23,6 +23,23 @@ declare module 'i18next' {
];
};

prefix: {
greeting: string;
timeOfDay: {
morning: string;
afternoon: string;
};
parent: {
parent: string;
other: string;
};
deep: {
deep: {
deep: string;
};
};
};

ord: {
ord: [
{
Expand Down
137 changes: 83 additions & 54 deletions test/typescript/array-access/t.test.ts
Original file line number Diff line number Diff line change
@@ -1,76 +1,105 @@
import { describe, it, expectTypeOf, assertType } from 'vitest';
import { TFunction } from 'i18next';
import { KeyPrefix, Namespace, TFunction } from 'i18next';

describe('t', () => {
describe('main', () => {
const t = (() => '') as TFunction<['main']>;
describe('main', () => {
const t = (() => '') as TFunction<['main']>;

it('works with simple usage', () => {
expectTypeOf(t('arrayOfStrings.0')).toEqualTypeOf<'zero'>();
expectTypeOf(t('arrayOfStrings.1')).toEqualTypeOf<'one'>();
it('works with simple usage', () => {
expectTypeOf(t('arrayOfStrings.0')).toEqualTypeOf<'zero'>();
expectTypeOf(t('arrayOfStrings.1')).toEqualTypeOf<'one'>();

expectTypeOf(t('readonlyArrayOfStrings.0')).toEqualTypeOf<'readonly zero'>();
expectTypeOf(t('readonlyArrayOfStrings.1')).toEqualTypeOf<'readonly one'>();
expectTypeOf(t('readonlyArrayOfStrings.0')).toEqualTypeOf<'readonly zero'>();
expectTypeOf(t('readonlyArrayOfStrings.1')).toEqualTypeOf<'readonly one'>();

expectTypeOf(t('arrayOfObjects.0.foo')).toEqualTypeOf<'bar'>();
expectTypeOf(t('arrayOfObjects.1.fizz')).toEqualTypeOf<'buzz'>();
expectTypeOf(t('arrayOfObjects.0.foo')).toEqualTypeOf<'bar'>();
expectTypeOf(t('arrayOfObjects.1.fizz')).toEqualTypeOf<'buzz'>();

expectTypeOf(t('arrayOfObjects.2.0.test')).toEqualTypeOf<'success'>();
expectTypeOf(t('arrayOfObjects.2.0.sub.deep')).toEqualTypeOf<'still success'>();
});
expectTypeOf(t('arrayOfObjects.2.0.test')).toEqualTypeOf<'success'>();
expectTypeOf(t('arrayOfObjects.2.0.sub.deep')).toEqualTypeOf<'still success'>();
});

it('should throw an error when key is not present', () => {
// @ts-expect-error expected error
assertType(t('arrayOfStrings.2'));
it('should throw an error when key is not present', () => {
// @ts-expect-error expected error
assertType(t('arrayOfStrings.2'));

// @ts-expect-error expected error
assertType(t('arrayOfObjects.0.food'));
// @ts-expect-error expected error
assertType(t('arrayOfObjects.0.fizz'));
// @ts-expect-error expected error
assertType(t('arrayOfObjects.0.food'));
// @ts-expect-error expected error
assertType(t('arrayOfObjects.0.fizz'));

// @ts-expect-error expected error
assertType(t('arrayOfObjects.2'));
// @ts-expect-error expected error
assertType(t('arrayOfObjects.2'));

// @ts-expect-error expected error
assertType(t('arrayOfObjects.2.bar'));
// @ts-expect-error expected error
assertType(t('arrayOfObjects.2.sub.deep'));
// @ts-expect-error expected error
assertType(t('arrayOfObjects.2.test'));
});
// @ts-expect-error expected error
assertType(t('arrayOfObjects.2.bar'));
// @ts-expect-error expected error
assertType(t('arrayOfObjects.2.sub.deep'));
// @ts-expect-error expected error
assertType(t('arrayOfObjects.2.test'));
});

it('should work with `returnObjects`', () => {
expectTypeOf(t('arrayOfStrings', { returnObjects: true })).toBeArray();
expectTypeOf(t('arrayOfObjects', { returnObjects: true })).toEqualTypeOf<
[{ foo: 'bar' }, { fizz: 'buzz' }, [{ test: 'success'; sub: { deep: 'still success' } }]]
>();
expectTypeOf(t('arrayOfObjects.0', { returnObjects: true })).toEqualTypeOf<{ foo: 'bar' }>();
});
it('should work with `returnObjects`', () => {
expectTypeOf(t('arrayOfStrings', { returnObjects: true })).toBeArray();
expectTypeOf(t('arrayOfObjects', { returnObjects: true })).toEqualTypeOf<
[{ foo: 'bar' }, { fizz: 'buzz' }, [{ test: 'success'; sub: { deep: 'still success' } }]]
>();
expectTypeOf(t('arrayOfObjects.0', { returnObjects: true })).toEqualTypeOf<{ foo: 'bar' }>();
});

it('should work with const keys', () => {
const alternateTranslationKeys = ['arrayOfStrings.0', 'arrayOfObjects.0.foo'] as const;
it('should work with const keys', () => {
const alternateTranslationKeys = ['arrayOfStrings.0', 'arrayOfObjects.0.foo'] as const;

const result = alternateTranslationKeys.map((value) => t(value));
const result = alternateTranslationKeys.map((value) => t(value));

assertType<string[]>(result);
});
assertType<string[]>(result);
});
});

it('should work with context', () => {
const t = (() => '') as TFunction<'ctx'>;
it('should work with context', () => {
const t = (() => '') as TFunction<'ctx'>;

expectTypeOf(t('dessert.0.dessert', { context: 'cake' })).toEqualTypeOf<'a nice cake'>();
expectTypeOf(t('dessert.0.dessert', { context: 'cake' })).toEqualTypeOf<'a nice cake'>();

// context + plural
expectTypeOf(t('dessert.0.dessert', { context: 'muffin', count: 3 })).toMatchTypeOf<string>();
});
// context + plural
expectTypeOf(t('dessert.0.dessert', { context: 'muffin', count: 3 })).toMatchTypeOf<string>();
});

it('should process ordinal plurals', () => {
const t = (() => '') as TFunction<'ord'>;
it('should process ordinal plurals', () => {
const t = (() => '') as TFunction<'ord'>;

expectTypeOf(t('ord.0.place', { ordinal: true, count: 1 })).toBeString();
expectTypeOf(t('ord.0.place', { ordinal: true, count: 2 })).toBeString();
expectTypeOf(t('ord.0.place', { ordinal: true, count: 3 })).toBeString();
expectTypeOf(t('ord.0.place', { ordinal: true, count: 4 })).toBeString();
});

describe("don't break prefixes", () => {
it('does not allow access to morning', () => {
const t = (() => '') as TFunction<'prefix', 'deep'>;
expectTypeOf(t('deep.deep')).toEqualTypeOf<string>();

// @ts-expect-error expected error
assertType(t('morning'));
});

expectTypeOf(t('ord.0.place', { ordinal: true, count: 1 })).toBeString();
expectTypeOf(t('ord.0.place', { ordinal: true, count: 2 })).toBeString();
expectTypeOf(t('ord.0.place', { ordinal: true, count: 3 })).toBeString();
expectTypeOf(t('ord.0.place', { ordinal: true, count: 4 })).toBeString();
it('should work with useTranslation', () => {
const useTranslation = <Ns extends Namespace, KPrefix extends KeyPrefix<Ns> = undefined>(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_: Ns,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
__: {
keyPrefix?: KPrefix;
},
): {
t: TFunction<Ns, KPrefix>;
// @ts-expect-error we only care about typing here, not about actual returns
} => undefined;

const use = useTranslation('prefix', { keyPrefix: 'deep' });

expectTypeOf(use).toEqualTypeOf<{ t: TFunction<'prefix', 'deep'> }>();

// @ts-expect-error expected error
assertType(use.t('morning'));
});
});
10 changes: 2 additions & 8 deletions typescript/t.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,15 @@ type AppendNamespace<Ns, Keys> = `${Ns & string}${_NsSeparator}${Keys & string}`
* Build all keys and key prefixes based on Resources *
***************************************************** */
type KeysBuilderWithReturnObjects<Res, Key = keyof Res> = Key extends keyof Res
? Res[Key] extends $Dictionary
?
| JoinKeys<Key, WithOrWithoutPlural<keyof $OmitArrayKeys<Res[Key]>>>
| JoinKeys<Key, KeysBuilderWithReturnObjects<Res[Key]>>
: Res[Key] extends readonly unknown[]
? Res[Key] extends $Dictionary | readonly unknown[]
?
| JoinKeys<Key, WithOrWithoutPlural<keyof $OmitArrayKeys<Res[Key]>>>
| JoinKeys<Key, KeysBuilderWithReturnObjects<Res[Key]>>
: never
: never;

type KeysBuilderWithoutReturnObjects<Res, Key = keyof $OmitArrayKeys<Res>> = Key extends keyof Res
? Res[Key] extends $Dictionary
? JoinKeys<Key, KeysBuilderWithoutReturnObjects<Res[Key]>>
: Res[Key] extends readonly unknown[]
? Res[Key] extends $Dictionary | readonly unknown[]
? JoinKeys<Key, KeysBuilderWithoutReturnObjects<Res[Key]>>
: Key
: never;
Expand Down
10 changes: 2 additions & 8 deletions typescript/t.v4.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,21 +50,15 @@ type AppendNamespace<Ns, Keys> = `${Ns & string}${_NsSeparator}${Keys & string}`
* Build all keys and key prefixes based on Resources *
***************************************************** */
type KeysBuilderWithReturnObjects<Res, Key = keyof Res> = Key extends keyof Res
? Res[Key] extends $Dictionary
?
| JoinKeys<Key, WithOrWithoutPlural<keyof $OmitArrayKeys<Res[Key]>>>
| JoinKeys<Key, KeysBuilderWithReturnObjects<Res[Key]>>
: Res[Key] extends readonly unknown[]
? Res[Key] extends $Dictionary | readonly unknown[]
?
| JoinKeys<Key, WithOrWithoutPlural<keyof $OmitArrayKeys<Res[Key]>>>
| JoinKeys<Key, KeysBuilderWithReturnObjects<Res[Key]>>
: never
: never;

type KeysBuilderWithoutReturnObjects<Res, Key = keyof $OmitArrayKeys<Res>> = Key extends keyof Res
? Res[Key] extends $Dictionary
? JoinKeys<Key, KeysBuilderWithoutReturnObjects<Res[Key]>>
: Res[Key] extends readonly unknown[]
? Res[Key] extends $Dictionary | readonly unknown[]
? JoinKeys<Key, KeysBuilderWithoutReturnObjects<Res[Key]>>
: Key
: never;
Expand Down

0 comments on commit 32c69e1

Please sign in to comment.