Software localization

Full-Stack JavaScript I18n Step by Step

Follow our full-stack JavaScript i18n guide to learn how to use resources more efficiently when implementing your JS localization strategy.
Software localization blog category featured image | Phrase

In software development, there are two core concepts to consider when developing web applications:

  1. Handling the frontend part, which requires unique skills and techniques to offer the greatest user engagement and UI experience
  2. Handling the backend part that preserves all the valuable information and provides the services for the clients.

Both concepts are equally important for the end-user. Supporting new features like internationalization (i18n) and localization (l10n) needs to fit in with both sides seamlessly and efficiently.

A good strategy to manage the complexity of both parts is to have a full-stack solution, preferably in the same language—in this case, JavaScript. Then, you have the flexibility to re-use code either via configuration options, leveraging existing framework integration, or just using good communication language.

Read along for some efficient implementation solutions for full-stack JavaScript i18n.

🔗 Resource » Make sure to stop by our Ultimate Guide on JavaScript Localization for a deep dive into all the steps you need to make your JS apps ready for a global userbase.

Framework integration

The first logical step to have an efficient i18n solution is to adopt frameworks that offer a built-in i18n module. The idea is to avoid the unnecessary work of finding possible unsupported plugins that work with that framework or introducing breaking changes when upgrading those frameworks in the future.

Let's note some typical examples from full-stack frameworks that offer built-in i18n and l10n.

Next.js

Next.js is a modern server-side React framework suitable for quick prototyping and developing web applications. Since v10.00 you can have i18n-aware routing. This means that, when you provide a list of supported locales and navigate to a page with the locale prefix, the framework will determine the current locale and assign the locale context for that page.

You can examine this by creating the Next.js starter example with the following command:

npx create-next-app nextjs-blog --use-npm --example "https://github.com/vercel/next-learn-starter/tree/master/learn-starter"

Then, you need to create a new page inside the posts folder:

mkdir -p pages/posts

touch pages/posts/first-page.jsx

Then add the code for the page:

export default function FirstPost() {

    return <h1>First Post</h1>

}

For now, it renders a simple message. Before you start the server, you need to provide the locale configuration:

next.config.js

module.exports = {

    i18n: {

        locales: ['en-UK', 'el-GR'],

        defaultLocale: 'en-UK',

    },

}

Here, we define a list of available locales and a defaultLocale. Because this is a common configuration parameter seen elsewhere, you can re-use them in the frontend.

You can access the current locale, as well as the available locales, by using the useRouter:

import { useRouter } from 'next/router';

export default function FirstPost() {

    const router = useRouter();

    console.info(`Current Locale is: ${router.locale}`);

    console.info(`Available Locales is: ${router.locales}`)

    return <h1>First Post</h1>

}

If you start the server and navigate to the following route: http://localhost:3000/el-GR/posts/first-post, you can check the browser console for the following information:

Browser console | Phrase

Note that Next.js handles only the routing part and not the translation part. You will have to include a library here to render the right translation messages. However, allowing a common configuration, it can help you avoid any drifts when supporting new locales. We will see later on how i18next.js can integrate with Next.js to provde those translations.

Sails.js

Sails.js is a full-stack MVC framework for Node.js. It comes battery-included with many features like ORM support, REST API generation, and so on. It comes out of the box with i18n by inspecting the Accept: Language headers using the i18n-node-2 integration package.

You can start easily with Sails by installing the global helper tool first:

npm install sails -g

Then create a new project starter:

sails new sailsjs-blog

Chose the Web App option when prompted for a project type.

To configure the localization side of the project, you will need to modify the following config file:

sails.config.i18n

Change the contents of the file as follows:

module.exports.i18n = {

  locales: ['en', 'el'],

  defaultLocale: 'el',

  localesDirectory: 'config/locales'

};

As you can see, this is almost the same structure as the Next.js config, with an additional property for the path of localesDirectory, the directory where the translations are stored in JSON format. Inside it, you can add the following translation:

el.json

{

 "A new Sails app.": "Μια νέα εφαρμογή χρησιμοποιώντας Sails",

}

To view the translation, you need to wrap a string with the message key:

sailsjs-blog/views/pages/homepage.ejs

<h1 class="display-4 pb-5"> <%= __('A new Sails app.') %></h1>

Then you start the development server:

sails lift

You see the translated page by refreshing the page http://localhost:1337/.

Translated page | Phrase

If you want to share the i18n configuration with the client code, you can expose it in the views by adding it as local variables. You will need to add the following information to the hooks section:

sailsjs-blog/api/hooks/custom/index.js

before: {

        '/*': {

          skipAssets: true,

          fn: async function(req, res, next){

            var url = require('url');

            res.locals.localization = {

              availableLocales: ['en', 'el'],

              defaultLocale: 'en'

            }

...

}

Here, the localization object is defined on the server side and attached as a local parameter inside the request/response context. When the server renders the page, it has access to the view variables. This way, you can pass them along to the client side as globals. For example, let's feed this information into the client:

layout.ejs

<script>

      window.i18nConfig = {

        availableLocales: <%- JSON.stringify(localization.availableLocales) %>,

        defaultLocale: '<%= localization.defaultLocale %>'

      };

    </script>

This would be available in the client side as i18nConfig.

i18nconfig code | Phrase

Using those approaches can help you reuse these configuration parameters for both the server and the client. There are many frameworks in Node.js that have i18n modules but surprisingly enough, many don't. In those cases, you can try to integrate a library instead. Let's explore that option next.

Library integration

Not all frameworks offer an i18n solution, so the next logical step is to allow a library that works both in the front end and the backend. Here you have to seek to bridge both sides by operating a library that reuses part of configuration parameters when bootstrapping the program. The primary benefit is to avoid configuration drift and maintain a single point of truth when it comes to i18n. We explain one example with i18next.js and Next.js.

i18next

i18next.js is one of the most well known libraries for i18n in JavaScript. It has plugins that work in many frameworks like Express.js, React, Angular, Vue and so on.  You can use i18next as a de-facto tool for i18n. We can see an example using Next.js  that we saw before and integrate i18nextjs.

Next.js Integration

First, you want to install i18next and some required plugins that enable support for Next.js:

npm i i18next next-i18next --save

Then you need a specific config file alongside ...

next-i18next.config.js

module.exports = {

    i18n: {

        defaultLocale: 'el-GR',

        locales: ['en-UK', 'el-GR'],

    }

}

... and replace the original next.config.js by importing this object:

next.config.js

const { i18n } = require('./config/localeConfig');

module.exports = {

    i18n

}

You create translation messages inside the public folder so they are available in the Next.js server:

nextjs-blog/public/locales/el-GR/common.json

{

  "Welcome to React": "Χαίρετε React"

}

nextjs-blog/public/locales/en-UK/common.json

{

  "Welcome to React": "Welcome to React"

}

Then you need to add Next.js hooks for the base _app.js:

import { appWithTranslation } from 'next-i18next';

const MyApp = ({ Component, pageProps }) => <Component {...pageProps} />

export default appWithTranslation(MyApp)

Then, replace the main index.jsx component with the following contents:

index.jsx

import { useTranslation } from 'next-i18next'

import { serverSideTranslations } from 'next-i18next/serverSideTranslations';

export function MyComponent() {

    const { t } = useTranslation('common');

    return (

        <div>

            <div>{t('Welcome to React')}</div>

        </div>

    )

}

export const getStaticProps = async ({ locale }) => ({

    props: {

        ...await serverSideTranslations(locale, ['common']),

    },

})

export default MyComponent;

You can now start the development server and navigate to http://localhost:3000 and view the translation:

Localhost:3000 translation | Phrase

You can also see the same translation when you visit the localized route at http://localhost:3000/el-GR.

Unifying translations between the server and the client

If you want to unify translations between the server and the client so that the translation yields the same result in both server-rendered and client-rendered code, you will need to incorporate some common functionality using plugins. You want to have the translation files using a common format and accessible from both client and the server. Using the Next.js framework is easy as the translations are available in the public folder:

localePath './public/locales'

For any other case, you will need to experiment with some i18next plugins. For example, you can use the following configuration that works in the server:

server.js

import i18next from 'i18next';

import Backend from 'i18next-fs-backend';

import config from './i18nextConfig';

i18next.use(Backend).init(config);

And the following configuration that works on the client side:

client.js

import i18next from 'i18next';

import Fetch from 'i18next-fetch-backend';

import config from './i18nextConfig';

i18next

    .use(Fetch)

    .init(config);

The config file is common and is imported on both sides:

i18nextConfig.js

const i18nextConfig = {

    lng: 'el',

    supportedLngs: ['en', 'el'],

    debug: true,

    ns: 'translation',

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

}

export default i18nextConfig;

Both configurations point to the same locale resources folder, but they use different backends. The server uses the fs-backend to load translations from the filesystem and the client uses the fetch-backend that uses fetch to retrieve the translations from a public folder. Because both resource folders use the same config, the translations will be common when used in conjunction with the server and the client.

Using an extensible framework like i18next gives you great flexibility and re-using capabilities. Let's see now how to unify date formats.

Unifying number and date formatting

Similar to the examples with keeping translations in sync for both the client and the server, you can unify the number and date formatting as well. You can leverage the:

interpolation.format

parameter in the i18next config to provide common formatting of numbers or dates for both the client and the server. Here is what you can use to format numbers for currencies using the right locale. Let's see an example config that is used for both:

i18nextConfig.js

import moment from 'moment';

const i18nextConfig = {

    lng: 'el',

    supportedLngs: ['en', 'el'],

    ns: 'translation',

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

    interpolation: {

        format: function (value, format, lng) {

            if (format === 'uppercase')

                return value.toUpperCase();

            if (format === 'currency')

                return new Intl.NumberFormat(lng, {

                    style: 'currency',

                    currency: 'EUR'

                }).format(Number(value));

            if (value instanceof Date)

                return moment(value).format(format);

            return value;

        }

    },

}

export default i18nextConfig;

It is very important how you structure the format parameter for currency. Here, we've used a custom name called currency. Here is an example message:

locales/translation-en.json

{

  "key": "The amount is {{price, currency}}"

}

And:

locales/translation-el.json

{

  "key": "Το τελικό ποσό είναι {{price, currency}}"

}

When you provide the actual price value you can use the following call:

i18next.t('key', { price: 1000 })

This would match the price format and render the formatted currency for the current locale. This is what you will see for each locale:

en: The amount is €1,000.00

el: Το τελικό ποσό είναι 1.000,00 €

This would work both on the client and the server side, as the library API is the same.

In essence, when trying to adopt a library, you have to make sure that all the parts are working as expected, and there are no apparent limitations or caveats. A quick spike task can help reveal potential issues beforehand as an initial strategy. Later on you may have to refine it by keeping only the parts you need in a common configuration file and reduced duplicated code.

Concluding our full-stack JavaScript i18n guide

Developing full-stack applications with JavaScript allows you the flexibility to work in a web-native language that enjoys good support and a vast library ecosystem. If you want to be more efficient in implementing i18n and l10n features into your apps, you need to either use existing framework integrations or provide libraries that support both front-end and server-side configuration. This guide reviewed both options and how to leverage them in practice. If you're now ready to let your translator team work together on your multilingual app content, try Phrase, a cloud-based translation platform that provides high flexibility and integration support. Sign up today for a free 14-day trial.