是否可以在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以某种方式扩展库以启用它?我想避免创建包装器函数。
答案 0 :(得分:3)
虽然我同意强类型键在i18next中非常有帮助,但有两个原因导致无法实现:
1。)TypeScript无法像'footer.copyright'
那样评估dynamic/computed string expressions,因此footer
和copyright
可以被识别为翻译对象层次结构中的关键部分。 / p>
2。)响应i18next useTranslation
API与字符串一起使用(比较类型here和here),并且不对您定义的字典/译文施加类型依赖性。相反,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
您可能可以通过infer自动recursive types这些类型,而不是上面的多个重载签名。但是in this case是TS团队does not recommend them for production,所以我在这里介绍后者。
希望,它会有所帮助。
答案 1 :(得分:1)
实现此行为的另一种方法是生成TranslationKey类型并使用它,而不是用在useT挂钩和自定义Trans组件中。
{
"PAGE_TITLE": "Product Status",
"TABLES": {
"COUNTRY": "Country",
"NO_DATA_AVAILABLE": "No price data available"
}
}
/**
* 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)
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
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
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 使用键查找和插值完成打字:
答案 4 :(得分:0)
我写了一个cli,支持从多个json配置生成dts类型定义文件。你可以试试看。目前ts 4的高级类型还没有完全支持i18next的特性,所以选择了代码生成。