Skip to content
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

Take fallbackNs into consideration for TFunction key type check #1268

Closed
Haaxor1689 opened this issue Mar 2, 2021 · 11 comments
Closed

Take fallbackNs into consideration for TFunction key type check #1268

Haaxor1689 opened this issue Mar 2, 2021 · 11 comments
Assignees

Comments

@Haaxor1689
Copy link

Haaxor1689 commented Mar 2, 2021

🐛 Bug Report

When using the method of redeclaring Resources to provide type check for TFunction the generated keys also contain namespace that is not a fallback one which results in typescript being ok with a key that doesn't resolve into a translation.

To Reproduce

const resources = {
	sk: {
		foo: {
			a: 'A',
		},
		goo: {
			b: 'B',
		},
	},
};

i18next.use(initReactI18next).init({
	resources,
	ns: Object.keys(resources.sk),
	fallbackNS: 'foo',
        // ...
});

declare module 'react-i18next' {
	type DefaultResources = typeof resources['sk'];
	// eslint-disable-next-line
	interface Resources extends DefaultResources {}
}

With this setup none of these usages show a type error:

const { t } = useTranslation();
t('a') // -> "A", OK
t('b') // -> "b", Wrong, prints the key
t('foo:a') // -> "A", OK
t('goo:b') // -> "B", OK

Another issue with key type check arises when you specify a namespace in useTranslate hook.

const { t } = useTranslation('goo');
t('a') // -> "A", Wrong, shows a type error
t('b') // -> "B", OK
t('foo:a') // -> "A", Wrong, shows a type error
t('goo:b') // -> "B", Wrong, shows a type error

Expected behavior

Add option to also specify which of the namespaces are also fallback namespaces so keys of non fallback namespaces don't pass through type check in TFunction.

const { t } = useTranslation();
t('b') // -> Should show type error
const { t } = useTranslation('goo');
t('a') // -> Shouldn't show a type error
t('foo:a') // -> Shouldn't show a type error
t('goo:b') // -> Shouldn't show a type error

Your Environment

  • i18next version: 19.9.1
  • react-i18next version: 11.8.8
  • typescript version: 4.1.2
@adrai adrai assigned adrai and pedrodurek and unassigned adrai Mar 2, 2021
@pedrodurek
Copy link
Member

pedrodurek commented Mar 2, 2021

Hey @Haaxor1689, thanks for pointing that out.
Regarding the first case (without specifying a namespace), if the default namespace is different than translation, you must override it like this (for now):

const { t } = useTranslation<'foo'>();

I'll be adding that to the documentation.

Regarding the second case, you don't need to override the DefaultNamespace.

const { t } = useTranslation('goo');

And it's behaving as expected, it'll accept t('b') only.

I've created this example to illustrate that.

@Haaxor1689
Copy link
Author

The provided example is missing the fallbackNS in the config, that's why it isn't working.

@pedrodurek
Copy link
Member

Ohh sorry, I fixed it, thanks! The idea of this example is to show the types working 😄

@Haaxor1689
Copy link
Author

Haaxor1689 commented Mar 2, 2021

Few more issues:

  • you are using lng: "en" while the resources are using the sk language key
  • you are missing the import "./i18n/config"; in index/App file

After that is fixed you can see that everything in App.tx:7 console.log is translated while typescript is reporting an error.

@Haaxor1689
Copy link
Author

Haaxor1689 commented Mar 2, 2021

Also now that you mention it, this should also not give an type error since it gets translated.

// "foo" is a `defaultNS` in this case
const { t } = useTranslation<"foo">();
t("foo:a");

@pedrodurek
Copy link
Member

Hey @Haaxor1689, when relying on type augmentation to infer keys and return type, this format will only work when loading multiple namespaces like this (array format): const { t } = useTranslation(['foo']), I explained the reason for that here: #1222 (comment)

@pedrodurek
Copy link
Member

Few more issues:

  • you are using lng: "en" while the resources are using the sk language key
  • you are missing the import "./i18n/config"; in index/App file

After that is fixed you can see that everything in App.tx:7 console.log is translated while typescript is reporting an error.

I updated the example, thanks!

@Haaxor1689
Copy link
Author

Haaxor1689 commented Mar 2, 2021

Ok I think my use case was not following the ideology of how the namespaces should be used. By specifying useTranslation('foo') you are saying you will only be using keys from foo namespace and none other. Therefore defaultNS should just mean useTranslation(ns: string = defaultNS).

If this is the case then those type checks giving errors are correct even though the translations are found during runtime.

Still there is the fallbackNS which is supposed to be used so you can have let's say a setup like this

const resources = {
	en: {
		moduleA: { keyA: '...' },
		moduleB: { keyB: '...' },
		common: { cancel: '...' },
	}
}

i18next.use(initReactI18next).init({
	resources,
	lng: 'en',

	ns: Object.keys(resources.en),
	fallbackNS: 'common',
});

// Component in "module a"
const ComponentA = () => {
	const { t } = useTranslation('moduleA');
	console.log(t('keyA'), t('cancel'));
}

// Component in "module b"
const ComponentB = () => {
	const { t } = useTranslation('moduleB');
	console.log(t('keyB'), t('cancel'));
}

Is that correct?

If so then keys from fallbackNS module(s) should always show up in the t key type check in a non namespace prefixed form (so cancel instead of common:cancel from the snippet).

@pedrodurek
Copy link
Member

Hey @Haaxor1689, sorry for the delayed response, it's been a very busy week. Basically, by relying on the typescript to infer the keys and return types, you wouldn't need a fallbackNS (if you're not retrieving translations from the server), but if you really want to use fallbackNS you can use the following approach:

import 'react-i18next';
import moduleA from 'locales/en/moduleA.json';
import moduleB from 'locales/en/moduleB.json';
import common from 'locales/en/common.json';

declare module 'react-i18next' {
  interface Resources {
    moduleA: typeof moduleA & typeof common;
    moduleB: typeof moduleB & typeof common;
    common: typeof common;
  }
}

@Haaxor1689
Copy link
Author

Haaxor1689 commented Mar 10, 2021

Hey @Haaxor1689, sorry for the delayed response, it's been a very busy week. Basically, by relying on the typescript to infer the keys and return types, you wouldn't need a fallbackNS (if you're not retrieving translations from the server), but if you really want to use fallbackNS you can use the following approach:

import 'react-i18next';
import moduleA from 'locales/en/moduleA.json';
import moduleB from 'locales/en/moduleB.json';
import common from 'locales/en/common.json';

declare module 'react-i18next' {
  interface Resources {
    moduleA: typeof moduleA & typeof common;
    moduleB: typeof moduleB & typeof common;
    common: typeof common;
  }
}

This workaround works like a charm :)
Here is a small helper I made to make the code cleaner:

type MapFallback<T, Fallback extends keyof T> = {
  [key in keyof T]: T[key] & T[Fallback];
};
declare module 'react-i18next' {
  // eslint-disable-next-line
  interface Resources extends MapFallback<typeof resources['en'], 'common'> {}
}

@vricop
Copy link

vricop commented Mar 19, 2022

Hey @Haaxor1689, thanks for pointing that out. Regarding the first case (without specifying a namespace), if the default namespace is different than translation, you must override it like this (for now):

const { t } = useTranslation<'foo'>();

I'll be adding that to the documentation.

Regarding the second case, you don't need to override the DefaultNamespace.

const { t } = useTranslation('goo');

And it's behaving as expected, it'll accept t('b') only.

I've created this example to illustrate that.

But this has an unexpected behavior when removing the default namespace from the t() function. As you can see in the next screenshot now keys are show with & without namespaces. So they're duplicated.

Screenshot 2022-03-19 at 13 22 33

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants