I have prepared a minimal test project at Github to demonstrate, what I have already tried myself:
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:
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:
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.
Ok, solved my problem by utilizing the "transform" and "generateBundle" hooks:
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: