Using the SFPx library component for simplifying the localization experience

September 6, 2019

One of the things hardest things when creating and maintaining a product is localization. It requires a lot of testing, validation and follow-up. In SharePoint Framework projects it is no different, maybe even a bit harder, as the localization files are created per component. This means that you might have to take care of duplicate localization keys and labels.

Back in July 2019, I wrote about a way to simplify the localization process by using only one localization file in your project.

Related article: Implementing localization file checks to prevent issues in SharePoint Framework solutions

I would still recommend this approach/solution when you are mainting only one project with multiple components. When your solutions eventually expands and exists of X number of components, it might be easier to split them up in separate projects. This splitting comes with a price, as that means that yet again you have to maintain multiple localization files over different projects.

Info: At Valo, we split up our projects/components based on their functionality and modules. Another reason why we do this is to speed up the project build times. For example, a project with five web parts is much slower than a project with only one web part.

When we first started creating our project(s) structure, we knew that localization was going to be difficult to maintain. That is why internally we created our own CLI and localization process which allows us to easily import and export all the localization keys and labels. All of this is done during our builds. The keys and labels are automatically propagated to the projects they belong to.

Info: During our development phase, we only have to take care of English keys/labels. The other localization files will automatically be generated by our localization process during the builds.

Example of our localization process (before we switched to the library component approach)
Example of our localization process (before we switched to the library component approach)

The Excel file in the above picture is used as our central location for managing all these keys and their corresponding labels. This approach works great, but I’m not going to lie. There is still some manual work required to check if you don’t have any duplicate keys or similarities which could be merged.

Looking for a better experience

As the product keeps on growing and new keys are required, we had to find a better solution. The most important thing of the new solution had to be an easy way to share the keys and labels. Another plus would be to easily check for duplicated keys.

Right at that time when I was looking for a better solutions, the long-awaited library component feature in SharePoint Framework became available in beta. This feature is now in GA since SPFx version 1.9 and provides a way of sharing code between other projects.

Info: Here you can find a tutorial of how you can create your first library component: library component tutorial.

Tip: Lerna is a great tool for managing projects which use the library component functionality.

As I already mentioned, Sharing is key for our localization story. We need that easy way of sharing keys through all our projects, so this makes the library component an ideal for what we were looking for.

How it’s implemented

The way it is implemented is by creating a new project with a library component, once you have such a project, do the following things:

  • Create a new loc folder in the library component folder (not necessary, but it separates your component source and the localization files)
  • Create an en-us.js with the following contents:
1
2
3
4
5
define([], function() {
  return {
    // Your keys
  };
});
  • Create a mystrings.d.ts file with the following contents:
1
2
3
4
5
6
7
declare interface ILibraryStrings {
}

declare module  'LibraryStrings' {
  const strings: ILibraryStrings;
  export = strings;
}
  • Add a localization reference in your config.json file
1
2
3
4
5
6
{
  ...
  "localizedResources": {
    "LibraryStrings": "lib/LocalizationLibrary/loc/{locale}.js"
  }
}
  • Once these things are in place, all we have to do is to update the code of the library component itself.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import * as strings from 'LibraryStrings';
import { LocaleKeys } from './loc/LocaleKeys';

export class LocalizationLibrary {

  /**
   * Retrieve the locale label for the specified key
   *
   * @param localeKey
   */
  public static getLocale(localeKey: LocaleKeys | string): string {
    return strings[localeKey] || `Locale key not defined`;
  }
}

As you can see, this is simple, but this is not all.

Normally when you are working with localization, you will add keys to the localization JS file and mystrings.d.ts file. To simplify this process, I created an extra script which runs during a build and creates an enum and generates all keys and comments to quickly see what the localization key will return. This means you no longer have to update the mystrings.d.ts file with all the keys as the enum will be used in the project.

Another advantage of generating the enum file is that you don’t have to worry about duplicate keys, as it can only have unique keys.

The script which I use is a gulp task in a created in a seperate file gulpfile.generatelocalkeys.js with the following content:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
const fs = require('fs');
const path = require('path');

function getKeyValuePairs(fileContents) {
  let localeKeyValue = [];
  // Check if file contents were passed
  if (fileContents) {
    // Find the position of the return statement
    const fileLines = fileContents.split("\n");
    const returnIdx = fileLines.findIndex(line => {
      const matches = line.trim().match(/(^return|{$)/gi);
      return matches !== null && matches.length >= 2;
    });

    // Check if the index has been found
    if (returnIdx !== -1) {
      // Loop over all the lines
      for (const line of fileLines) {
        const lineVal = line.trim();
        // Get the colon location
        const colonIdx = lineVal.indexOf(":");
        if (colonIdx !== -1) {
          const keyName = lineVal.substring(0, colonIdx);
          let keyValue = lineVal.substring((colonIdx + 1));
          keyValue = keyValue.trim();
          keyValue = stripQuotes(keyValue);

          // Add the key and value to the array
          if (keyName && keyValue) {
            localeKeyValue.push({
              key: stripQuotes(keyName),
              value: keyValue
            });
          }
        }
      }
    }
  }

  return localeKeyValue;
}

function stripQuotes(value) {
  // Strip the comma
  if (value.endsWith(",")) {
    value = value.substring(0, value.length - 1);
  }

  if ((value.startsWith(`'`) && value.endsWith(`'`)) ||
      (value.startsWith(`"`) && value.endsWith(`"`)) ||
      (value.startsWith("`") && value.endsWith("`"))) {
    return value.substring(1, value.length - 1);
  }

  return value;
}

const generateLocaleKeys = build.task('generateLocaleKeys', {
  execute: async (config) => {
    // IMPORTANT: Update this path or get the location from the config.json
    const srcLocPath = "./src/localizationLibrary/loc";
    const fileContents = fs.readFileSync(path.join(__dirname, `${srcLocPath}/en-us.js`), { encoding: "utf8" });
    if (fileContents) {
      const keyPairs = getKeyValuePairs(fileContents);
      if (keyPairs && keyPairs.length > 0) {
        let enumInfo = [];
        let keys = [];
        enumInfo.push(`export enum LocaleKeys {`);
        for (const keyPair of keyPairs) {
          keys.push(`  /**
   * Label value: "${keyPair.value}"
   */
  ${keyPair.key} = "${keyPair.key}"`)
        }
        enumInfo.push(keys.join(`,\n`))
        enumInfo.push(`}`);
        fs.writeFileSync(path.join(__dirname, `${srcLocPath}/LocaleKeys.ts`), enumInfo.join(`\n`));
      }
    }
  }
});

build.rig.addPreBuildTask(generateLocaleKeys);

To use this script, add in your gulpfile.js file the following line:

1
require('./gulpfile.generatelocalkeys.js');

Important: Be sure to put it before the build.initialize(gulp); line.

Once you have all of this in place and run a build/bundle, it will automatically generate the enum file.

Important: Make sure to update the file references in the gulp task to that of your project or write some extra code to automatically find the path from the config.json file.

When the localization key enum is generated. All we have to do in our projects when we want to add a localization key is the following:

1
LocalizationLibrary.getLocale(LocaleKeys.generalSave);
Localization library component in use with comments
Localization library component in use with comments

Besides that, the localization keys can now be shared in all your projects, there have to be less files loaded when loading a page. Normally you would have one localization file which requires to be loaded per web part. With this solution only one localization file will be loaded for all of them.

Happy coding

Comments

comments powered by Disqus