Any large application at some point requires localization. So no wonder why there are so many frameworks that solve this particular issue. But is it possible to localize any kind of JS application without any frameworks? Yes it is. And let me show you how.
First of all, let's list the major requirements:
Use no frameworks
Use standard techniques, no hacks
Ideal code completion: ability to navigate from any place of the system to a particular string, autocompletion when you type, code navigation and static analysis must work
Manageability on a scale: resource files has to be duplicate-free, statically parseable, shippable to localization vendors
Ability to change locale on the fly in runtime without page reloads
Locale and UI must be synchronized in all open tabs immediately after locale changes
Now when requirements have been set we need to decide how to store localizations.
Some approaches use english strings as keys for other localizations like so (pardon my French, pun intended):
This is a no go for a truly large scale project. It does not scale well and it leads to enormous issues during refactoring since. It is also hard to harvest strings from JS files (something likewhere is a translation function and will be used as key to find proper equivalent in another language). Also code completion won't work, developer won't be able to understand which strings were used before to reuse them.
So we will use predefined keys instead:
With this we can refer to any string by its key.
But what about autocompletion if we store strings in JSON files? Not every IDE has autocompletion that is smart enough. Let's use plain old JS files and exports (although that's optional if your IDE can work with JSON well):
Usage in components
Now, how can we use it in components? Easily, just import it:
Of course such approach has a limitation: we can only use it in render methods or in functions that are called in runtime.
But what to do if we would want to create a class with some default props? We can do the following trick with the getter:
We just got the auto-completable usage of our strings. In pure JS.
But how to switch locale?
We can create a special micro-loader:
This file of course can be code-generated, but manual changes are easy and rare.
But how will this loader affect what we had before? We were importing en-US in all of the components, so how to swap to another locale?
JS Modules are singletons, so no matter how many times and where we import a module we will always get the same object. So all we need to do when we load locale is to overwrite en-US strings with something else:
Now all we need is to call thewhen we need to swap the in-memory locale and before everything, even . Also notice that this simple mechanism gives a free fallback to english if localized string won't be found. This simple example assume that you have flat key-value pairs, if you use complex objects you can take something like Lodash's and but we agreed not to use frameworks in this article, so we keep things simple.
Here's a small Gate component that will make sure nothing is rendered before localized strings are ready:
This whole schema works fine if you don't plan to swap locale on the fly without reloading the page. It's important to remember that in real life during the lifecycle of the app most likely some data has already been downloaded from the server. Best practices suggest that APIs should be locale-free (e.g. no translated strings, currencies, date formatting, etc.), but also there are lots of legit cases when backend logic is using user locale and/or produce pre-formatted data. In such case app has to redownload all API requests, which could be a quite challenging task.
It is much easier to just reload the whole app to make sure nothing is broken. How often users change their language preference? It is quite a rare occasion, so there has to be a good reason to make things more complicated.
And surely we will explore dynamic case too.
Dynamic locale switching, tab synchronization
Assume we have a good API, which serve raw data and we want to make the best user experience and allow to swap locale at any time. Moreover, it would be quite embarassing if locale has changed in one tab of the browser and some other tab still would have old locale until the page reload.
In order to achieve great UX we need to make sure React (or other framework) re-renders if in-memory string are changed. This means some sort of a pub-sub (publish-subscribe) pattern has to be utilized. Which has to work in multiple tabs.can be used as synchronization object, as it can store values between reloads and can send messages across tabs.
First of all, let's alter thecode to be able to send some event when locale is changed.
Please note that we need to dispatch event manually inside current tab sincewill appear only in other open tabs, not current one.
Next we need to create a subscription mechanism:
Obviously, there's still plenty of room for improvements. For example, we chose not to use any frameworks, but we can optimize the performance of straightforward subscriptions if we use state management tools like MobX or Redux to store locale and do synchronization.
Now let's create a link between this pub-sub and React components. We will do it both ways using HOC and Hooks.
React HOC for dynamic locale switching
HOC has to wrap the existing component and provide ways to read current locale and set desired locale:
Or in the above described Gate component:
React Hooks for dynamic locale switching
Same thing can be achieved with new React Hooks, but in a way less verbose way:
At this point we've achieved all the requirements we've set in the beginning of the article, let's recap what this setup is capable of:
Only Webpack/Parcel or any other ES-compatible bundler is needed, we used only standard documented ES features, no hacks
Key-based translation files, with autocompletion, code navigation, static analysis works fine
JS (or JSON) files can be shipped to vendors as is, no preprocessing needed
Locale can be switched in real time without page reload
Locale and UI are synchronized across all open tabs
And all of that using less that 100 LOC.