i18n and typescript
Intro
I am using typescript and i18next for our project, I would like to summarize the experience of strongly applying the type check of the i18n JSON file.
About I18n
First, the following is how i18n was being used.
i18n is used to display text in multiple languages on the same webpage, meaning to display it in a language with one key, multiple values.
const i18nJSON = {
'simple-example': 'This is example text',
'values-example': 'I need to show {0} value',
'line-break-example': 'Hello. \n I am FE developer'
} as const
First, there is an object in the form of Key-Value as above. The object may be a JSON or an object of a typescript.
import tFunction from 'utils';
tFunction('simple-example') // This is example text
tFunction('values-example', [15]) // I need to show 15 value
tFunction('line-break-example') // Hello <br /> I am FE developer
Second, tFunction
is used to insert the i18n key to obtain a string that fits the key.
In some cases, the string including the value that fits each text may be returned by inserting a variable such as values-example
.
In the last line-break-example
, the \n
line break character is converted to the <br/>
tag so that it can be line break on React.
This article uses a function called tFunction
as a method for applying i18n.
Since it mainly deals with content at the type level, not JS logic, it will not deal with what actually happens to internal logic.
It can be said that it plays the same role as a function such as i18next.t.
How about I18n return type
Here, it may be seen what value the returned value in tFunction
is. Then, what is the type of each return value?
import tFunction from 'utils';
tFunction('simple-example')
// This is example text
// string
tFunction('values-example', [15])
// I need to show 15 value
// string
tFunction('line-break-example')
// Hello <br /> I am FE developer
// ReactElement
It may be considered that L3 and L6 are string types.
However, in L9, ReactElement is returned because there is \n
in the i18n text.
To support the line break by returning the string without returning to ReactElement
,
dangerouslySetInnerHTML shall be used.
However, since there are many limitations, return to ReactElement instead of string.
If the variable values-example
contains JSX such as atag instead of 15
, the return type of values-example
is also different.
import tFunction from 'utils';
tFunction('values-example', [15])
// I need to show 15 value
// string
tFunction('values-example', [<a href="/about">more</a>])
// I need to show <a href="/about">more</a> value
// ReactElement
Even if the values-example
I18n key is used as above, the type must vary depending on what value comes to values.
This is because a tag
must also be returned to a component that is a ReactElement, not a string, as in <br/>
.
What is matter?
It was found that the i18n Text type varies as string and ReactElement depending on the case. So what is the problem?
tFunction
is that it does not intelligently infer types as mentioned above.
What is the problem if the type is not properly inferred here?
<input
type='input'
placeholder={tFunction('line-break-example')} // type error
/>
In many cases, typically when using HTML tags, attributes defined as strings may contain i18n values rather than strings.
In this case, [object Object]
is displayed in the placeholder instead of the xlt text.
Of course, it's obviously strange to have an a tag in the placeholder or a line change like \n. However, there may be cases where the i18n key is incorrectly used, or there may be cases where the i18n text is incorrectly registered.
In other words, there are the above problems in that the type is not properly inferred when using typescript, and it can cause another problem.
In this article, we will talk about how to define the type by format of i18n text. This article is basically related to typescript Template Literal Types, although i18n is an example.
- General knowledge of
typescript 4.1+
- Interest in Template Literal Types
- The experience of
i18n system
(optional)
Goal
Rules must be established to respond to line breaks and variables while using i18n.
Like rule that "put in \n
when you want to line break on the string"
or "If you want to insert a variable that depends on the case, use {}
.
We will first explain the i18n rule we are using and then talk about how to decide the type for the rule.
I18n Rule
Line Break
In i18n text, if you want to line break and display it, put \n
in the part of i18n text.
// i18n text json
{
"lineBreak": "Hello. \n I am FE developer"
}
// displayed text
Hello
I am FE developer
Variable
In i18n text, if you want to include a variable in the JS code that is contextually different, rather than static text, Cover with {}
.
Because you can put multiple variables in one i18n text, Inside the {}
, start from 0 and increase it by 1.
// i18n text json
{
"oneValue": "This product is {0}.",
"unit": "${0}",
"twoValue": "This product is {0} and it will be delivered after {1} days"
}
import tFunction from 'utils';
tFunction('oneValue', [tFunction('unit', [1000])])
// This product is $1000.
tFunction('twoValue', [tFunction('unit', [500]), 3])
// This product is $500 and it will be delivered after 3 days
Link
If you want to include a link in the i18n text, you use the same way as the variable.
{}
is wrapped and used, and the text to be shown on the link is made into a separate i18n text.
// i18n text json
{
"link": "click",
"linkText": "{0} to show more information"
}
import tFunction from 'utils';
tFunction('linkText', [<a href="/more">{tFunction('link')}</a>])
// <a href="/more">click</a> to show more information
I18n Return Type
Then, for the above cases, what type of tFunction
would be ideal to be returned to?
import tFunction from 'utils';
const i18nJson = {
"lineBreak": "Hello. \n I am FE developer",
"oneValue": "This product is {0}.",
"unit": "${0}",
"twoValue": "This product is {0} and it will be delivered after {1} days",
"link": "click",
"linkText": "{0} to show more information"
} as const;
tFunction('lineBreak')
// Hello <br /> I am FE developer
tFunction('oneValue', [tFunction('unit', [1000])])
// This product is $1000.
tFunction('twoValue', [tFunction('unit', [500]), 3])
// This product is $500 and it will be delivered after 3 days
tFunction('linkText', [<a href="/more">{tFunction('link')}</a>])
// <a href="/more">click</a> to show more information
Since L12 contains <br />
, it should not be inferred as a normal string.
Therefore, if there is a line change such as \n
in i18n text, the type should be inferred as ReactElement
.
L14 and L16 contain values ({0} and {1})
, but the additional value is also string or number. Therefore, it may be inferred as string
.
The last L18 is slightly different from the L14 and L16.
The a tag was included as values, and in order to display it, the type must be inferred as ReactElement
.
Recap of Goal
Therefore, the goals are summarized as follows.
- The return type of
tFunction
should be inferred according to the i18n key. - The return type of
tFunction
depends on the format of the i18n text that matches the i18n key.- If you have a
\n
line-breaking keyword in i18n text, infer it asReactElement
. - In i18n text, if the variable
{}
has a keyword, and values such as string and number are entered as variables, it is inferred as string. - In i18n text, if the variable
{}
has a keyword, and a value other than string and number is entered as a variable, it is inferred as ReactElement.
- If you have a
- In addition, it would be good to check the type in the second parameter of
tFunciton
depending on whether the i18n text has the{}
variable keyword or how many.
Definition of Type
Now, we are planning to fill in the return type of tFunction
according to the above conditions.
Let's look at an example of the code below.
import React from 'react';
const i18nJson = {
"simple": "Hello World",
"lineBreak": "Hello. \n I am FE developer",
"oneValue": "This product is {0}.",
"unit": "${0}",
"twoValue": "This product is {0} and it will be delivered after {1} days",
"link": "click",
"linkText": "{0} to show more information"
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type TFunction = (key: I18nKey, values?: any) => string
const tFunction: TFunction = (key: I18nKey, values: any) => i18nJson[key];
const simple = tFunction('simple'); // string
const lineBreak = tFunction('lineBreak'); // string
const oneValue = tFunction('oneValue', [tFunction('unit', 500)]);
// string
const twoValue = tFunction('twoValue', [tFunction('unit', [500]), 3]);
// string
const linkText = tFunction('linkText', [<a href="/about">{tFunction('link')}</a>])
// string
All previous examples of i18n text used as examples are included in i18nJson
, which is inferred as I18nJson
and I18nKey
.
This defines the TFunction
function type and uses it for tFunction
.
Currently, all are returning to the string type, but our goal is to infer this from a different type depending on the case.
In other words, although both L20-L27 are currently inferred as strings, at the end of this article,
it aims to be divided into strings and ReactElement according to the format of text according to each key.
LineBreak
First, if \n
is included in i18n text, we will learn how to return to ReactElement instead of string.
We need to understand the Template Literal Type for this feature.
Template Literal Type
Typescript has a string type and a literal type. Furthermore, there is a Template Literal Type
that combines literal types to create another type.
As an example, it is as follows.
let str1 = 'example' // string
const str2 = 'example' // 'example'
type StrPrefix = 'one' | 'two'
type StrPostfix = 'type' | 'sample'
type TemplateStr = `${StrPostfix}_${StrPostfix}`
// 'one_type' | 'two_type' | 'one_sample' | 'two_sample'
The str1
of L1 is declared let
and is not inferred as a literal type,
but as a string type because the str1
variable can be reallocated to a different value.
On the other hand, str2
of L2 is declared as const
and is inferred as a literal type of example
.
By combining StrPrefix
and StrPostfix
declared in L4-L5, TemplateStr
is inferred as a Union type with four literal types in L7.
Using this principle, we will check the \n
text in i18n text and infer it as a separate type.
Define line break type
import React, { ReactElement } from 'react';
const i18nJson = {
"simple": "Hello World",
"lineBreak": "Hello. \n I am FE developer",
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type LineBreakFormat = `${string}\n${string}`;
type TFunction = <Key extends I18nKey,>(
key: Key,
values?: any,
) => I18nJson[Key] extends LineBreakFormat ? ReactElement : string;
const tFunction: TFunction = (key: I18nKey, values: any) =>
i18nJson[key] as any;
const simple = tFunction('simple'); // string
const lineBreak = tFunction('lineBreak'); // ReactElement
If you look at L12, use the Template Literal String to make the LineBreakFormat
type separately.
If you set ${string}\n${string}
, you have a string of \n
wrapped in the string
It is inferred as LineBreakFormat
.
Function type inference
We should infer the return type by looking at the literal type of the key entering run-time when calling TFunction
.
If I18nKey
is used as it is in the type area, the return type cannot be changed according to the key input value.
Here, change it as follows using Function type conference.
// Before
type TFunction = (key: I18nKey, values?: any) => string
// After
type TFunction = <Key extends I18nKey,>(key: Key, values?: any) => string;
If Key
is extracted as Generic in the above way and TFunction
is not explicitly declared, the literal type entering the key can be used.
Finally, determine whether the I18n text for the key is LineBreakFormat
and branch ReactElement and string.
// Before
type TFunction = <Key extends I18nKey,>(key: Key, values?: any) => string;
// After
type TFunction = <Key extends I18nKey,>(key: Key, values?: any) =>
I18nJson[Key] extends LineBreakFormat ? ReactElement : string;
In typescript
, extends
means inheritance when used in interface, but it can also be used as Conditional Types.
Therefore, if i18n Text, I18nJson[Key]
is LineBreakFormat
, it returns to ReactElement, otherwise it returns to string type.
Here, the key of I18nJson[Key]
is a literal type used for run-time.
Therefore, if simple
is introduced as a key, use the Hello World
literal type by inference as a type for I18nJson['simple']
.
If lineBreak
is used as a key, the condition logic of LineBreakFormat
is meaningful because the type comparison of Hello. \n I am FE developer
is performed as a type of I18nJson['lineBreak']
.
I18nJson[Key]
- Key: simple ->
I18nJson['simple']
->'Hello World'
- Key: lineBreak ->
I18nJson['lineBreak']
->'Hello. \n I am FE developer'
- Key: simple ->
Thanks to this, the return type proceeds differently in L17-L18 with the string and react element types, respectively.
To determine the type of entry to a variable
Next, if there is only a number and a string in the second parameter array of TFunction
,
the string is returned, and if other values such as ReactElement are included, the ReactElement is returned.
As an example, the code to view is as follows.
import React from 'react';
const i18nJson = {
"oneValue": "This product is {0}.",
"twoValue": "This product is {0} and it will be delivered after {1} days",
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type TFunction = (key: I18nKey, values?: any) => string
const tFunction: TFunction = (key: I18nKey, values: any) =>
i18nJson[key] as any;
const oneValue = tFunction('oneValue', [100]);
const twoValue = tFunction('twoValue', [100, '200']);
const twoValueWithReactElement = tFunction(
'twoValue',
[100, <a>Hello</a>],
);
Only necessary parts of the existing playground code have been left.
Our goal is to return L15 and L16 to string because the second parameters is an array consisting only of number and string,
and return to ReactElement because L17 puts a tag
as a variable.
The solution is simple. All you have to do is determine whether the values are of type (string | number)[]
.
type TFunction = <Params extends any[],>(key: I18nKey, values?: Params) =>
Params extends (string | number)[]
? string : ReactElement
In the above code, the type of values
is extracted as a Generic type to use
type inference,
and no Generic is used when calling tFunction
. Then we can use condition type as the type of Params
.
If Params
is an array consisting of only number and string,
it is automatically inferred as a type string, and if another value(ReactElement) is included, it is returned as a ReactElement.
When checked on the playground
, it is inferred as follows.
const oneValue = tFunction('oneValue', [100]); // string
const twoValue = tFunction('twoValue', [100, 200]); // string
const twoValueWithReactElement = tFunction(
'twoValue',
[100, <a>Hello</a>],
); // ReactElement
Thus, if a value other than number and string is included in the variable, we found a method to infer as ReactElement.
However, if i18n including variables is used, there may be the following cases.
const i18nJson = {
"normal": "Hello World",
"oneValue": "This product is {0}.",
"twoValue": "This product is {0} and it will be delivered after {1} days",
} as const;
const oneValue = tFunction('oneValue');
// NEED type error!!
// Variables should be used, but empty values are entered.
const twoValue = tFunction('twoValue', [100]);
// NEED type error!!
// The two is time to be, but when it enters only one.
const normal = tFunction('normal', [100]);
// NEED type error!!
// If there should be no variables, but they are being used.
In other words, it is about the case where the number of variables is not correct or used when they should be used, or used when they should not be used. For this, a more complex type declaration is needed, and let's continue with the next paragraph.
To check the number of variables
Now, we need to find how many strings are wrapped with {}
.
As we used in the line break before, We'll use the Template Literal Type
We will use it, but that is not enough.
Recursive Conditional Types
We need to find string that wrapped {}
and check it again. The Recursive method is used for this.
// This is not working!
type ValuesArray<I18nText extends string> =
I18nText extends `${string}{${string}}${string}`
? [any, ...ValuesArray<I18nText>] : [];
It is used in the same way as above. (Of course, the code above is incorrect.)
I think it declares Values Array
and hands over i18n text to I18nText
for type inference.
Then, if there is a literal type of {}
in the I18nText, it is to create an array and perform ValuesArray
again.
Of course, this code will not work properly now.
This is because the input of Recursive, that is, I18nText, is being put back in full I18nText.
We need to extract the remaining right string from the string containing {}
and put it as input of the recursive.
To example this as an example,
Hello {0} World {1} Thank {2} you
World {1} Thank {2} you
Thank {2} you
If we use text 1 on ValuesArray
, result is inferred [any, ...ValuesArray<?>]
and rest string(2nd text) is entered on ?
.
Next ValuesArray
uses text 2 as input. it returns [any, any, ...ValuesArray<?>]
.
At last, ValuesArray
should use text 3 as input, and it should return [any, any, any]
.
To above logic, we will use infer
Inferring Within Conditional Types
If Infer
is used, the generic used in the conditional type can be taken and used as the return type.
In other words, it is as follows.
type ValuesFormat<Suffix extends string> =
`${string}{${string}}${Suffix}`;
type ValuesArray<I18nText extends string> =
I18nText extends ValuesFormat<infer Rest>
? [any, ...ValuesArray<Rest>] : [];
ValuesFormat
is a type that includes generic Suffix
after }
,
rather than a string containing simple {}
.
It is meaningless if used alone, but if used with infer
in Values Array
below, the meaning is different.
As in the previous example, ValuesArray
check if {}
exist and fill in the array.
Then, infer Rest
is used, and Rest
means Suffix, the first generic of ValuesFormat
, and if we use Rest for the next recursive, what we want is completed.
type Example = 'Hello {0} World {1} Thank {2} you';
type ValuesFormat<Suffix extends string> =
`${string}{${string}}${Suffix}`;
type ValuesArray<I18nText extends string> =
I18nText extends ValuesFormat<infer Rest>
? [any, ...ValuesArray<Rest>] : [];
type Result = ValuesArray<Example> // [any, any, any];
Now, let's apply ValuesArray
to the first example.
import React, { ReactElement } from 'react';
const i18nJson = {
"normal": "Hello World",
"oneValue": "This product is {0}.",
"twoValue": "This product is {0} and it will be delivered after {1} days",
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type ValuesType = string | number | ReactElement;
type ValuesFormat<Suffix extends string> =
`${string}{${string} }${Suffix}`;
type ValuesArray<I18nText extends string> =
I18nText extends ValuesFormat<infer Rest>
? [ValuesType, ...ValuesArray<Rest>] : [];
type TFunction = <
Key extends I18nKey,
Params extends ValuesArray<I18nJson[Key]>,
>(key: Key, values?: Params) => string
const tFunction: TFunction = (key: I18nKey, values: any) => i18nJson[key] as any;
const oneValue = tFunction('oneValue'); // Error?
const twoValue = tFunction('twoValue', [100]); // Type Error!
// Argument of type '[number]' is not assignable
// to parameter of type '[ValuesType, ValuesType]'.
const normal = tFunction('normal', [100]); // Type Error!
// Type 'number' is not assignable to type 'undefined'.
In TFunction
of L19, ValuesArray
was applied to Parmas
.
As the generic of ValuesArray
, I18nJson[Key]
is added to dynamically receive the key through type inference so that i18n text can enter the generic.
If so, Params will be inferred as a ValuesType
array with a fixed length, respectively.
Therefore, an error occurs in the type check of L28 and L31, and the cause of the error is that an appropriate array length is not matched. However, L26 does not generate an error because the variable is not included, which is caused by the keyword '?' attached to the values of TFunction.
Let's increase the level of perfection a little.
Rest Parameters
Rest Parameters
is used to dynamically set the number of arguments in the function.
type TFunction = <
Key extends I18nKey,
Params extends ValuesArray<I18nJson[Key]>
>(key: Key, values?: Params) => string
const oneValue = tFunction('oneValue'); // Error?
const twoValue = tFunction('twoValue', [100]); // Type Error!
Therefore, in the above example, tFunction
used in oneValue
use ...
(Rest Parameters), which changes the number of values depending on the case, instead of ?
indicating that it is not necessary.
type TFunction = <
Key extends I18nKey,
Params extends ValuesArrayResult<I18nJson[Key]>
>(
key: Key,
...values: Params extends [] ? [undefined?] : [Params]
) => string
const tFunction: TFunction = (key: I18nKey, ...values: any) => i18nJson[key] as any;
const oneValue = tFunction('oneValue'); // Type Error!
Change values
to ...values
in L1 and type [undefined?]
if Params
is an empty array, and [Params]
if it is not an empty array.
If so, if values are not required, it is set to optional, and if values are required, it is a method of receiving arrays of the length of the number.
Here, the ? in the item of the Array is about the Optional elements, which is a grammar in which each element can be set to optional.
Result
We have implemented the following type definitions in previous articles.
- Define the Return Type differently depending on the presence or absence of the line break keyword.
- Define the return type differently according to the type of runtime value entering the i18n variable parameter.
- Define the number of i18n variables entering
TFunction
according to the number of i18n variable keywords.
When the above three definitions are combined, we can check the first i18n Rule at the type level.
- Draft
- Line Break
- + Values Type Check
- + Value Number Check
import React from 'react';
const i18nJson = {
"simple": "Hello World",
"lineBreak": "Hello. \n I am FE developer",
"oneValue": "This product is {0}.",
"unit": "${0}",
"twoValue": "This product is {0} and it will be delivered after {1} days",
"link": "click",
"linkText": "{0} to show more information",
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type TFunction = (key: I18nKey, values?: any) => string
const tFunction: TFunction = (key: I18nKey, values: any) => i18nJson[key];
const simple = tFunction('simple'); // string
const lineBreak = tFunction('lineBreak'); // string
const oneValue = tFunction('oneValue', [tFunction('unit', 500)]); // string
const twoValue = tFunction('twoValue', [tFunction('unit', [500]), 3]); // string
const linkText = tFunction('linkText', [<a href="/about">{tFunction('link')}</a>]) // string
import React, { ReactElement } from 'react';
const i18nJson = {
"simple": "Hello World",
"lineBreak": "Hello. \n I am FE developer",
"oneValue": "This product is {0}.",
"unit": "${0}",
"twoValue": "This product is {0} and it will be delivered after {1} days",
"link": "click",
"linkText": "{0} to show more information"
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type LineBreakFormat = `${string}\n${string}`;
type TFunction = <Key extends I18nKey,>(
key: Key,
values?: any,
) => I18nJson[Key] extends LineBreakFormat
? ReactElement : string;
const tFunction: TFunction = (key: I18nKey, values: any) => i18nJson[key] as any;
const simple = tFunction('simple'); // string
const lineBreak = tFunction('lineBreak'); // ReactElement
const oneValue = tFunction('oneValue', [tFunction('unit', 500)]); // string
const twoValue = tFunction('twoValue', [tFunction('unit', [500]), 3]); // string
const linkText = tFunction('linkText', [<a href="/about">{tFunction('link')}</a>]) // string
import React, { ReactElement } from 'react';
const i18nJson = {
"simple": "Hello World",
"lineBreak": "Hello. \n I am FE developer",
"oneValue": "This product is {0}.",
"unit": "${0}",
"twoValue": "This product is {0} and it will be delivered after {1} days",
"link": "click",
"linkText": "{0} to show more information"
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type LineBreakFormat = `${string}\n${string}`;
type TFunction = <Key extends I18nKey, Params extends any[]>(
key: Key,
values?: Params,
) => I18nJson[Key] extends LineBreakFormat
? ReactElement
: Params extends (string | number)[]
? string
: ReactElement;
const tFunction: TFunction = (key: I18nKey, values: any) => i18nJson[key] as any;
const simple = tFunction('simple'); // string
const lineBreak = tFunction('lineBreak'); // ReactElement
const oneValue = tFunction('oneValue', [tFunction('unit', [500])]); // string
const twoValue = tFunction('twoValue', [tFunction('unit', [500]), 3]); // string
const linkText = tFunction(
'linkText',
[<a href="/about">{tFunction('link')}</a>],
) // ReactElement
import React, { ReactElement } from 'react';
const i18nJson = {
"simple": "Hello World",
"lineBreak": "Hello. \n I am FE developer",
"oneValue": "This product is {0}.",
"unit": "${0}",
"twoValue": "This product is {0} and it will be delivered after {1} days",
"link": "click",
"linkText": "{0} to show more information"
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type LineBreakFormat = `${string}\n${string}`;
type ValuesType = string | number | ReactElement;
type ValuesFormat<Suffix extends string,> = `${string}{${string}}${Suffix}`;
type ValuesArray<I18nText extends string,> = I18nText extends ValuesFormat<infer Rest,> ? [ValuesType, ...ValuesArray<Rest,>] : [];
type TFunction = <
Key extends I18nKey,
Params extends ValuesArray<I18nJson[Key]>,
>(
key: Key,
...values: Params extends [] ? [undefined?] : [Params]
) => I18nJson[Key] extends LineBreakFormat
? ReactElement
: Params extends (string | number)[]
? string
: ReactElement;
const tFunction: TFunction = (key, ...values) => i18nJson[key] as any;
const simple = tFunction('simple'); // string
const lineBreak = tFunction('lineBreak'); // ReactElement
const oneValue = tFunction('oneValue', [tFunction('unit', [500])]); // string
const twoValue = tFunction('twoValue', [tFunction('unit', [500]), 3]); // string
const linkText = tFunction('linkText', [<a href="/about">{tFunction('link')}</a>]) // ReactElement
Let's look at the completed code above. Draft is a translation of the code that was originally imported as an example.
Here, all TFunctions
are returned to the string type.
Line Break
In the Line Break
version, the type of lineBreak
is inferred as ReactElement by adding a comparative sentence of LineBreakFormat
.
Values Type Check
In Values Type Check
, we deduced Params
and compared the string | number
array, adding code so that linkText
is inferred as a ReactElement.
Value Number Check
In the last Value Number Check
, we did not change the return type by specifying the type for Params
and applying values
as the Rest Parameter
The type was strongly applied by checking the number of values entering the second factor of tFunction
at the type check stage.
Recap
We have thus defined the type so that we can proceed in the type check step, including whether there are line changes and variables in i18n Text, and the runtime type of the variable and the number of variables. In this process, we utilized the following functions of Typecript.
- Template Literal Type
- Function Type inference
- Conditional Types
- Recursive Conditional Types
- Inferring Within Conditional Types
- Rest Parameters
- Optional elements in tuple types
A considerable number of features were used. If you hadn't defined complex types, there would have been features that you didn't know, and there would have been features that you didn't understand conceptually but didn've actually used.
We used i18n Text to define and use complex but useful types to check errors at TypeCheck instead of checking errors at Runtime, and also checked the typescript knowledge required to implement each requirement.
Readers of this article may implement a similar type of service that applies the i18n they are developing, or they may use the above typescript concepts to improve the type.
What I felt while applying the type system to i18n is that the scope of use of typescript is wider than I thought, and it is very useful and safe to build a system so that it can be checked at Type-check rather than at runtime.
I hope you have gained fresh stimulation or useful knowledge while reading this article, and I leave an appendix on how it can be if an object enters the value position rather than an arrangement.
Appendix #1 Receive variables as objects instead of arrays
We used Array format when we put variables in i18n text. However, even if you look at the example i18next interpolation, there are many cases where objects are used in the form of variables. Therefore, type definitions for tFunctions that inject objects other than arrays are summarized as appendices.
Example
import React, { ReactElement } from 'react';
const i18nJson = {
"oneValue": "This product is {value1}.",
"twoValue": "This product is {value1} and it will be delivered after {value2} days",
"link": "click",
"linkText": "{link} to show more information"
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type LineBreakFormat = `${string}\n${string}`;
type TFunction = <Key extends I18nKey,>(
key: Key,
values?: any,
) => I18nJson[Key] extends LineBreakFormat
? ReactElement : string;
const tFunction: TFunction = (key: I18nKey, values: any) => i18nJson[key] as any;
const oneValue = tFunction(
'oneValue',
{
value1: '$500'
},
); // string
const twoValue = tFunction(
'twoValue',
{
value1: '$500',
value2: 3,
},
); // string
const linkText = tFunction(
'linkText',
{
link: <a href="/about">{tFunction('link')}</a>,
},
) // string
We will use the example above, and the form of putting as a variable has changed from an array to an object type. So we're talking about the form of the object that goes into the variable.
To get keys for variables
The keys in the {}
format of the text are acquired using Recursive and infer.
type ValueFormat<
Key extends string = string,
Suffix extends string = string,
> = `${string}{${Key}}${Suffix}`
type ValuesKeyArray<I18nText extends string> =
I18nText extends ValueFormat<infer Key, infer Suffix>
? [Key, ...ValuesKeyArray<Suffix>] : [];
type ValuesKeyUnion<I18nText extends string> =
ValuesKeyArray<I18nText>[number];
type result = ValuesKeyArray<'This product is {value1} and it will be delivered after {value2} days'>
// result: 'value1' | 'value2'
Again, We separate key and suffix to infer using ValueFormat
and value of {}
is separated on ValuesKeyArray
.
Then, we make it union using ValuesKeyUnion
.
This completes the extraction of keys that should enter the variable.
Set the type to allow the acquired keys to enter the parameters
import React, { ReactElement } from 'react';
const i18nJson = {
"oneValue": "This product is {value1}.",
"twoValue": "This product is {value1} and it will be delivered after {value2} days",
"link": "click",
"linkText": "{link} to show more information"
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type LineBreakFormat = `${string}\n${string}`;
type ValuesType = string | number | ReactElement;
type ValueFormat<
Key extends string = string,
Suffix extends string = string,
> = `${string}{${Key}}${Suffix}`
type ValuesKeyArray<I18nText extends string> =
I18nText extends ValueFormat<infer Key, infer Suffix>
? [Key, ...ValuesKeyArray<Suffix>] : [];
type ValuesKeyUnion<I18nText extends string> =
ValuesKeyArray<I18nText>[number];
type TFunction = <
Key extends I18nKey,
Params extends I18nJson[Key] extends ValueFormat
? [Record<ValuesKeyUnion<I18nJson[Key]>, ValuesType>]
: [undefined?]
>(
key: Key,
...values: Params
) => I18nJson[Key] extends LineBreakFormat
? ReactElement : string;
const tFunction: TFunction = (key: I18nKey, ...values: any) => i18nJson[key] as any;
const oneValue = tFunction(
'oneValue',
{
value1: '$500'
},
); // string
const twoValue = tFunction(
'twoValue',
{
value1: '$500',
value2: 3,
},
); // string
const linkText = tFunction(
'linkText',
{
link: <a href="/about">{tFunction('link')}</a>,
},
) // string
Using ValuesKeyUnion
mentioned above, set the values type so that only objects for the key of the variable can be inserted
using Rest Parameters as shown in L22.
If so, in the L31-L33 example, an error will occur if a variable is not entered or if it is entered incorrectly.
If I18n text is not stringed, infer with ReactElement
import React, { ReactElement } from 'react';
const i18nJson = {
"oneValue": "This product is {value1}.",
"twoValue": "This product is {value1} and it will be delivered after {value2} days",
"link": "click",
"linkText": "{link} to show more information"
} as const;
type I18nJson = typeof i18nJson;
type I18nKey = keyof I18nJson;
type LineBreakFormat = `${string}\n${string}`;
type ValuesType = string | number | ReactElement;
type ValueFormat<
Key extends string = string,
Suffix extends string = string,
> = `${string}{${Key}}${Suffix}`
type ValuesKeyArray<I18nText extends string> =
I18nText extends ValueFormat<infer Key, infer Suffix>
? [Key, ...ValuesKeyArray<Suffix>] : [];
type ValuesKeyUnion<I18nText extends string> =
ValuesKeyArray<I18nText>[number];
type TFunction = <
Key extends I18nKey,
Params extends I18nJson[Key] extends ValueFormat
? [Record<ValuesKeyUnion<I18nJson[Key]>, ValuesType>] : [undefined?],
>(
key: Key,
...values: Params
) => I18nJson[Key] extends LineBreakFormat
? ReactElement
: I18nJson[Key] extends ValueFormat
? Params extends [Record<string, string | number>]
? string : ReactElement
: string;
const tFunction: TFunction = (key, ...values) => i18nJson[key] as any;
const oneValue = tFunction('oneValue', { value1: '$500' }); // string
const twoValue = tFunction('twoValue', { value1: '$500', value2: 3 }); // string
const linkText = tFunction('linkText', { link: <a href="/about">{tFunction('link')}</a> }) // ReactElement
In the case of ValueFormat
in L35-L37, if condition is set to Record<string, string | number>
,
and if a type other than string and number is used as a variable, it can be confirmed that it is inferred as ReactElement like L44.
In this way, the variable type can be specified not only for the array but also for the object.