This article will show you how to integrate i18n with Typescript in your projects to provide a better development experience for developers. A better development experience means providing typing prompts, auto-replenishing, parameter type validation, and so on, avoiding low-level errors.

Before optimization:



After the optimization:

If you’re clear on how to do this, reading on probably won’t help much.

Start of the text 📝

Recall that in front-end projects, i18N usually appears in the form of multiple language maps, with the same key translated into different contents in different language files:

// en.json
{
  "hello": "Hello"
}

// zh-CN.json
{
  "hello": "Hello"
}
Copy the code

Where the copy needs to be displayed, use the method provided by the I18N toolkit to pass a “key” to specify the multilingual text to be displayed.

<div>{t('hello')}</div>
Copy the code

React i18N react-i18N react-i18N react-i18N react-i18N react-i18N react-i18N

Problems appear 🚧

The t method here can take any argument, even if it doesn’t have any problem passing in a nonexistent key, and most I18N libraries will automatically fallback to displaying the key directly.

There’s certainly room for low-level errors, which we don’t want to happen in Typescript.

It’s easy to tell Typescript T methods which translated keys to accept. We all can be enumerated, using a combination of | symbols. And declare an I18nT function type.

type TranslateKeys = 'event.title' | 'event.description'|... type I18nT = { (key: TranslateKeys): string }Copy the code

Depending on the tool you use, you may need to declare the form of merge or type assertion to make your program understand the type of T.

// In the i18Next project, the t method is of type TFunciton, which can be merged using declarations

declare module 'i18next' {
  interface TFunction extends I18nT {
  }
}

// In my project, the t method is provided by other JS modules and can be exported using type assertions.
const i18n = createI18n()
export default i18n.t.bind(i18n) as I18nT
Copy the code

When this is done, the editor will automatically alert you when using the t method, and tsLINT, TSC will check if a key does not exist.

Those of you who are not familiar with Typescript can check out my last article to learn about Typescript with examples. The following sections will show you how to continue refining and optimizing this feature.

Read the type 📄 directly from the JSON file

In the example above, TranslateKeys is manually maintained, which is definitely not possible. It would be too expensive to change it every time a CURD is created. Typescript supports direct import of JSON files. Parameter types can be obtained from objects that are imported.

type I18nStoreType = typeof import('.. /assets/en.json')

export type I18nT = {
   (key: keyof I18nStoreType): string
}
Copy the code

Import the return value of a json file is the Object of the file content. Typeof is used to obtain the Object type, and keyof is used to specify the key typeof the t method as the keyof the Object type.

The configuration item related to tsconfig is resolveJsonModule. After the configuration, VSCode needs to be restarted to take effect.

// tsconfig.json
{
  "compilerOptions": {
    // ...
    "resolveJsonModule": true}}Copy the code

With I18nStoreType, we can go one step further and have Typescript tell us what the translation is when we pass a key value.

This has the advantage of avoiding typing an existing key by hand in the wrong place, where Typescript can’t help you. The only way to be sure is to copy the key and translate the document to make sure it’s correct.

In Typescript we can type a type alias, similar to the object[key] value in JS, which in combination with generics can be rewritten as follows.

export type I18nT<T> = {
   (key: T): I18nStoreType[T]
}
Copy the code

By defining a generic parameter

, Typescript will automatically infer the type T represents from the key and apply the type T to I18nStoreType[T], which automatically returns the value of a given key.

However, the reality is that all values obtained through import are of type STIRng and cannot be prompted to display the translated content.

According to??? Typescript treats an Object value loosely because it can be reassigned to another value, so even if JOSN files write string literals, Typescript extrapolate them to the looser string type.

To make Typescript understand that value is a constant, use the as const or readonly keyword.

Const foo = {val: 'val'} typeof foo.val // string const foo = {val: 'val' as const} typeof foo.valCopy the code

Is there a way to add an as const assertion to an import JSON file? When SEARCHING github, I found that this issue has been mentioned for a long time, and it is still in Open state.

You can write a Node JS file that copies the contents of the multi-language JSON file and inserts export default and as const respectively.

const path = require('path')
const fs = require('fs')
 
const targetPath = path.join(
  process.cwd(),
  './assets/i18n.d.ts'.)const sourcePath = path.join(process.cwd(), './assets/i18n/en.json')
 
const sourceContent = require(sourcePath)
fs.writeFileSync(
  targetPath,
  `export default The ${JSON.stringify(sourceContent)} as const`.)console.log('✨ Generate i18n ts file successfully.')
Copy the code

The JSON file type will be changed to the newly added D.ts file.

type I18nStoreType = typeof import('.. /assets/en.json')

export type I18nT<T> = {
   (key: T): I18nStoreType[T]
}
Copy the code

Here, we have achieved the i18N Key verification, prompt, and show the corresponding translation content of the Key. 🚀 🚀 🚀

Purely relying on the definition of JSON files has major limitations, such as some translations with slots for passing parameters.

const translactions = {
  'transactions.list.count': 'total {count} items'
}
 
t('transactions.list.count', {
  count: 10
})
Copy the code

It would be nice if Typescript could validate parameter names when typing translation keys without worrying about the obvious bug 🐛. (Just as an example, spelling errors can actually be fixed by installing code-spell-checker.

But the word is correct, just different from the parameters needed for translation and it is necessary to cover this scenario. The next step is to write a Node gadget that automatically parses parameters and generates Typescript i18n declaration files.

Authoring tools automatically generate 🔧

1. Parse the parameters from the template string

Translation slots usually have fixed patterns, and in this example Angle brackets {} are used to define slots. We can easily match the text in Angle brackets using regular expressions.

export function getSlots(template: string, regexp: RegExp) :string[] {
  const res: string[] = []

  while (true) {
    const matches = regexp.exec(template)
    if(! matches) {break
    }
    res.push(matches[1].trim())
  }

  return res
}

getSlots('from {min} to {max}'./{([\s\S]+?) }/g) // ['min', 'max']
Copy the code

The regexp.prototype.exec () method is used here, which is stateful in the global state and records the position of the successful match in the lastIndex property. This property allows you to iterate over the contents of the string wrapped in Angle brackets.

2. Typescript function overloading

The ultimate goal is to have Typescript prompt you when you type a given key into a t method, which is where function overloading comes in. There is no concept of function overloading in Javascript, because variables in Javascript can be assigned arbitrary values. To implement a variable that returns a different value depending on the argument type, typeof determines the runtime typeof the argument. Function overloading is supported in Typescript, where methods with the same name can be declared multiple times (with different parameter types).

type sort = {
  (entities: number[]) :number[];
  (entities: string[]) :string[];
  (entities: any[]) :any[];
}
Copy the code

In the example above, we declare a sort method with two overloads, telling Typescript that number[] returns number[] and string[] returns string[]. When using function overloading, the last line should be compatible with entities: any[], as in the previous example. This does not mean passing any arguments to entities, as in sort([‘foo’, 0]) will raise an exception. To figure out what format you want to generate, all that’s left is to iterate through the JSON file, concatenate the strings, and finally save to a D.ts file.

export type I18nKey = keyof typeof import('./en.json')

export type I18nT = {
  (key: 'common.action.collect') :'Collect'

  (
    key: 'withdraw.tips.amount_limit'.params: {
      min: any
      max: any
    },
  ): 'Please enter amount between {min} and {max}'

  // ...

  (key: I18nKey): string
}
Copy the code
3. Prettier

To keep the coding style of the project uniform, virtually every project uses the prettier tool to format the code automatically. But not every project might look exactly the same, and to format an automatically generated file for more projects, prettier also needs to be added.

import * as prettier from 'prettier'
const prettierOptions = await prettier.resolveConfig(sourcePath)

fs.writeFileSync(
  targetPath,
  prettier.format(
    '// String concatenation,
    {
      ...prettierOptions,
      parser: 'typescript',},),)Copy the code

The prettier resolveConfig method prettier resolveConfig looks for configuration from sourcePath and returns the configuration when it finds it. This is done by passing configuration items to the Format method, specifying parser based on the file contents. Another point worth noting in the string concatenation process is the issue of quotation marks. Initially, I used quotation marks to wrap the i18n key and value.

export function getOverlapFunctionDeclaration(
  i18nKey: string,
  value: string, params? :string[].) {
  if (params && params.length) {
    return `
      (key: '${i18nKey}', params: {
        ${params.map((key: string) = >` '${key}': any`)}}) : '${value}'`
  }

  return `
    (key: '${i18nKey}') :${value}'`
}
Copy the code

However, when the strings themselves contain single quotes (such as I’m {name}), prettier formatting errors because it is not a legitimate TS file. This is done by substituting the single-citation ‘sign with the string \’ where all input text is needed.

 export function replaceQuotes(str: string) {
   return str.replace(/'/g.'\ \ \')}Copy the code
4. Package into NPM package

Finally, you can add configurable parameters to the program (such as the regex of the slot, the location of the input JSON file, the name of the output method type, etc.), add unit tests, document usage, and publish it to NPM for others to use. (Not the focus of this article). NPM post[script] can be used to re-generate the declaration file with each translation update. For example, in my project, every time I execute the translation, I re-pull the translation from the remote side and save it as a JSON file, so I add posttransify script. This will regenerate my D.ts declaration file every time I pull the translation.

"scripts": {
  "transify": "..."."posttransify": "transify-ts --sourcePath=./assets/strings/i18n/en.json",}Copy the code

Results 🎉

The end result is that we apply what we’ve learned to make Typescript and I18n work perfectly together. Happy Coding 🎉 🎉 🎉