Software localization

Electron App Tutorial on Internationalization (i18n)

Want to set up internationalization (i18n) and localization (l10n) support in your Electron application? Let this Electron app tutorial be your guide!
Software localization blog category featured image | Phrase

Building an Electron application is not much different from building a Single Page application with React or Angular. There are a few obvious differences as the app itself will run on the client host computer but the main design elements are the same. Adding i18n and l10n has a few tricky bits that we need to take care of. In this Electron app tutorial, we are going to create our own Electron application with full i18n support both in the menu and in the content in a way that is efficient and scalable. We are going to use Electron + React for UI and i18next as the i18n provider.

For your convenience, you can also find the code in the GitHub repo.

Base Project Structure

Let's start by creating a base project structure that will enable a convenient way to localize our application content. Later on, we are going to add a language menu both in the toolbar and in our content and will show how we can do it without sacrificing clarity.

1. Create an npm project and add the initial packages:

$ mkdir phrase-app-electron-i18n && cd phrase-app-electron-i18n

$ npm init --yes

$ npm install --save electron react-scripts electron-devtools-installer cross-env webpack

2. Create a main.js filed and add the base electron boilerplate code to instantiate the application window and the close handlers:

$ touch main.js

$ mkdir -p src/configs/app.config

File: main.js

const electron = require('electron');

const path = require('path');

const url = require('url');

const { default: installExtension, REACT_DEVELOPER_TOOLS, REDUX_DEVTOOLS } =

  require('electron-devtools-installer');

const app = electron.app;

const BrowserWindow = electron.BrowserWindow;

const iconPath = path.join(__dirname, '/assets/phrase-app-icon.png');

let win;

function createAppWindow() {

  win = new BrowserWindow({

    width: 960,

    height: 600,

    'minWidth': 800,

    'minHeight': 600,

    icon: iconPath,

    title: config.title,

  });

  const baseUrl = `http://localhost:${config.port}`;

  // and load the index.html of the app.

  const startUrl = baseUrl || url.format({

    pathname: path.join(__dirname, '../build/index.html'),

    protocol: 'file:',

    slashes: true,

  });

  win.loadURL(startUrl);

  // Emitted when the window is closed.

  win.on('closed', function() {

    win = null;

  });

  installExtensions();

}

app.on('ready', createAppWindow);

// Quit when all windows are closed.

app.on('window-all-closed', function() {

  // On macOS it is common for applications and their menu bar

  // to stay active until the user quits explicitly with Cmd + Q

  if (process.platform !== 'darwin') {

    app.quit()

  }

});

app.on('activate', function() {

  // On macOS it's common to re-create a window in the app when the

  // dock icon is clicked and there are no other windows open.

  if (win === null) {

    createAppWindow()

  }

});

function installExtensions() {

  if (process.env.NODE_ENV === 'development') {

    // Open the DevTools.

    win.webContents.openDevTools();

    // Install extensions

    installExtension(REACT_DEVELOPER_TOOLS)

      .then(name => console.log(`Added Extension:  ${name}`))

      .catch(err => console.log('An error occurred: ', err));

    installExtension(REDUX_DEVTOOLS)

      .then(name => console.log(`Added Extension:  ${name}`))

      .catch(err => console.log('An error occurred: ', err));

  }

}

File: src/configs/app.config.js

module.exports = {

  platform: process.platform,

  port: process.env.PORT ? process.env.PORT : 3000,

  title: 'PhraseApp Electron i18n'

};

We are just opening a window with some default dimensions and we attach some exit handlers for each OS. We also enable some developer tools that will help us when we develop our React Renderer process.

3. Update the package.json  to include starting scripts:

File: package.json

{

  "homepage": "./",

  "name": "phrase-app-electron-i18n",

  "version": "1.0.0",

  "description": "",

  "main": "main.js",

  "scripts": {

    "electron": "cross-env NODE_ENV=development electron .",

    "build": "react-scripts build",

    "start": "react-scripts start",

    "test": "react-scripts test --env=jsdom"

  },

  "keywords": [],

  "author": "Theo Despoudis",

  "license": "ISC",

  "build": {

    "appId": "com.phraseApp.phraseApp-i18n-electron",

    "win": {

      "iconUrl": "https://cdn2.iconfinder.com/data/icons/designer-skills/128/react-256.png"

    },

    "directories": {

      "buildResources": "public"

    }

  },

  "devDependencies": {

    "electron": "^1.8.4",

    "electron-devtools-installer": "^2.2.3",

    "react-scripts": "^1.1.4",

    "webpack": "^3.11.0",

    "cross-env": "^5.1.4"

  }

}

Now if you execute in the command line the following command:

$ npm run electron

It will open an empty window because we have no content to display. In order to display some content, we need to create a frontend application code that will be rendered in the Electron's Renderer process. Let's do that with the help of React.js.

React Frontend

Electron uses Chromium for displaying web pages, so we need a GUI for that. Let's create a simple demo page to test the view:

1. Add React's dependencies:

$npm install react react-dom react-hot-loader --save

2. Create an index.js  that will be used to bootstrap our React application. Since we are using react-scripts we need to place it in an src  folder:

$ touch src/index.js

$ touch src/index.css

File: src/index.js

import React from 'react';

import ReactDOM from 'react-dom';

import App from './App';

import './index.css';

ReactDOM.render(

  <App />,

  document.getElementById('root')

);

File: src/index.css

body {

    margin: 0;

    padding: 0;

    font-family: sans-serif;

}

3. Add a component that will like to display:

$ touch src/App.js

$ touch src/App.css

File: src/App.js

import React, { Component } from 'react';

import logo from './assets/phrase-app-icon.png';

import './App.css';

class App extends Component {

  render() {

    return (

      <div className="App">

        <div className="App-header">

          <img src={logo} className="App-logo" alt="logo" />

          <h2>Welcome to PhraseApp</h2>

        </div>

        <p className="App-intro">Welcome PhraseApp!</p>

      </div>

    );

  }

}

export default App;

File: src/App.css

.App {

    text-align: center;

}

.App-logo {

    animation: App-logo-spin infinite 20s linear;

    height: 80px;

}

.App-header {

    background-color: #0091EB;

    height: 150px;

    padding: 20px;

    color: white;

}

.App-intro {

    font-size: large;

}

@keyframes App-logo-spin {

    from { transform: rotate(0deg); }

    to { transform: rotate(360deg); }

}

Let's run our app and see what we have got:

$ npm run electron

$ npm run start

[caption id="attachment_4260" align="aligncenter" width="837"] Image 1: Voilà! Our first Electron Application[/caption]

Now if you noticed, the menu of the application is preloaded with some default options that follow some OS guidelines for window layout. However, for our example, we would like to provide our own menu that will also include a drop-down of the available languages we support and the ability to change the language for the whole application. Let's implement that in the next section.

Adding The Menu

We can add our own customized menu by utilizing the Menu API. We need to create 2 separate menu types. One for Mac computers and one for Windows and Linux.

1.  Start by creating a MenuFactoryService that will handle our Menu operations:

$ mkdir -p src/services

$ touch src/services/menuFactory.js

File: src/services/menuFactory.js

const Menu = require('electron').Menu;

const config = require('../configs/app.config');

const darwinTemplate = require('../menus/darwinMenu');

const otherTemplate = require('../menus/otherMenu');

const menu = null;

const platform = process.platform;

function MenuFactoryService(menu) {

  this.menu = menu;

  this.buildMenu = buildMenu;

}

function buildMenu(app, mainWindow) {

  if (config.platform === 'darwin') {

    this.menu = Menu.buildFromTemplate(darwinTemplate(app, mainWindow));

    Menu.setApplicationMenu(this.menu);

  } else {

    this.menu = Menu.buildFromTemplate(otherTemplate(app, mainWindow));

    mainWindow.setMenu(this.menu)

  }

}

module.exports = new MenuFactoryService(menu);

The above class just calls the associated methods of the Electron's Menu API for each platform.

2. Create the menu functions for each platform:

$ mkdir -p src/menus

$ touch src/menus/darwinMenu.js

$ touch src/menus/otherMenu.js

File: src/menus/darwinMenu.js

const config = require('../configs/app.config');

module.exports = (app, mainWindow) => {

  let menu = [

    {

      label: 'PhraseApp i18n',

      submenu: [

        {

          label: 'About PhraseApp i18n',

          role: 'about'

        },

        {

          type: 'separator'

        },

        {

          label: 'Hide App',

          accelerator: 'Command+H',

          role: 'hide'

        },

        {

          label: 'Hide Others',

          accelerator: 'Command+Shift+H',

          role: 'hideothers'

        },

        {

          label: 'Show All',

          role: 'unhide'

        },

        {

          type: 'separator'

        },

        {

          label: 'Quit',

          accelerator: 'Command+Q',

          click: () => {

            app.quit();

          }

        }

      ]

    },

    {

      label: 'View',

      submenu: [

        {

          label: 'Reload',

          accelerator: 'Command+R',

          click: (item, focusedWindow) => {

            if (focusedWindow) {

              focusedWindow.reload();

            }

          }

        },

        {

          label: 'Full Screen',

          accelerator: 'Ctrl+Command+F',

          click: (item, focusedWindow) => {

            if (focusedWindow) {

              focusedWindow.setFullScreen(!focusedWindow.isFullScreen());

            }

          }

        },

        {

          label: 'Minimize',

          accelerator: 'Command+M',

          role: 'minimize'

        },

        {

          type: 'separator'

        },

        {

          label: 'Toggle Developer Tools',

          accelerator: 'Alt+Command+I',

          click: (item, focusedWindow) => {

            focusedWindow.webContents.toggleDevTools();

          }

        }

      ]

    },

    {

      label: 'Help',

      submenu: [

        {

          label: 'About App',

          click: function (item, focusedWindow) {

            if (focusedWindow) {

            }

          }

        }

      ]

    }

  ];

  return menu;

};

File: src/menus/otherMenu.js

module.exports = (app, mainWindow) => {

  let menu = [

    {

      label: '&File',

      submenu: [

        {

          label: '&Quit',

          accelerator: 'Ctrl+Q',

          click: function () {

            app.quit();

          }

        }

      ]

    },

    {

      label: 'View',

      submenu: [

        {

          label: 'Reload',

          accelerator: 'Command+R',

          click: function (item, focusedWindow) {

            focusedWindow.reload();

          }

        },

        {

          label: 'Full Screen',

          accelerator: 'Ctrl+Command+F',

          click: function (item, focusedWindow) {

            focusedWindow.setFullScreen(!focusedWindow.isFullScreen());

          }

        },

        {

          label: 'Minimize',

          accelerator: 'Command+M',

          role: 'minimize'

        },

        {

          type: 'separator'

        },

        {

          label: 'Toggle Developer Tools',

          accelerator: 'Alt+Command+I',

          click: function (item, focusedWindow) {

            focusedWindow.webContents.toggleDevTools();

          }

        }

      ]

    },

    {

      label: 'Help',

      submenu: [

        {

          label: 'About App',

          click: function (item, focusedWindow) {

            if (focusedWindow) {

            }

          }

        }

      ]

    }

  ];

  return menu;

};

If you run the application again you will see our new menu:

Next, we are going to see how we can add our locale switcher in the menu and change language on demand.

Switching Locale from the Menu

We have built our own menu and now we would like to have a locale switcher menu for changing languages. We are going to use i18next that will help us manage the translations.

1. Add the i18next libraries to the project

$ npm install --save i18next i18next-node-fs-backend

2. Create a config object that will host our configuration options for the i18next framework

$ touch src/configs/i18next.config.js

File: src/configs/i18next.config.js

const i18n = require('i18next');

const i18nextBackend = require('i18next-node-fs-backend');

const config = require('../configs/app.config');

const i18nextOptions = {

  backend:{

    // path where resources get loaded from

    loadPath: './locales/{{lng}}/{{ns}}.json',

    // path to post missing resources

    addPath: './locales/{{lng}}/{{ns}}.missing.json',

    // jsonIndent to use when storing json files

    jsonIndent: 2,

  },

  interpolation: {

    escapeValue: false

  },

  saveMissing: true,

  fallbackLng: config.fallbackLng,

  whitelist: config.languages,

  react: {

    wait: false

  }

};

i18n

  .use(i18nextBackend);

// initialize if not already initialized

if (!i18n.isInitialized) {

  i18n

    .init(i18nextOptions);

}

module.exports = i18n;

3. Update the app.config.js  to specify the list of supported locales

File: src/configs/app.config.js

module.exports = {

  platform: process.platform,

  port: process.env.PORT ? process.env.PORT : 3000,

  title: 'PhraseApp Electron i18n',

  languages: ['el', 'en'],

  fallbackLng: 'en',

  namespace: 'translation'

};

Here we define our list of supported locales and the default locale. The namespace field will match the file names that the i18next library will seek in order to load our translations.

4. Create the locale folders but leave the files empty for now

$ mkdir -p locales/el

$ mkdir -p locales/en

$ touch locales/el/translation.json

$ touch locales/el/translation.missing.json

$ touch locales/en/translation.json

$ touch locales/el/translation.missing.json

The missing files will be populated with entries that the i18next library has not found translations for that key yet so it's handy for translators.

Now we can request our i18n object by just calling:

const i18n = require('./src/configs/i18next.config');

However, there is one caveat here. The i18n object has to initialize first before the first usage as it needs to apply the config we provided. Luckily for us, the i18next library exposes a list of events that dispatches on certain occasions. Out of the possible options we are interested in the onLoaded event that gets fired when we loaded resources. We can pair it up with the onLanguageChanged to build our translated menu.

1. Subscribe that event in order to load our initial translations properly.

File: main.js

const i18n = require('./src/configs/i18next.config');

const menuFactoryService = require('./src/services/menuFactory');

...

function createAppWindow() {

...

i18n.on('loaded', (loaded) => {

    i18n.changeLanguage('en');

    i18n.off('loaded');

  });

i18n.on('languageChanged', (lng) => {

    menuFactoryService.buildMenu(app, win, i18n);

  });

...

2. Update the menuFactoryService  to accept the i18n config object

File: src/services/menuFactory.js

...

function buildMenu(app, mainWindow) {

  if (config.platform === 'darwin') {

    this.menu = Menu.buildFromTemplate(darwinTemplate(app, mainWindow, i18n));

    Menu.setApplicationMenu(this.menu);

  } else {

    this.menu = Menu.buildFromTemplate(otherTemplate(app, mainWindow, i18n));

    mainWindow.setMenu(this.menu)

  }

}

...

3. Update the darwin and the other menu functions to utilize the i18n.t  translator function:

File: src/menus/darwinMenu.js

const config = require('../configs/app.config');

module.exports = (app, mainWindow, i18n) => {

  let menu = [

    {

      label: i18n.t('PhraseApp i18n'),

      submenu: [

        {

          label: i18n.t('About PhraseApp i18n'),

          role: 'about'

        },

        {

          type: 'separator'

        },

        {

          label: i18n.t('Hide App'),

          accelerator: 'Command+H',

          role: 'hide'

        },

        {

          label: i18n.t('Hide Others'),

          accelerator: 'Command+Shift+H',

          role: 'hideothers'

        },

        {

          label: i18n.t('Show All'),

          role: 'unhide'

        },

        {

          type: 'separator'

        },

        {

          label: i18n.t('Quit'),

          accelerator: 'Command+Q',

          click: () => {

            app.quit();

          }

        }

      ]

    },

    {

      label: i18n.t('View'),

      submenu: [

        {

          label: i18n.t('Reload'),

          accelerator: 'Command+R',

          click: (item, focusedWindow) => {

            if (focusedWindow) {

              focusedWindow.reload();

            }

          }

        },

        {

          label: i18n.t('Full Screen'),

          accelerator: 'Ctrl+Command+F',

          click: (item, focusedWindow) => {

            if (focusedWindow) {

              focusedWindow.setFullScreen(!focusedWindow.isFullScreen());

            }

          }

        },

        {

          label: i18n.t('Minimize'),

          accelerator: 'Command+M',

          role: 'minimize'

        },

        {

          type: 'separator'

        },

        {

          label: i18n.t('Toggle Developer Tools'),

          accelerator: 'Alt+Command+I',

          click: (item, focusedWindow) => {

            focusedWindow.webContents.toggleDevTools();

          }

        }

      ]

    },

    {

      label: i18n.t('Help'),

      submenu: [

        {

          label: i18n.t('About App'),

          click: function (item, focusedWindow) {

            if (focusedWindow) {

            }

          }

        }

      ]

    }

  ];

  const languageMenu = config.languages.map((languageCode) => {

      return {

        label: i18n.t(languageCode),

        type: 'radio',

        checked: i18n.language === languageCode,

        click: () => {

          i18n.changeLanguage(languageCode);

        }

      }

  });

  menu.push({

    label: i18n.t('Language'),

    submenu: languageMenu

  });

  return menu;

};

File: src/menus/otherMenu.js

module.exports = (app, mainWindow, i18n) => {

  let menu = [

    {

      label: i18n.t('&File'),

      submenu: [

        {

          label: i18n.t('&Quit'),

          accelerator: 'Ctrl+Q',

          click: function () {

            app.quit();

          }

        }

      ]

    },

    {

      label: 'View',

      submenu: [

        {

          label: i18n.t('Reload'),

          accelerator: 'Command+R',

          click: function (item, focusedWindow) {

            focusedWindow.reload();

          }

        },

        {

          label: i18n.t('Full Screen'),

          accelerator: 'Ctrl+Command+F',

          click: function (item, focusedWindow) {

            focusedWindow.setFullScreen(!focusedWindow.isFullScreen());

          }

        },

        {

          label: i18n.t('Minimize'),

          accelerator: 'Command+M',

          role: 'minimize'

        },

        {

          type: 'separator'

        },

        {

          label: i18n.t('Toggle Developer Tools'),

          accelerator: 'Alt+Command+I',

          click: function (item, focusedWindow) {

            focusedWindow.webContents.toggleDevTools();

          }

        }

      ]

    },

    {

      label: i18n.t('Help'),

      submenu: [

        {

          label: i18n.t('About App'),

          click: function (item, focusedWindow) {

            if (focusedWindow) {

            }

          }

        }

      ]

    }

  ];

  const languageMenu = config.languages.map((languageCode) => {

      return {

        label: i18n.t(languageCode),

        type: 'radio',

        checked: i18n.language === languageCode,

        click: () => {

          i18n.changeLanguage(languageCode);

        }

      }

  });

  menu.push({

    label: i18n.t('Language'),

    submenu: languageMenu

  });

  return menu;

};

Pay particular attention to the languageMenu function where we build a radio menu with the list of available locales and how we attach the handler to change the language.

4. Run the application to populate the missing.json . That will create the following list of translations.

File: locales/en/translation.missing.json

{

  "PhraseApp i18n": "PhraseApp i18n",

  "About PhraseApp i18n": "About PhraseApp i18n",

  "Hide App": "Hide App",

  "Hide Others": "Hide Others",

  "Show All": "Show All",

  "Quit": "Quit",

  "View": "View",

  "Reload": "Reload",

  "Full Screen": "Full Screen",

  "Minimize": "Minimize",

  "Toggle Developer Tools": "Toggle Developer Tools",

  "Help": "Help",

  "About App": "About App",

  "el": "Greek",

  "en": "English",

  "Language": "Language"

}

5. Copy the contents of this file for each language translation.json  files and provide translations

File: locales/el/translation.json

{

  "PhraseApp i18n": "PhraseApp i18n",

  "About PhraseApp i18n": "Σχετικά με το PhraseApp i18n",

  "Hide App": "Κρύψε την εφαρμογή",

  "Hide Others": "Κρύψε όλα τα παράθυρα",

  "Show All": "Εμφάνησε όλα τα παράθυρα",

  "Quit": "Έξοδος",

  "View": "Προβολή",

  "Reload": "Φόρτωση",

  "Full Screen": "Πλήρης Προβολή",

  "Minimize": "Μικρή Προβολή",

  "Toggle Developer Tools": "Toggle Developer Tools",

  "Help": "Βοήθεια",

  "About App": "Σχετικά με την εφαρμογή",

  "el": "Ελληνικά",

  "en": "Αγγλικά",

  "Language": "Γλώσσα"

}

6. Finally, run the application and try to switch language from the menu. You can download the following video clip to see how it looks like.

Now that we have our locale aware menu lets try to make our rest of the application to switch language when we select it from the menu.

Changing locale in the Application Content

We have our little menu with the ability to change locale but we need to be able to update the main content as well. We need to include additional code for the renderer process in order to apply the translations in the content as well.

1. Update the index.js  and the main.js  files to preload the initial translations on them. For that, we are going to use the ipcMain API to request the initial messages before we render the frontend application.

File: src/index/js

import React from 'react';

import ReactDOM from 'react-dom';

import App from './App';

import i18n from './configs/i18next.config.client';

import { I18nextProvider } from 'react-i18next';

import { ipcRenderer } from './exportHelpers';

import './index.css';

let initialI18nStore = ipcRenderer.sendSync('get-initial-translations');

ReactDOM.render(

  <I18nextProvider i18n={ i18n } initialI18nStore={ initialI18nStore } initialLanguage="en">

    <App />

  </I18nextProvider>,

  document.getElementById('root')

);

File main.js

const ipcMain = electron.ipcMain;

...

ipcMain.on('get-initial-translations', (event, arg) => {

  i18n.loadLanguages('en', (err, t) => {

    const initial = {

      'en': {

        'translation': i18n.getResourceBundle('en', config.namespace)

      }

    };

    event.returnValue = initial;

  });

});

2. Create the required files for the React.js Application

$ touch src/exportHelpers.js

$ touch src/configs/i18next.config.client.js

File: src/exportHelpers.js

const electron = window.require('electron');

const fs = electron.remote.require('fs');

export const ipcRenderer = electron.ipcRenderer;

export const remote = electron.remote;

File: src/configs/i18next.config.client.js

const i18n = require('i18next');

const reactI18nextModule = require('react-i18next').reactI18nextModule;

const config = require('../configs/app.config');

const i18nextOptions = {

  interpolation: {

    escapeValue: false

  },

  saveMissing: true,

  lng: 'en',

  fallbackLng: config.fallbackLng,

  whitelist: config.languages,

  react: {

    wait: false

  }

};

i18n

  .use(reactI18nextModule);

// initialize if not already initialized

if (!i18n.isInitialized) {

  i18n

    .init(i18nextOptions);

}

module.exports = i18n;

4. Update the App.js  to include the translatable strings and provide the translations for all missing languages.

File: src/App.js

import React, { Component } from 'react';

import logo from './assets/phrase-app-icon.png';

import { I18n } from 'react-i18next';

import i18n from './configs/i18next.config.client';

import './App.css';

class App extends Component {

  render() {

    return (

      <I18n>

        {

          (t, { i18n }) => (

            <div className="App">

              <div className="App-header">

                <img src={logo} className="App-logo" alt="logo" />

                <h2>{t('welcomeMessage')}</h2>

              </div>

              <p className="App-intro">

                {t('helloMessage')}

              </p>

            </div>

          )

        }

      </I18n>

  );

  }

}

export default App;

5. Run the Electron window and check to see that the content is translated correctly. However, there is a catch here. If you try to change the locale from the menu you will see that the content is not changed (it fallbacks to the translation keys instead). This happens because we have preloaded only one locale so far and we need the new language message catalog. Typically we would solve this using an i18next backend plugin but until now there is no plugin for Electron and we cannot use XHR.

Lucky for us there is a simple solution. We can utilize the webContents API to try to send from the main process the new message catalog when we change it from the menu. Then in the Renderer process, we can just call the addResourceBundle method and change locale. That way we can make sure we have all the correct translations on demand.

6. Add handlers for main.js  and index.js  to load the translations on demand.

File: main.js

...

i18n.on('languageChanged', (lng) => {

    menuFactoryService.buildMenu(app, win, i18n);

    win.webContents.send('language-changed', {

      language: lng,

      namespace: config.namespace,

      resource: i18n.getResourceBundle(lng, config.namespace)

    });

  });

...

File: src/index.js

import React from 'react';

import ReactDOM from 'react-dom';

import App from './App';

import i18n from './configs/i18next.config.client';

import { I18nextProvider } from 'react-i18next';

import { ipcRenderer } from './exportHelpers';

import './index.css';

let initialI18nStore = ipcRenderer.sendSync('get-initial-translations');

ipcRenderer.on('language-changed', (event, message) => {

  if (!i18n.hasResourceBundle(message.language, message.namespace)) {

    i18n.addResourceBundle(message.language, message.namespace, message.resource);

  }

  i18n.changeLanguage(message.language);

});

ReactDOM.render(

  <I18nextProvider i18n={ i18n } initialI18nStore={ initialI18nStore } initialLanguage="en">

    <App />

  </I18nextProvider>,

  document.getElementById('root')

);

7. Provide the missing translations

File: locales/el/translation.json

{

  "welcomeMessage": "Καλως ήλθατε στο PhraseApp.com i18n",

  "helloMessage": "Γιά σου PhraseApp",

  ...

}

File: locales/en/translation.json

{

  "welcomeMessage": "Welcome to PhraseApp.com i18n",

  "helloMessage": "Hello PhraseApp",

   ...

}

8. Run the Application now and try to change the locale from the menu.

Check the following video for a small demo:

Electron Menu Example

Extra points

I leave as an exercise for the reader the ability to change the locale from within the application. You can add 2 buttons, one for each language and call the changeLanguage  method. Make sure you request the message translations from the backend as well because you will stumble on the same issue as before.

Happy coding!

Phrase

Phrase supports many different languages and frameworks, including React.js and Node.js. It allows you to easily import and export translation data and search for any missing translations, which is really convenient. On top of that, you can collaborate with translators as it is much better to have professionally done localization for your website. If you’d like to learn more about Phrase, refer to our beginner's beginner's guide. You can also get a 14-day trial. So what are you waiting for?

Conclusion

Applications written in Electron have a better native feel compared to web versions and with the addition of i18n capabilities, you can expect to be more enjoyable for the users. In this tutorial, we've shown a few ways we can do that by developing a small application. This is by no means an exhaustive Electron app tutorial on internationalization as every application has different needs and scope requirements.

Interested in exploring software internationalization with different JavaScript frameworks? Check out some of our other guides: