react-i18next
react-i18next copied to clipboard
Typescript safer interpolation
🚀 Feature Proposal
The goal here is to improve the type the TFunction so that if we have variables in our translation file, the TFunction is aware of it and help prevent missing keys or extra keys.
Motivation
Let's say we have this json file:
{
"key1": "text",
"key2": "text with {{var}}"
}
Then if we do
t("key1") // works
t("key1", { unknown: "fail plz" }) // fails
t("key2") // fails
t("key2", { var: "cool" }) // works
Additional context:
I am aware that is could be a dupe of https://github.com/i18next/react-i18next/issues/1423. But I don't think the lib should wait for the feature to be implemented because there is workarounds for it. In our project, we automatically type the json by generating an associated .d.ts with the proper type.
So the json in our example has the following type:
declare const $defaultExport: {
"key1": "text",
"key2": "text with {{var}}"
}
So the type DefaultResources = TypeOptions['resources'];
has more informations and we should use that.
Proposal:
The previous definition was:
export interface TFunction<N extends Namespace = DefaultNamespace, TKPrefix = undefined> {
<
TKeys extends TFuncKey<N, TKPrefix> | TemplateStringsArray extends infer A ? A : never,
TDefaultResult extends TFunctionResult | React.ReactNode = string,
TInterpolationMap extends object = StringMap
>(
key: TKeys | TKeys[],
options?: TOptions<TInterpolationMap> | string,
): TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>;
<
TKeys extends TFuncKey<N, TKPrefix> | TemplateStringsArray extends infer A ? A : never,
TDefaultResult extends TFunctionResult | React.ReactNode = string,
TInterpolationMap extends object = StringMap
>(
key: TKeys | TKeys[],
defaultValue?: string,
options?: TOptions<TInterpolationMap> | string,
): TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>;
}
And the new one is:
type Keys<S extends string> = S extends '' ? [] :
S extends `${infer _}{{${infer B}}}${infer C}` ? [B, ...Keys<C>] : []
export interface TFunction<N extends Namespace = DefaultNamespace, TKPrefix = undefined> {
<
TKeys extends TFuncKey<N, TKPrefix> | TemplateStringsArray extends infer A ? A : never,
TDefaultResult extends TFunctionResult | React.ReactNode = string,
>(
key: TKeys | TKeys[],
...defaultValueOrOptions: Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number] extends never ? ([] | [TOptionsBase | string] | [string, TOptionsBase | string]) : [TOptions<Record<Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number], string | number>>] | [string, TOptions<Record<Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number], string | number>>]
): TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>;
}
Let's analyse the changes here:
Keys<S>
is here to get the value of variables in the string. So Keys<"string">
is []
and Keys<"{{var}} foo">
is ["var"]
.
Keys<"{{var1}} bar {{var2}}">[number]
is "var1" | "var2"
. Keys<"string">[number]
is never
.
We changed TInterpolationMap
to Record<Keys<DefaultResources[N][TKeys]>[number], string | number>
.
The type we want for TInterpolationMap
is a map { variables: value }
. To get the variables we need to use Keys<S>
on the string value. The string value is the current implementation of TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>
. If we change the TFuncReturn value to interpolate the result like the Interpolate in this SO , this need to change.
We directly use Record<Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number], string | number>
instead of TInterpolationMap extends to avoid the possible extra keys.
Since now the argument of the TFunction is optional based on the generics, I did not find a better way that to use spread operator. (SO) leading to a less readable type.
Let's analyse it:
...defaultValueOrOptions: Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number] extends never ? ([] | [TOptionsBase | string] | [string, TOptionsBase]) : [TOptions<Record<Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number], string | number>> | string] | [string, TOptions<Record<Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number], string | number>> | string]
Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number] extends never
is a check to know if there is any variables. If there is no variables:
defaultValueOrOptions can be
- no args: []
- one arg: the base options with no extra key or default value.
- two args: the default value and the base options with no extra keys.
If there is variables, the defaultValueOrOptions can be
- one arg: the base options with the variables map (
TOptions<Record<Keys<TFuncReturn<N, TKeys, TDefaultResult, TKPrefix>>[number], string | number>>
) - two args: the default value and the options with the variables map.
I did not test what this code will result if the json is not typed with real string. But maybe it could be something like:
type Keys<S extends string> = string extends S ? string[] : S extends '' ? [] :
S extends `${infer _}{{${infer B}}}${infer C}` ? [B, ...Keys<C>] : []
In our project, we don't use nested keys so I did not test that.
Please don't hesitate to point out any mistakes or improvement. I would love to have this changes integrated or have a better support with a fully typed json.
Great proposal !
I would just add that the variable interpolation token {{
/}}
can be customized in the config interpolation
entry using prefix
/suffix
keys so it should be also supported in the typing.
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.
should this issue be reopened? Looks like very cool proposal!
should this issue be reopened? Looks like very cool proposal!
@pedrodurek ok if we open this again?
@adrai yes, we should keep it open!
This would be really cool.
I would like to contribute to this. Was trying to implement this myself, but overwriting type of t returned from the hook is the pain in the ass.
@pedrodurek is currently working on a different TS approach
I'm curretly running into problems, when updating @types/react to the latest version, as these now get checked and will fail the type check.
See https://github.com/flathub/website/pull/843
Please try with i18next v23.0.1 and react-i18next v13.0.0
should work.... proof here: https://github.com/locize/i18next-typescript-examples/blob/main/2/index.ts
https://github.com/i18next/i18next/issues/1953#issuecomment-1601317429
Should this also work with <Trans>
or what do I need to do to allow that? https://github.com/flathub/website/actions/runs/5349795927/jobs/9701520404?pr=843
@razzeee I don't think that is related to this interpolation issue... please isolate your problem and create a minimal reproducible example repo and open a new issue.
I suspect having something like this would also error the same way (also without Trans):
<span>hello {{ id: appId }} you</span>