March 23, 2022

Externalizing Strings in React - Part 2

Table of Contents

Continuing from Externalizing Strings in React - Part 1, we’ll look at leveraging TypeScript to provide static analysis, validation, and autocomplete.

First, we must write a node script. This will read a `YAML` file and generate types. For example, if we have the following YAML:

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

Then it should produce the following type:

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

Note that, for nested values, we’re generating the key using all of the keys of its parent (until we reach the root), separated by a `.`. This is a general convention followed in the JavaScript ecosystem and supported by libraries such as lodash.

Next, we can utilize this type within the `StringsContext` file and take advantage of TypeScript.

The following script should read the YAML file and generate the types:

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);

You can put this script in `scripts/generate-types.mjs` and run `node scripts/generate-types.mjs`. Furthermore, you should see `src/strings.types.ts` being written with the following content:

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

The script, in its current form, doesn’t handle all of the use cases/edge cases. You can enhance it, when required, and customize it according to your use case.

Now we can update the `StringsContext.tsx` to utilize the generated type `StringKeys`.

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

After this change, you should be able to utilize autocomplete and validation for presence strings using TypeScript.

Externalizing Strings, Pt. 2 - TypeScript
Externalizing Strings, Pt. 2 - TypeScript

Moreover, you can integrate the string generation into your build system. This will automate the generation of types whenever there is a change in the `strings.yaml` file. I've done it here using a vitejs plugin.

Conclusion

I hope you find this useful and will use it as a starting point for your own implementation. For those who missed Part 1, again, you can find it here: Externalizing Strings In React – Part 1.

Happy coding!

You might also like
No items found.

Similar Blogs

No items found.
Platform