In large projects there are a lot of texts that should be localized. And it might be a mess to manage them all if they are just plainly added in a single resources file.
That's why I want to discuss how we can logically structure resources (strings) in SPFx solutions.

Out of the box

First, let's look at what SPFx solution proposes in terms of localization.
The localization files used by the solution are stored in the ./src/<webparts|extensions>/<name-of-solution>/loc folder.
This folder contains a TypeScript type definition file (loc/mystrings.d.ts) that informs TypeScript of the different strings included in the localized files. Using the information from this file, your code editor can provide you with IntelliSense when working with strings in code. Additionally, while building your project, TypeScript can verify that you're not referring to a string that hasn't been defined. And you can imagine how this file will look if the solution contains a lot of strings.
For each locale supported by your web part, there is also a plain JavaScript file (not TypeScript) named in lowercase after the locale (for example en-us.js) that contains the translated strings.
Mapping between specific module definition from TypeScript file and translated strings is done in config/config.json file in localizedResources section:

"localizedResources": {
    "YourSolutionStrings": "lib/webparts|extensions/yoursolution/loc/{locale}.js"
And usage of the localized resources in the code is as simple as importing the module and referencing specific property of the interface:
import * as strings from YourSolutionStrings;

Option 1: Nested objects in a single localization file

This option allows you to use single localization file for all the strings. Disadvantage of the approach is that IntelliSense feature will be lost and TypeScript compiler will not check second level properties' names and won't inform about misspelling. It may lead to undefined texts in the solution.
To use nested objects do the following:
  • In type definition file (.d.ts) add property declaration with object type:
    AboutDialog: { [key: string]: string };
  • In locale JavaScript file describe all the properties for the object:
    "AboutDialog": {
        "Title": "About",
        "Edition": "Enterprise"
  • use strings in the code:

Option 2: Multiple localization files

This option requires to create separate files for different logical parts of the solutions. The good thing is that developer doesn't loose IntelliSense and misspelling errors notifications. The disadvantage is necessity to make separate request for each file in runtime.
To use multiple localization files:
  • Add as much type definition files and local JavaScript files as needed. For example, aboutDialog.d.ts and aboutDialog.en-us.js
  • dd strings to the files in the same manner as it is done for the default localization file in the project. Don't forget to use unique interface and module name:
    // d.ts
    declare interface IAboutDialogStrings {
        Title: string;
        Edition: string;
      declare module 'AboutDialogStrings' {
        const strings: IAboutDialogStrings;
        export = strings;
      // en-us.js
      define([], function () {
        return {
            "Title": "About",
            "Edition": "Enterprise"
  • Reference new localization files in config/config.json:
    "localizedResources": {
      "AboutDialogStrings": "lib/webparts/yourwebpart/loc/aboutDialog.{locale}.js"
  • Use new file (module) in the code:
    import * as aboutStrings from 'AboutDialogStrigs';
    // ...

Instead of conclusion

You can use any of these two approaches or combine them.
When selecting which one to use take into consideration the cons of the approaches.
Also, it doesn't make sense to complicate your life if you have few strings in the localization file ;)
You can find an example project that uses both approaches here:

Have fun!