What's the process for validating i18n dictionaries using TypeScript?

Is there a way to enforce type checking on existing keys within dictionaries in react-i18next? This means that TypeScript will provide warnings at compile time if a key does not exist.

For example:

Let's say we have the following dictionary:

{
  "footer": {
    "copyright": "Some copyrights"
  },

  "header": {
    "logo": "Logo",
    "link": "Link",
  },
}

If I use a non-existent key, TypeScript should catch it:

const { t } = useTranslation();

<span> { t('footer.copyright') } </span> // this is fine as footer.copyright exists
<span> { t('footer.logo') } </span> // TypeScript error - footer.logo does not exist in the dictionary

What is the proper term for this technique? I'm sure many others are interested in this feature as well.

Does react-i18next include this functionality by default? Are there any APIs in react-i18next that allow for extending the library to enable this? I prefer not to create wrapper functions.

Answer №1

Enhanced Type Safety in TypeScript 4.1

The latest version of TypeScript, TS 4.1, introduces support for typed string-key lookups and interpolation using template literal types.

With this new feature, we can now access dictionary keys or object paths deeply by providing a string argument:

t("footer"); // ✅ { copyright: "Some copyrights"; }
t("footer.copyright"); // ✅ "Some copyrights"
t("footer.logo"); // ❌ should trigger compile error

Let's delve into how we can define a suitable return type for the translate function t, how to emit compile errors for non-matching key arguments, provide IntelliSense support, and explore an example of string interpolation.

1. Key Lookup: Return Type

// Returns the property value from object O given the property path T, otherwise 'never'
type GetDictValue<T extends string, O> =
    T extends `${infer A}.${infer B}` ?
    A extends keyof O ? GetDictValue<B, O[A]> : never
    : T extends keyof O ? O[T] : never

function t<P extends string>(p: P): GetDictValue<P, typeof dict> { /* implementation */ }

Check out the Playground to play with the code: Playground

2. Key Lookup: IntelliSense and Compile Errors

To enforce type safety and provide IntelliSense suggestions, we use conditional types like so:

// Returns the same string literal T if props match, else 'never'
type CheckDictString<T extends string, O> = 
  T extends `${infer A}.${infer B}` ?
  A extends keyof O ? `${A}.${Extract<CheckDictString<B, O[A]>, string>}` : never
  : T extends keyof O ? T : never

function t<P extends string>(p: CheckDictString<P, typeof dict>): GetDictValue<P, typeof dict> { /* implementation */ }

Explore the Playground for interactive examples: Playground

3. Interpolation

Incorporating string interpolation into translations:

// Helper types for string interpolation
type Keys<S extends string> = S extends '' ? [] :
    S extends `${infer _}{{${infer B}}}${infer C}` ? [B, ...Keys<C>] : never

type Interpolate<S extends string, I extends Record<Keys<S>[number], string>> =
    S extends '' ? '' :
    S extends `${infer A}{{${infer B}}}${infer C}` ?
    `${A}${I[Extract<B, keyof I>]}${Interpolate<C, I>}`
    : never

Example:

type Dict = { "key": "yeah, {{what}} is {{how}}" }
type KeysDict = Keys<typeof dict["key"]> // ['what', 'how']
type I1 = Interpolate<typeof dict["key"], { what: 'i18next', how: 'great' }>;;
// Result: "yeah, i18next is great"

function t<K extends keyof Dict, I extends Record<Keys<Dict[K]>[number], string>>(k: K, args: I): Interpolate<Dict[K], I> { /* implementation */ }

const ret = t('key', { what: 'i18next', how: 'great' } as const);
// Output: "yeah, i18next is great"

Try it out on the Playground: Playground

For more details, refer to the discussion on deep keyof of a nested object: Typescript: deep keyof of a nested object

Note: These advanced typing features may have limits due to complexity and recursion depth restrictions in TypeScript compiler.

Answer №2

Good news! React-i18next now includes native support for this feature. While official documentation may be lacking, the source code contains useful comments to guide you.

If your translations are stored in

public/locales/[locale]/translation.json
and English is your main language:

// src/i18n-resources.d.ts

import 'react-i18next'

declare module 'react-i18next' {
  export interface Resources {
    translation: typeof import('../public/locales/en/translation.json')
  }
}

For those utilizing multiple translation files, remember to include all of them in the Resources interface, each identified by namespace.

Don't forget to add

"resolveJsonModule": true
to your tsconfig.json if you're importing translations from a json file.

Answer №3

To achieve similar functionality, one can create a TranslationKey type and utilize it in the useT hook along with a custom Trans component.

  1. Start by creating a translation.json file
{
  "PAGE_TITLE": "Product Status",
  "TABLES": {
    "COUNTRY": "Country",
    "NO_DATA_AVAILABLE": "No price data available"
  }
}
  1. Generate the TranslationKey type using generateTranslationTypes.js
/**
 * This script generates the TranslationKey.ts types that are used from
 * useT and T components
 *
 * ...
 */

const translation = require("./translation.json")
const fs = require("fs")

function extractKeys(obj, keyPrefix = "", separator = ".") {
  const combinedKeys = []
  const keys = Object.keys(obj)

  keys.forEach(key => {
    if (typeof obj[key] === "string") {
      if (key.includes("_plural")) {
        return
      }
      combinedKeys.push(keyPrefix + key)
    } else {
      combinedKeys.push(...extractKeys(obj[key], keyPrefix + key + separator))
    }
  })

  return combinedKeys
}

function saveTypes(types) {
  const content = `// generated file by src/i18n/generateTranslationTypes.js

type TranslationKey =
${types.map(type => `  | "${type}"`).join("\n")}
`
  fs.writeFile(__dirname + "/TranslationKey.ts", content, "utf8", function(
    err
  ) {
    if (err) {
      console.log("An error occurred while writing to File.")
      return console.log(err)
    }

    console.log("file has been saved.")
  })
}

const types = extractKeys(translation)

console.log("types: ", types)

saveTypes(types)

  1. Implement the useT hook for handling translations using the TranslationKey type
import { useTranslation } from "react-i18next"
import { TOptions, StringMap } from "i18next"

function useT<TInterpolationMap extends object = StringMap>() {
  const { t } = useTranslation()
  return {
    t(key: TranslationKey, options?: TOptions<TInterpolationMap> | string) {
      return t(key, options)
    },
  }
}

export default useT

  1. Create a custom component similar to Trans for rendering translated content
import React, { Fragment } from "react"
import useT from "./useT"
import { TOptions, StringMap } from "i18next"

export interface Props<TInterpolationMap extends object = StringMap> {
  id: TranslationKey
  options?: TOptions<TInterpolationMap> | string
  tag?: keyof JSX.IntrinsicElements | typeof Fragment
}

export function T<TInterpolationMap extends object = StringMap>({
  id,
  options,
  tag = Fragment,
}: Props<TInterpolationMap>) {
  const { t } = useT()
  const Wrapper = tag as "div"
  return <Wrapper>{t(id, options)}</Wrapper>;
}

export default T

  1. Utilize useT and T components to render content with type-checked ids
const MyComponent = () => {
    const { t } = useT()


    return (
        <div>
            { t("PAGE_TITLE", {count: 1})}
            <T id="TABLES.COUNTRY" options={{count: 1}} />
        </div>
    )
}

Answer №4

To ensure type safety, the official documentation provides a detailed explanation: https://www.i18next.com/overview/typescript

For example:

// importing original type declarations
import "i18next";
// importing all namespaces (for default language only)
import ns1 from "locales/en/ns1.json";
import ns2 from "locales/en/ns2.json";

declare module "i18next" {
  // Extending CustomTypeOptions
  interface CustomTypeOptions {
    // custom namespace type, if modified
    defaultNS: "ns1";
    // custom resources type
    resources: {
      ns1: typeof ns1;
      ns2: typeof ns2;
    };
    // other modifications
  }
}

Answer №5

Excellent response @ford04! However, there seems to be a slight issue with Keys and Interpolate types. If you use it in this particular manner without a variable at the end of the string, the interpolation may not recognize it. To address this issue, you can make the adjustment as follows:

export type Keys<S extends string> =
  S extends `${string}{{${infer B}}}${infer C}`
    ? C extends `${string}{{${string}}}${string}`
      ? [B, ...Keys<C>]
      : [B]
    : never;
type Interpolate<
  S extends string,
  I extends Record<Keys<S>[number], string>,
> = S extends ''
  ? ''
  : S extends `${infer A}{{${infer B}}}${infer C}`
  ? C extends `${string}{{${string}}}${string}`
    ? `${A}${I[Extract<B, keyof I>]}${Interpolate<C, I>}`
    : `${A}${I[Extract<B, keyof I>]}`
  : never;

For reference, check out this playground example: Playground

Answer №7

I have devised a solution that extracts the key (including nested keys) from a dictionary.

declare module 'i18next' {
    import deTranslation from 'path/to/dictionary.json';

    type Translations<T = typeof deTranslation> = T;
    type ConcatKeys<T, U> = T extends string ? `${T & string}.${U & string}` : never;

    // Create a recursive type for nested keys
    type NestedKeys<T> =
        T extends object ? {
                [K in keyof T]: K | ConcatKeys<K, NestedKeys<T[K]>>;
            }[keyof T] :
            string;

    type TranslationKeys<T> = NestedKeys<T>;
    interface TranslationFunction<T> {
        <K extends TranslationKeys<T>>(key: K, nestedKeys?: string[]): string;
    }
    export const t: TranslationFunction<Translations>;
}

This allows for suggestions and validation to be implemented. https://i.sstatic.net/dka4x.png

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Is Typescript syntax for a collection of strings comparable to using string[]?

When working with Typescript, the convention to define an Array of Strings is either string[] or Array<string>. In our team, we lean towards using the more concise string[]. However, when it comes to defining a Set of Strings, is there a shorter syn ...

An array of objects in Typescript utilizing a generic type with an enum

Here’s a glimpse of code that showcases the issue: enum ServicePlugin { Plugin1, Plugin2, Plugin3, } interface PluginOptions { [ServicePlugin.Plugin1]: { option1: string }; [ServicePlugin.Plugin2]: { option1: number; option2: number }; } type ...

Tips for creating unit tests for my Angular service that utilizes the mergeMap() function?

As a beginner with the karma/jasmine framework, I am currently exploring how to add a test case for my service method shown below: public getAllChassis(): Observable<Chassis[]> { return this.http.get('chassis').pipe( merge ...

Displaying updated information in Angular

I recently developed a chat application using Angular that utilizes the stomp socket from @stomp/ng2-stompjs. To display all messages, I am leveraging *ngFor. <p *ngFor="let item of messages" style="padding: 5px; font-size: 18px"> <span style ...

Challenges arise with dependencies during the installation of MUI

[insert image description here][1] I attempted to add mui styles and other components to my local machine, but encountered a dependency error. How can I resolve this issue? [1]: https://i.stack.imgur.com/gqxtS.png npm install @mui/styles npm ERR! code ERE ...

The values obtained from an HTTP GET request can vary between using Curl and Ionic2/Angular2

When I make a GET request using curl in the following manner: curl https://api.backand.com:443/1/objects/todos?AnonymousToken=my-token I receive the correct data as shown below: {"totalRows":2,"data":[{"__metadata":{"id& ...

Is it possible for Angular's ngStyle to accept multiple functions? If not, what is the alternative solution?

Can multiple conditions be added to different attributes with ngStyle? Is it possible to change text color and background color based on specific JSON values using ngStyle? About the Project: I am currently working on creating a tab bar that resembles a ...

I am interested in creating an input range slider using React and TypeScript

This code was used to create a slider functionality import { mainModule } from 'process'; import React, { useState } from 'react'; import styled from 'styled-components'; const DragScaleBar = () => { const [value, setV ...

Incorporating Moralis into Ionic Angular with TypeScript

I'm currently developing an app using Ionic Angular (TypeScript) that will be compatible with both Android and iOS devices. I've decided to incorporate the Moralis SDK to establish a connection with the Metamask wallet. Here's a summary of ...

I make a commitment to continue working until the issue is resolved and the page is successfully changed in the Protractor

I have a table with rows and I need to click on the edit button in a row that has a specific label (test server label). This is my function: public selectOnRow( textSelector:string , clickableSelector : string , value:string) { let promise = new Prom ...

Attempting to incorporate alert feedback into an Angular application

I am having trouble referencing the value entered in a popup input field for quantity. I have searched through the documentation but haven't found a solution yet. Below is the code snippet from my .ts file: async presentAlert() { const alert = awa ...

Having trouble with React npm start: 'No chokidar version found' error occurring

After cloning my React-Typescript app on Github Pages and attempting to make some changes, I encountered an issue. Despite running npm install to install all dependencies, when I tried to run npm start, I received the following error message: https://i.st ...

Determining when to include @types/packagename in React Native's dev dependencies for a specific package

Just getting started with React Native using typescript. Take the package vector icon for example, we need to include 2 dependencies: 1. "react-native-vector-icons": "^7.1.0" (as a dependency) 2. "@types/react-native-vector-icons": "^6.4.6" (as a dev ...

Creating objects based on interfaces in TypeScript is a common practice. This process involves defining

Within my TypeScript code, I have the following interface: export interface Defined { 4475355962119: number[]; 4475355962674: number[]; } I am trying to create objects based on this interface Defined: let defined = new Defined(); defined['447 ...

The function call with Ajax failed due to an error: TypeError - this.X is not a function

I am encountering an issue when trying to invoke the successLogin() function from within an Ajax code block using Typescript in an Ionic v3 project. The error message "this.successLogin() is not a function" keeps popping up. Can anyone provide guidance on ...

Parsing values from deeply nested objects and arrays

I've come across this issue before, but I'm having difficulty navigating through a nested structure. I can't seem to find any guidance in the right direction. Here is the object I'm attempting to parse: const nestedArray = { id ...

What is the reason for TypeScript disabling unsecure/non-strict compiler rules by default?

Recently, I found myself having to enable a slew of compiler options in my application: "alwaysStrict": true, "extendedDiagnostics": true, "noFallthroughCasesInSwitch": true, "noImplicitAny", true, "noImplicitThis", true, "noImplicitReturns": true, "noUnu ...

Is there a way for me to access the user's gender and birthday following their login using their Google account details?

I have successfully implemented a Google sign-in button in my Angular application following the example provided in Display the Sign In With Google button: <div id="g_id_onload" class="mt-3" data-client_id="XXXXXXXXXXXX-XX ...

Exploring Typescript within React: Creating a property on the current instance

Within my non-TypeScript React component, I previously implemented: componentWillMount() { this.delayedSearch = _.debounce((val) => { this.onQuerySearch(val); }, 1000); } This was for debouncing user input on an input field. The corres ...

What is the best way to first identify and listen for changes in a form

In Angular, there are reactive forms that allow you to track changes in both the complete form and specific fields: this.filterForm.valueChanges.subscribe(() => { }); this.filterForm.controls["name"].valueChanges.subscribe(selectedValue => { }); ...