» FrontAid CMS / Help

Generate I18N Key Types for TypeScript

Note: You can find the complete source code on our GitHub repository.

When localizing an application using JavaScript, you are most likely using a library for that. Many of them provide a function into which you can pass the (nested) key of localized text. The function is then responsible to map the key to the actual value.

import {t} from 'some-random-i18n-library';
t('index.meta.title');

That works nicely if you only have a couple of keys. But it gets annoying when you have many of them. And especially when using TypeScript, you probably also want to have proper code completion and type checks. Luckily that is very easy to achieve.

Assume, we have this little FrontAid Data Model.

{
  ":name": "A simple FrontAid Model",
  "name": {":type": "line"},
  "index": {
    "meta": {
      "title": {":type": "text"},
      "description": {":type": "text"}
    },
    "title": {":type": "text"},
    "body": {":type": "line", ":richtext": true},
  }
}

From that model, we will generate a TypeScript Union Type that will include all possible keys. Given the model from above, that would be the following type.

export type ContentKeys =
| 'name'
| 'index.meta.title'
| 'index.meta.description'
| 'index.title'
| 'index.body'

To achieve that, you can use the function below. It iterates recursively through our Model object and returns a nested list of arrays. Something like [['name'], ['index','meta','title'], ...]. Given that data structure, we can easily generate the expected content for the ContentKeys type.

const isUserProp = key => !key.startsWith(':');
const isListNode = node => ':items' in node;
const isFieldNode = node => ':type' in node;

function getNodePaths(model) {
  const paths = [];
  for (const prop in model) {
    const node = model[prop];
    if (isUserProp(prop) && !isListNode(node)) {
      if (isFieldNode(node)) {
        paths.push([prop]);
      } else {
        getNodePaths(node).forEach(subPaths => paths.push([prop, ...subPaths]));
      }
    }
  }
  return paths;
}

const nodePathsAsNestedArray = getNodePaths(model);
const nodePathsAsTypeScript = [
    `export type ContentKeys =`,
    ...nodePathsAsNestedArray.map(path => `| '${path.join('.')}'`),
].join('\n');

After concatenating those nested arrays into a single string, you can write it to a file and use it with TypeScript. Depending on which localization library you are using, you can pass that type into it or you might have to patch it accordingly yourself. Either way, you should be able to get code completion and type checks with this approach for easy localization of your application.

More code can be found in our GitHub repository.