Universe:i18n: How to dynamically load/import translation JSON files?

We’re migrating our Meteor app from tap-i18n to meteor-universe-i18n and I’m having a lot of trouble trying to dynamically load i18n JSON translation files. The package claims it’s a feature yet the documentation is quite lacking, doesn’t have an example, and seems to mention several different approaches.

The good news is everything is working as expected with the package if I load all the JSON translation files into the client at once on startup:

//  Client startup code

//  Import i18n files used in app.
import "../../i18n/app.ar.i18n.json";
import "../../i18n/app.bn.i18n.json";
import "../../i18n/app.da.i18n.json";
//  Plus twelve more languages...

All the callbacks work correctly and all the locales are automatically detected.

The problem is most of our users are English-speaking and adding the other fourteen JSON language files in the client at once adds about 5% onto the JS bundle size. So we want to dynamically load the JSON files when the respective language is selected.

The README mentions using JSON translation files to bypass calling i18n.addTranslation(). This is working fine when loading them all in the client at build time.

In the API section it mentions using import('../path/to/i18n') to dynamically import translation files. Our kneejerk implementation of this is to do this in the onChangeLocale callback (which is also client code):

i18n.onChangeLocale(function(newLocale) {
  (async () => {
      await import(`../../i18n/app.${newLocale}.i18n.json`);
      reactiveLanguage.set(newLocale);
  })();
});

This doesn’t seem to work because the dynamic import call import() doesn’t work if a variable string is passed. I’m assuming this is because if at build time, the compiler doesn’t know what to import than those files are not available to be imported. I’m not super-savvy with dynamic imports, but I thought this would retrieve the dynamically imported resource from the server?

I even tried moving the i18n JSON files to a /public/i18n folder and changing the code to:

await import(Meteor.absoluteUrl() + `i18n/app.${newLocale}.i18n.json`);

to see if that would serve them from the Meteor web server. But the dynamic import still fails saying it can’t find the resource. However I can click on the above link and see the JSON file load in my browser.

If dynamic import can only include things at runtime, what is the point of dynamic importing? What is it actually doing? I thought it kept any resources from being packed into the bundle and thus loads them dynamically from the server somehow.

Then there’s another API method called i18n.loadLocale(locale, params) that mentions loading translation files from a remove server. My translation files are on my Meteor server so I’m assuming not to go this route?

So far all of the above is all in client code. The README also alludes to another approach of having a server version of i18n that I assume loads all the JSON translation files on the server and then that can somehow “sync” to your client. There isn’t a lot of examples or explanation on how this works.

Anyone have a clue on how to do this?

Tagging some users that have posted in other i18n threads: @radekmie @waldgeist @zendranm

I am really curious if anyone out there found a “smart” way to solve this. Here is the hacky way we are doing this in some of our projects:

I am not happy with it, but I did a lot of research on the topic and could not find any other/better solution yet.

3 Likes

Ah, I see. Thank you sir.

This instantly made me think of doing it this way too:

i18n.onChangeLocale(function(newLocale) {
  (async () => {
      //  Check if locale isn't already loaded, if not...
      switch (newLocale) {
      case "fr":
        await import(`../../i18n/app.fr.i18n.json`);  
        break;
      case "de":
        await import(`../../i18n/app.de.i18n.json`);  
        break;
      //  And so on...
      }
      reactiveLanguage.set(newLocale);
  })();
});

When I do it like this, the bundle only includes the default English JSON file on load. Then indeed dynamically loads the other languages from the server at runtime (as observable in the Chrome Network log). Basically you have to hard-code all your languages and update your code when languages are added or removed. Indeed kind of clunky.

The Mozilla developer docs for import() specifically say that it should be useable for:

  • Modules that don’t exist at runtime
  • When the import specifier string needs to be constructed dynamically.

I wonder if the way Meteor dynamic importing works is at compile time it has to statically analyze what you may dynamically load and then place those in a special area on the server it builds. If the paths in the code are dynamic, then it has no idea what to make available for potential dynamic import?

1 Like

I’m not sure right now, but I think the following should work:

  1. Call addTranslations on the server (either import files statically only in the server files or call it directly, e.g., from the database).
  2. Call setLocale on the client.

It should automatically do a GET request (setLocale calls loadLocale indirectly) to the configured hostUrl. The default should just work, but you can configure a CDN there.

I was checking the official documentation and that is the proper/official way of doing it. (dynamic-import | Meteor API Docs)

1 Like

Both Meteor’s dynamic imports, and loading translations on the server and then calling setLocale on the client are valid solutions. I’ve updated the package’s readme to include these solutions.

1 Like