webpack-translations-plugin
Version:
A plugin to create separate bundles for each of your supported languages, with reasonable defaults. #0CJS
208 lines (162 loc) • 5.88 kB
JavaScript
const fs = require('fs');
const { resolve, join, basename, extname } = require('path');
const { ExternalsPlugin } = require('webpack');
const SOURCE_FILE_LANGUAGE_CODE = 'source';
const OBJECT_FOR_ALL_LANGUAGES_KEY = 'objectForAllLanguages';
module.exports = class WebpackTranslationsPlugin {
constructor({
directory = 'translations',
fileNameBase = 'messages',
moduleName = 'translations',
development = false,
} = {}) {
this.directory = directory;
this.fileNameBase = fileNameBase;
this.moduleName = moduleName;
this.development = development;
this.MODULE_STRING_TO_BE_REPLACED = '__TRANSLATIONS_MODULE_STRING_TO_BE_REPLACED__';
}
apply(compiler) {
this.setPath(compiler.context);
this.resolveTranslationsModuleAsAStringToBeReplaced(compiler);
compiler.hooks.emit.tap('TranslationPlugin', compilation => {
const translatedAssets = this.createTranslatedAssets(compilation.assets);
// eslint-disable-next-line no-param-reassign
compilation.assets = {
...compilation.assets,
...translatedAssets,
};
});
}
setPath(context) {
this.path = resolve(context, this.directory);
}
resolveTranslationsModuleAsAStringToBeReplaced(compiler) {
new ExternalsPlugin('var', {
[this.moduleName]: `'${this.MODULE_STRING_TO_BE_REPLACED}'`,
}).apply(compiler);
}
createTranslatedAssets(assets) {
const scriptFileNames = getScriptFileNamesFromAssets(assets);
return this.createTranslatedAssetsForFileNames(scriptFileNames, assets);
}
createTranslatedAssetsForFileNames(fileNames, assets) {
return fileNames.reduce(
(assetsTillNow, fileName) => ({
...assetsTillNow,
...this.createTranslatedAssetsForFileName(fileName, assets),
}),
{},
);
}
createTranslatedAssetsForFileName(name, assets) {
const translationObjects = this.getTranslationObjects();
const fileContent = assets[name].source();
return translationObjects.reduce((assetsTillNow, object) => {
const translations = isTranslationObjectForAllLanguages(object)
? object[OBJECT_FOR_ALL_LANGUAGES_KEY]
: object;
const fileContentWithTranslations = this.addTranslationsToFileContent(
translations,
fileContent,
);
return {
...assetsTillNow,
[getFileNameForFileNameAndTranslationObject(name, object)]: {
source: () => fileContentWithTranslations,
size: () => fileContentWithTranslations.length,
},
};
}, {});
}
addTranslationsToFileContent(object, fileContent) {
const string = this.stringifyTranslationsObject(object);
return fileContent.replace(
new RegExp(`['"]${this.MODULE_STRING_TO_BE_REPLACED}['"]`, 'g'),
string,
);
}
stringifyTranslationsObject(object) {
const TEMPORARY_REPLACEMENT_STRING = '___TEMPORARY_REPLACEMENT___';
return this.development
? JSON.stringify(object)
.replace(/\\"/g, TEMPORARY_REPLACEMENT_STRING)
.replace(/"/g, '\\"')
.replace(new RegExp(TEMPORARY_REPLACEMENT_STRING, 'g'), `\\\\\\"`)
: JSON.stringify(object);
}
getTranslationObjects() {
const codes = this.getLanguageCodes();
if (codes.length === 0) {
// eslint-disable-next-line no-console
console.log(
`WebpackTranslationsPlugin: No translation files found matching ${join(
this.directory,
`${this.fileNameBase}.*.json`,
)}, creating a bundle with source file...`,
);
return [{ [SOURCE_FILE_LANGUAGE_CODE]: this.getTranslationsForSourceFile() }];
}
const objectsForEveryLanguage = codes.map(code => ({
[code]: this.getTranslationsForCode(code),
}));
const objectForAllLanguages = objectsForEveryLanguage.reduce(
(objectForAll, objectForLanguage) => {
const language = Object.keys(objectForLanguage)[0];
return {
...objectForAll,
[language]: objectForLanguage[language],
};
},
{},
);
return [...objectsForEveryLanguage, { [OBJECT_FOR_ALL_LANGUAGES_KEY]: objectForAllLanguages }];
}
getLanguageCodes() {
const fileNames = fs.readdirSync(this.path);
return fileNames.map(getLanguageCodeFromFileName).filter(Boolean);
}
getTranslationsForSourceFile() {
return this.getTranslationsForCode('');
}
getTranslationsForCode(code) {
const filePath = this.getTranslationsPathForCode(code);
return readJSON(filePath);
}
getTranslationsPathForCode(code) {
const fileName = code ? `${this.fileNameBase}.${code}.json` : `${this.fileNameBase}.json`;
return join(this.path, fileName);
}
};
function getScriptFileNamesFromAssets(assets) {
const assetFileNames = Object.keys(assets);
return assetFileNames.filter(fileName => extname(fileName) === '.js');
}
function getFileNameForFileNameAndTranslationObject(name, object) {
const nameWithoutExtension = basename(name, '.js');
if (
translationObjectContainsOnlyOneLanguage(object) &&
!isTranslationObjectForSource(object) &&
!isTranslationObjectForAllLanguages(object)
) {
const language = Object.keys(object)[0];
return `${nameWithoutExtension}.${language}.js`;
}
return `${nameWithoutExtension}.js`;
}
function translationObjectContainsOnlyOneLanguage(object) {
return Object.keys(object).length === 1;
}
function isTranslationObjectForAllLanguages(object) {
return Object.keys(object)[0] === OBJECT_FOR_ALL_LANGUAGES_KEY;
}
function isTranslationObjectForSource(object) {
return Object.keys(object)[0] === SOURCE_FILE_LANGUAGE_CODE;
}
function getLanguageCodeFromFileName(name) {
const parts = name.split('.');
return parts.length === 3 ? parts[1] : null;
}
function readJSON(path) {
return JSON.parse(fs.readFileSync(path, 'utf-8'));
}