2022年3月23日
Continuous Delivery
Continuous Integration
Reactで文字列を外部化するシリーズの第2弾!TypeScriptを活用する方法をご紹介します。「Reactで文字列を外部化する その1」に引き続き、TypeScriptを活用して静的解析、検証、オートコンプリートを提供することについて見ていきます。
「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を使って存在する文字列のオートコンプリートや検証を利用できるようになるはずです。
さらに、文字列の生成をビルドシステムに統合することができます。これにより、strings.yamlファイルに変更があるたびに、型の生成が自動化されます。ここではvitejsのpluginを使ってやってみました。
ぜひ、この記事を参考にしていただき、ご自身の実装の出発点にしていただければと思います。「その1」を見逃した方のために、再度リンクを貼っておきます。
Reactで文字列を外部化する その1
Happy Coding!
この記事はHarness社のウェブサイトで公開されているものをDigital Stacksが日本語に訳したものです。無断複製を禁じます。原文はこちらです。