Как набрать словари Check i18n с помощью TypeScript?

Есть ли возможность ввести проверку существующих ключей в словарях react-i18next? Так что TS предупредит вас во время компиляции, если ключ не существует.

Пример.

Допустим, у нас есть такой словарь:

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

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

Если я предоставлю несуществующий ключ, TS взорвется:

const { t } = useTranslation();

<span> { t('footer.copyright') } </span> // this is OK, because footer.copyright exists
<span> { t('footer.logo') } </span> // TS BOOM!! there is no footer.logo in dictionary

Каково собственное название этой техники? Я очень уверен, что я не единственный, кто просит о таком поведении.

Реализовано в react-i18next из коробки? Есть ли в react-i18next API для расширения библиотеки, чтобы включить ее? Я хочу избежать создания функций-оберток.


person Green    schedule 07.10.2019    source источник


Ответы (5)


TS 4.1

наконец, поддерживает поиск по типу строковых ключей и интерполяцию с помощью типы литералов шаблона.

Теперь мы можем использовать аргумент строки с точками для глубокого доступа к ключам словаря / пути к объекту:

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

Давайте посмотрим 1.) на подходящий тип возвращаемого значения для функции перевода t 2.) как мы можем выдать ошибку компиляции при несовпадении ключевых аргументов и предоставить IntelliSense 3.) на примере строковой интерполяции.

1. Поиск ключа: возвращаемый тип

// returns property value from object O given 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> { /* impl */ }

площадка

2. Поиск ключа: IntelliSense и ошибки компиляции

Может быть достаточно просто вызвать ошибки компиляции на неправильных ключах:

// 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> { /* impl */ }

Продолжайте читать, если вам также нужна IntelliSense. Следующий тип будет запрашивать все возможные перестановки ключевых путей словаря, обеспечивать автозаполнение и помогать подсказками об ошибках для несовпадающих ключей:

// get all possible key paths
type DeepKeys<T> = T extends object ? {
    [K in keyof T]-?: `${K & string}` | Concat<K & string, DeepKeys<T[K]>>
}[keyof T] : ""

// or: only get leaf and no intermediate key path
type DeepLeafKeys<T> = T extends object ?
    { [K in keyof T]-?: Concat<K & string, DeepKeys<T[K]>> }[keyof T] : "";

type Concat<K extends string, P extends string> =
    `${K}${"" extends P ? "" : "."}${P}`
function t<P extends DeepKeys<typeof dict>>(p: P) : GetDictValue<P, typeof dict> 
  { /* impl */ } 

type T1 = DeepKeys<typeof dict> 
// "footer" | "header" | "footer.copyright" | "header.logo" | "header.link"
type T2 = DeepLeafKeys<typeof dict> 
// "footer.copyright" | "header.logo" | "header.link"

Детская площадка

Дополнительные сведения см. В разделе Typescript: deep keyof вложенного объекта.

Из-за комбинаторной сложности и в зависимости от формы объекта словаря вы можете нажать compiler ограничения глубины рекурсии. Более легкая альтернатива: предоставить IntelliSense для следующего ключевого пути постепенно на основе текущего ввода:

// T is the dictionary, S ist the next string part of the object property path
// If S does not match dict shape, return its next expected properties 
type DeepKeys<T, S extends string> =
    T extends object
    ? S extends `${infer I1}.${infer I2}`
        ? I1 extends keyof T
            ? `${I1}.${DeepKeys<T[I1], I2>}`
            : keyof T & string
        : S extends keyof T
            ? `${S}`
            : keyof T & string
    : ""

function t<S extends string>(p: DeepKeys<typeof dict, S>)
  : GetDictValue<S, typeof dict> { /* impl */ }

t("f"); // error, suggests "footer"
t("footer"); // OK
t("footer."); // error, suggests "footer.copyright"
t("footer.copyright"); // OK
t("header.") // error, suggests "header.logo" | "header.link"

площадка

3. Интерполяция

Вот пример использования строки интерполяция.

// retrieves all variable placeholder names as tuple
type Keys<S extends string> = S extends '' ? [] :
    S extends `${infer _}{{${infer B}}}${infer C}` ? [B, ...Keys<C>] : never

// substitutes placeholder variables with input values
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<Dict["key"]> // type KeysDict = ["what", "how"]
type I1 = Interpolate<Dict["key"], { what: 'i18next', how: 'great' }>;
// type I1 = "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> { /* impl */ }

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

площадка

Примечание. Все фрагменты можно использовать в сочетании с react-i18next или независимо.



Старый ответ

(PRE TS 4.1) Есть две причины, по которым ключи со строгой типизацией невозможны в react-i18next:

1.) TypeScript не имеет возможности оценивать динамические или вычисляемые строковые выражения, такие как 'footer.copyright', поэтому что footer и copyright могут быть определены как ключевые части в иерархии объектов переводов.

2.) useTranslation не применяет ограничения типа к определенному вами словарь / переводы. Вместо этого функция t содержит параметры универсального типа, по умолчанию равные string, если они не указаны вручную.


Вот альтернативное решение, в котором используется Остальные параметры / кортежи.

Typed t function:
type Dictionary = string | DictionaryObject;
type DictionaryObject = { [K: string]: Dictionary };

interface TypedTFunction<D extends Dictionary> {
    <K extends keyof D>(args: K): D[K];
    <K extends keyof D, K1 extends keyof D[K]>(...args: [K, K1]): D[K][K1];
    <K extends keyof D, K1 extends keyof D[K], K2 extends keyof D[K][K1]>(
        ...args: [K, K1, K2]
    ): D[K][K1][K2];
    // ... up to a reasonable key parameters length of your choice ...
}
Typed useTranslation Hook:
import { useTranslation } from 'react-i18next';

type MyTranslations = {/* your concrete type*/}
// e.g. via const dict = {...}; export type MyTranslations = typeof dict

// import this hook in other modules instead of i18next useTranslation
export function useTypedTranslation(): { t: TypedTFunction<typeof dict> } {
  const { t } = useTranslation();
  // implementation goes here: join keys by dot (depends on your config)
  // and delegate to lib t
  return { t(...keys: string[]) { return t(keys.join(".")) } }  
}
Import useTypedTranslation in other modules:
import { useTypedTranslation } from "./useTypedTranslation"

const App = () => {
  const { t } = useTypedTranslation()
  return <div>{t("footer", "copyright")}</div>
}
Test it:
const res1 = t("footer"); // const res1: { "copyright": string;}
const res2 = t("footer", "copyright"); // const res2: string
const res3 = t("footer", "copyright", "lala"); // error, OK
const res4 = t("lala"); // error, OK
const res5 = t("footer", "lala"); // error, OK

Вы потенциально можете Infer тех типов автоматически вместо подписи множественной перегрузки ( Детская площадка < / а>). Имейте в виду, что эти рекурсивные типы являются не рекомендуется для рабочей среды разработчиками ядра до TS 4.1.

person ford04    schedule 09.10.2019
comment
Я говорил об интерполяции i18n типа i18next.t('key', { what: 'i18next', how: 'great' });. проверьте здесь i18next.com/translation-function/interpolation - person Varun Sukheja; 15.09.2020
comment
@VarunSukheja обновил ответ с интерполяцией - person ford04; 17.12.2020
comment
Можно ли с этим иметь intellisense? - person Yassine Bridi; 11.02.2021
comment
@YassineBridi хорошее предложение. Обновленный ответ с разделом о IntelliSense. - person ford04; 11.02.2021
comment
Это гениально, спасибо. - person Yassine Bridi; 11.02.2021
comment
как насчет загрузки json из файла? - person Lucas Steffen; 01.07.2021

React-i18next теперь имеет встроенную поддержку для этого. Мне не удалось найти официальную документацию, но есть полезные комментарии в исходный код.

Предполагая, что ваши переводы сделаны на public/locales/[locale]/translation.json, а ваш основной язык - английский:

// src/i18n-resources.d.ts

import 'react-i18next'

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

Если вы используете несколько файлов перевода, вам необходимо добавить их все в интерфейс ресурсов, привязанный к пространству имен.

Обязательно установите "resolveJsonModule": true в своем tsconfig.json, если вы импортируете переводы из файла json.

person Aaron Frary    schedule 17.12.2020
comment
react.i18next.com/latest/typescript - person adrai; 29.06.2021

Другой способ добиться такого поведения - создать тип TranslationKey и использовать его, чем в ловушке useT и настраиваемом компоненте Trans.

  1. создать файл translation.json
{
  "PAGE_TITLE": "Product Status",
  "TABLES": {
    "COUNTRY": "Country",
    "NO_DATA_AVAILABLE": "No price data available"
  }
}
  1. сгенерируйте тип TranslationKey с помощью generateTranslationTypes.js
/**
 * This script generates the TranslationKey.ts types that are used from
 * useT and T components
 *
 * to generate type run this command
 *
 * ```
 * node src/i18n/generateTranslationTypes.js
 * ```
 *
 * or
 * ```
 * npm run generate-translation-types
 * ```
 */

/* eslint-disable @typescript-eslint/no-var-requires */
const translation = require("./translation.json")
const fs = require("fs")
// console.log("translation", translation)

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) {
      // eslint-disable-next-line no-console
      console.log("An error occurred while writing to File.")
      // eslint-disable-next-line no-console
      return console.log(err)
    }

    // eslint-disable-next-line no-console
    console.log("file has been saved.")
  })
}

const types = extractKeys(translation)

// eslint-disable-next-line no-console
console.log("types: ", types)

saveTypes(types)

  1. Перехватчик useT похож на useTranslation, использующий тип TranslationKey
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. Компонент T похож на компонент Trans
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. используйте useT и T с идентификаторами проверенных типов
const MyComponent = () => {
    const { t } = useT()


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

person madflanderz    schedule 10.12.2019

Завершенный набор текста с поиском ключа и интерполяцией на основе ответа @ ford04:

площадка

person ximage    schedule 21.06.2021

Я написал cli, который поддерживает создание файлов определения типа dts из нескольких конфигураций json. Можешь попробовать. В настоящее время расширенные типы ts 4 еще не полностью поддерживают функции i18next, поэтому я выбрал генерацию кода.

https://www.npmjs.com/package/@liuli-util/i18next-dts-gen

person rxliuli    schedule 29.06.2021