How to generate multiple localized bundles using Vite and React?

134 Views Asked by At

I have prepared a minimal test project at Github to demonstrate, what I have already tried myself:

three buttons

My goal is to generate one bundle per supported language.

In other words, I do not want let users to switch languages at the runtime in my React app, because my real app is pretty large and country-specific.

Instead I would like to generate these 3 files and then serve them from different URLs at my backend:

  • dist/index-en.js
  • dist/index-de.js
  • dist/index-fr.js

So I have tried to create a custom Vite plugin vite-plugin-react-localize.js, which replaces __PLACEHOLDERS__, that can be seen in the above screenshot:

const localizedStrings = {
  en: {
    __YES__: "Yes",
    __NO__: "No",
    __CANCEL__: "Cancel",
  },
  de: {
    __YES__: "Ja",
    __NO__: "Nein",
    __CANCEL__: "Abbrechen",
  },
  fr: {
    __YES__: "Oui",
    __NO__: "Non",
    __CANCEL__: "Annuler",
  },
};

export default function localize(lang) {
  return {
    name: "localize-plugin",
    transform(code, id) {
      console.log(lang, id);
      return code.replaceAll(/__[A-Z]+__/g, function (match) {
        return localizedStrings[lang][match] || match;
      });
    },
  };
}

To activate the plugin I have added it to the vite-config.js:

const lastArg =
  process.argv.length > 0 ? process.argv[process.argv.length - 1] : "";
const matches = lastArg.match(/--lang=(en|de|fr)$/);
const lang = matches ? matches[1] : "en";

export default defineConfig({
  plugins: [react(), localize(lang)],
  build: {
    target: "es2015",
    rollupOptions: {
      output: {
        entryFileNames: `[name].js`,
        chunkFileNames: `[name].js`,
        assetFileNames: `[name].[ext]`,
      },
    },
  },
});

And finally, I have added the 3 build commands to the package.json:

  "scripts": {
    "dev": "vite",
    "build en": "vite build -- --lang=en",
    "build de": "vite build -- --lang=de",
    "build fr": "vite build -- --lang=fr",
    "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
    "preview": "vite preview"
  },

My approach kind of works now and the nice thing is that even the "dev" task preview is properly localized to the default "en" language. Here a screenshot of my VS Code:

VS Code screenshot

However I have not achieved the goal of being able to generate 3 different dist/index-*.js files.

And I have a feeling, that there must be a better way to approach this problem.

1

There are 1 best solutions below

0
On BEST ANSWER

Ok, solved my problem by utilizing the "transform" and "generateBundle" hooks:

localized screenshot

Here my custom vite-plugin-react-localize.js file:

"use strict";

import fs from "fs";
import path from "path";

const localizedStrings = {
  en: {
    __YES__: "Yes",
    __NO__: "No",
    __CANCEL__: "Cancel",
  },
  de: {
    __YES__: "Ja",
    __NO__: "Nein",
    __CANCEL__: "Abbrechen",
  },
  fr: {
    __YES__: "Oui",
    __NO__: "Non",
    __CANCEL__: "Annuler",
  },
};

function replacePlacesholders(src, lang) {
  return src.replaceAll(/__[A-Z]+__/g, function (match) {
    return localizedStrings[lang][match] || match;
  });
}

export default function localize(isBuildingBundle) {
  return {
    name: "localize-plugin",
    transform(src, id) {
      // replace placeholders in .jsx files, when not building the bundle
      return id.endsWith(".jsx") && !isBuildingBundle
        ? replacePlacesholders(src, "de")
        : src;
    },
    generateBundle(outputOptions, bundle) {
      for (const [fileName, bundleValue] of Object.entries(bundle)) {
        if (!fileName.endsWith("index.js")) {
          continue;
        }
        const indexJsPath = path.resolve(outputOptions.dir, fileName);
        console.log("\nReplacing placeholders in", indexJsPath);

        // create index-XX.js file for each language, in the same folder as index.js
        for (const lang of Object.keys(localizedStrings)) {
          const indexLangPath = path.resolve(
            outputOptions.dir,
            `index-${lang}.js`
          );
          console.log("Creating localized file", indexLangPath);
          fs.writeFileSync(
            indexLangPath,
            replacePlacesholders(bundleValue.code, lang)
          );
        }
      }
    },
  };
}

It is called with the help of the modified vite.config.js file:

import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import localize from "./vite-plugin-react-localize";

// is this a "vite build" command?
const isBuildingBundle =
  process.argv.length > 0 && process.argv[process.argv.length - 1] === "build";

export default defineConfig({
  plugins: [react(), localize(isBuildingBundle)],
  build: {
    target: "es2015",
    rollupOptions: {
      output: {
        entryFileNames: `[name].js`,
        chunkFileNames: `[name].js`,
        assetFileNames: `[name].[ext]`,
      },
    },
  },
});

Now the "vite build" works as I wanted and produces 3 additional files:

VS Code screenshot