programing

TypeScript로 check i18n 사전을 타이핑하는 방법은 무엇입니까?

linuxpc 2023. 6. 15. 21:38
반응형

TypeScript로 check i18n 사전을 타이핑하는 방법은 무엇입니까?

react-i18 next 사전에 기존 키 확인을 타이핑할 수 있습니까?키가 존재하지 않을 경우 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개봉?API가 있습니까?react-i18next어떻게든 도서관을 확장해서 그것을 가능하게 할 수 있을까요?래퍼 함수를 만드는 것을 피하고 싶습니다.

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.)을 제공하는 방법.

키 조회: 반환 유형

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

놀이터.

키 조회: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"

놀이터.

자세한 내용은 중첩된 개체의 유형 스크립트: 키를 참조하십시오.

조합이 복잡하고 사전 개체 모양에 따라 컴파일러 재귀 깊이 한계에 도달할 수 있습니다.보다 가벼운 대안: 전류 입력에 따라 점진적으로 다음 키 경로에 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
            // fix issue allowed last dot
            ? T[I1] extends object
                ? `${I1}.${DeepKeys<T[I1], I2>}`
                : keyof T & string
            : 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 */ }

// IntelliSense suggestions and compile errors!
// Press Ctrl+Space just outside the string, inside parentheses
t("f"); // error, suggests "footer" | "header"
t("footer"); // OK
t("footer."); // error, suggests "footer.copyright"
t("footer.copyright"); // OK
t("header.") // error, suggests "header.logo" | "header.link"
t("footer.copyright."); // error, suggests "footer.copyright"

놀이터.

보간

다음은 문자열 보간사용하는 예제입니다.

// 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

예:

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또는 독립적으로.



구답

(PRETS 4.1) 강력한 입력 키를 사용할 수 없는 두 가지 이유가 있습니다.react-i18next:

1.) TypeScript는 다음과 같은 동적 또는 계산 문자열 식을 평가할 수 있는 방법이 없습니다.'footer.copyright',하도록footer그리고.copyright변환 개체 계층 구조의 핵심 요소로 식별할 수 있습니다.

2.)는 정의된 사전/설명에 형식 제약 조건을 적용하지 않습니다.대신 함수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

놀이터.

여러 오버로드 서명(플레이그라운드) 대신 이러한 유형을 자동으로 추론할 수 있습니다.TS 4.1까지는 핵심 개발자생산할 때 이러한 재귀 유형을 권장하지 않습니다.

이제 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 filejson 파일에서 .

이 동작을 수행하는 또 다른 방법은 translationKey 유형을 생성하여 useThook 및 사용자 지정 Trans 구성 요소에서 사용하는 것보다 사용하는 것입니다.

  1. translation.json 파일 생성
{
  "PAGE_TITLE": "Product Status",
  "TABLES": {
    "COUNTRY": "Country",
    "NO_DATA_AVAILABLE": "No price data available"
  }
}
  1. generateTranslationKey를 사용하여 typeTranslationKey를 생성합니다.유형.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. useThook 유사한 ThookTranslationKey 유형을 사용하는 Translation
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를 유형 확인된 ID와 함께 사용합니다.
const MyComponent = () => {
    const { t } = useT()


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

훌륭한 답변 @ford04, 하지만 키와 인터폴 유형에 약간의 문제가 있습니다. 만약 당신이 이런 식으로 사용하고 문자열 끝에 변수가 없다면 인터폴은 그것을 식별하지 못할 것입니다.이 문제를 해결하려면 다음과 같은 방법으로 수행할 수 있습니다.

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;

예를 따릅니다.놀이터

여러 json 구성에서 dts 유형 정의 파일 생성을 지원하는 CLI를 작성했습니다.드셔보세요.현재 고급 타입의 ts 4는 i18 next의 기능을 완전히 지원하지 않아서 코드 생성을 선택했습니다.

https://www.npmjs.com/package/ @liuli-slot/i18 next-slot-gen

언급URL : https://stackoverflow.com/questions/58277973/how-to-type-check-i18n-dictionaries-with-typescript

반응형