2022年3月23日

Continuous Delivery

Continuous Integration

Reactで文字列を外部化する その2

Reactで文字列を外部化するシリーズの第2弾!TypeScriptを活用する方法をご紹介します。「Reactで文字列を外部化する その1」に引き続き、TypeScriptを活用して静的解析、検証、オートコンプリートを提供することについて見ていきます。

02.-Design_Blog-header-12-tablet.webp

「Reactで文字列を外部化する その1」に引き続き、TypeScriptを活用して静的解析、検証、オートコンプリートを提供することについて見ていきます。

 

まず、ノードスクリプトを書く必要があります。これはYAMLファイルを読み込んで、型を生成します。例えば、以下のようなYAMLがあったとします。

key1: value1
key2: value2
key3:
	key3_1: value3_1
	key3_2: value3_2

そうすると、次のような型が出るはずです。

 

type StringKey = 'key1' | 'key2' | 'key3.key3_1' | 'key3.key3_2'

 

ネストされた値については、(ルートに到達するまでの)親のキーを全て使って、「.(ピリオド)」で区切ってキーを生成していることに注意してください。これはJavaScriptのエコシステムでは一般的な慣習で、lodashのようなライブラリでサポートされています。

 

次に、この型をStringsContextファイル内で利用し、TypeScriptを活用することができます。

 

以下のスクリプトでYAMLファイルを読み込んで、型を生成する必要があります。

 

import fs from "fs";
import path from "path";
import yaml from "yaml";

/**
 * Loops over object recursively and generate paths to all the values
 * { foo: "bar", foo2: { key1: "value1", key2: "value2" }, foo3: [1, 2, 3] }
 * will give the result:
 *
 * ["foo", "foo2.key1", "foo2.key2", "foo3.0", "foo3.1", "foo3.2"]
 */
function createKeys(obj, initialPath = "") {
  return Object.entries(obj).flatMap(([key, value]) => {
    const objPath = initialPath ? `${initialPath}.${key}` : key;

if (typeof value === "object" && value !== null) {
      return createKeys(value, objPath);
    }

return objPath;
  });
}

/**
 * Reads input YAML file and writes the types to the output file
 */
async function generateStringTypes(input, output) {
  const data = await fs.promises.readFile(input, "utf8");
  const jsonData = yaml.parse(data);
  const keys = createKeys(jsonData);

const typesData = `export type StringKeys =\n  | "${keys.join('"\n  | "')}";`;

await fs.promises.writeFile(output, typesData, "utf8");
}

const input = path.resolve(process.cwd(), "src/strings.yaml");
const output = path.resolve(process.cwd(), "src/strings.types.ts");

generateStringTypes(input, output);

このスクリプトをscripts/generate-types.mjsに記述して、node scripts/generate-types.mjsを実行します。さらに、src/strings.types.tsが以下の内容で書き込まれているのが確認できるはずです。

 

export type StringKeys =
  | "homePageTitle"
  | "aboutPageTitle"
  | "homePageContent.para1"
  | "homePageContent.para2"
  | "homePageContent.para3"
  | "aboutPageContent.para1"
  | "aboutPageContent.para2"
  | "aboutPageContent.para3";

このスクリプトは、このままで全てのユースケース/エッジケースを処理できるわけではありません。必要があれば拡張し、ユースケースに合わせてカスタマイズしてください。

 

これで、生成された型StringKeysを利用するためにStringsContext.tsxを更新できます。

import React, { createContext } from "react";
import has from "lodash.has";
import get from "lodash.get";
import mustache from "mustache";

+ import type { StringKeys } from "./strings.types";

+ export type StringsMap = Record<StringKeys, string>;

- const StringsContext = createContext({} as any);
+ const StringsContext = createContext<StringsMap>({} as any);

export interface StringsContextProviderProps {
- data: Record<string, any>;
+ data: StringsMap;
}

export function StringsContextProvider(
  props: React.PropsWithChildren<StringsContextProviderProps>
) {
  return (
    <StringsContext.Provider value={props.data}>
      {props.children}
    </StringsContext.Provider>
  );
}

- export function useStringsContext(): Record<string, any> {
+ export function useStringsContext(): StringsMap {
  return React.useContext(StringsContext);
}

export interface UseLocaleStringsReturn {
- getString(key: string, variables?: any): string;
+ getString(key: StringKeys, variables?: any): string;
}

export function useLocaleStrings() {
  const strings = useStringsContext();

return {
-   getString(key: string, variables: any = {}): string {
+   getString(key: StringKeys, variables: any = {}): string {
      if (has(strings, key)) {
        const str = get(strings, key);

        return mustache.render(str, variables);
      }

      throw new Error(`Strings data does not have a definition for: "${key}"`);
    },
  };
}

export interface LocaleStringProps extends React.HTMLAttributes<any> {
- strKey: string;
+ strKey: StringKeys;
  as?: keyof JSX.IntrinsicElements;
  variables?: any;
}

export function LocaleString(props: LocaleStringProps): React.ReactElement {
  const { strKey, as, variables, ...rest } = props;
  const { getString } = useLocaleStrings();
  const Component = as || "span";

  return <Component {...rest}>{getString(strKey, variables)}</Component>;
}

この変更により、TypeScriptを使って存在する文字列のオートコンプリートや検証を利用できるようになるはずです。

image-66-1920w.png

さらに、文字列の生成をビルドシステムに統合することができます。これにより、strings.yamlファイルに変更があるたびに、型の生成が自動化されます。ここではvitejsのpluginを使ってやってみました

結論

ぜひ、この記事を参考にしていただき、ご自身の実装の出発点にしていただければと思います。「その1」を見逃した方のために、再度リンクを貼っておきます。
Reactで文字列を外部化する その1 

 

Happy Coding!


この記事はHarness社のウェブサイトで公開されているものをDigital Stacksが日本語に訳したものです。無断複製を禁じます。原文はこちらです。

 

Harnessに関するお問い合わせはお気軽にお寄せください。

お問い合わせ