如何使用TypeScript输入检查i18n词典?

时间:2019-10-07 22:36:29

标签: typescript i18next react-i18next

是否可以在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以某种方式扩展库以启用它?我想避免创建包装器函数。

5 个答案:

答案 0 :(得分:3)

虽然我同意强类型键在i18next中非常有帮助,但有两个原因导致无法实现:

1。)TypeScript无法像'footer.copyright'那样评估dynamic/computed string expressions,因此footercopyright可以被识别为翻译对象层次结构中的关键部分。 / p>

2。)响应i18next useTranslation API与字符串一起使用(比较类型herehere),并且不对您定义的字典/译文施加类型依赖性。相反,t函数包含通用类型参数,如果没有手动指定,则默认为string或类似的扩展类型。


了解到您不想使用包装器,这只是我前段时间汇总的一个示例示例,它利用了Rest参数/元组。

已键入t函数:

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 ...
}

键入useTranslation挂钩:

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(".")) } }  
}

useTypedTranslation导入其他模块:

import { useTypedTranslation } from "./useTypedTranslation"

const App = () => {
  const { t } = useTypedTranslation()
  return <div>{t("footer", "copyright")}</div>
}

测试:

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

Playground

您可能可以通过infer自动recursive types这些类型,而不是上面的多个重载签名。但是in this case是TS团队does not recommend them for production,所以我在这里介绍后者。

希望,它会有所帮助。

答案 1 :(得分:1)

实现此行为的另一种方法是生成TranslationKey类型并使用它,而不是用在useT挂钩和自定义Trans组件中。

  1. 创建translation.json文件
{
  "PAGE_TITLE": "Product Status",
  "TABLES": {
    "COUNTRY": "Country",
    "NO_DATA_AVAILABLE": "No price data available"
  }
}
  1. 使用generateTranslationTypes.js生成类型TranslationKey
/**
 * 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挂钩,类似于使用TranslationKey类型的useTranslation
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. 与Trans组件相似的T组件
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与类型检查过的ID一起使用
const MyComponent = () => {
    const { t } = useT()


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

答案 2 :(得分:0)

React-i18next现在具有对此的内置支持。我找不到官方文档,但是source code中有一些有用的评论。

假设您的翻译是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')
  }
}

如果您使用的是multiple translation files,则需要将它们全部添加到“资源”界面,并以名称空间为键。

如果要从json文件导入翻译,请确保在"resolveJsonModule": true中设置tsconfig.json

答案 3 :(得分:0)

基于@ford04 answer 使用键查找和插值完成打字:

Playground

答案 4 :(得分:0)

我写了一个cli,支持从多个json配置生成dts类型定义文件。你可以试试看。目前ts 4的高级类型还没有完全支持i18next的特性,所以选择了代码生成。

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