Product
|
Cloud costs
|
released
March 23, 2022
|
3
min read
|

Externalizing Strings in React - Part 2

Updated

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!

Sign up now

Sign up for our free plan, start building and deploying with Harness, take your software delivery to the next level.

Get a demo

Sign up for a free 14 day trial and take your software development to the next level

Documentation

Learn intelligent software delivery at your own pace. Step-by-step tutorials, videos, and reference docs to help you deliver customer happiness.

Case studies

Learn intelligent software delivery at your own pace. Step-by-step tutorials, videos, and reference docs to help you deliver customer happiness.

We want to hear from you

Enjoyed reading this blog post or have questions or feedback?
Share your thoughts by creating a new topic in the Harness community forum.

Sign up for our monthly newsletter

Subscribe to our newsletter to receive the latest Harness content in your inbox every month.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Platform