@plone/scripts
Version:
Volto Core scripts package - Contains scripts and dependencies for these scripts for tooling when developing Plone 6 / Volto
361 lines (337 loc) • 10.8 kB
JavaScript
/* eslint no-console: 0 */
/**
* i18n script.
* @module scripts/i18n
*/
const { find, keys, map, concat, reduce } = require('lodash');
const glob = require('glob').sync;
const fs = require('fs');
const Pofile = require('pofile');
const babel = require('@babel/core');
const path = require('path');
const projectRootPath = path.resolve('.');
const { program } = require('commander');
const chalk = require('chalk');
/**
* Extract messages into separate JSON files
* @function extractMessages
* @return {undefined}
*/
function extractMessages() {
map(
// We ignore the existing customized shadowed components ones, since most
// probably we won't be overriding them
// If so, we should do it in the config object or somewhere else
// We also ignore the addons folder since they are populated using
// their own locales files and taken care separatedly in this script
glob('src/**/*.{js,jsx,ts,tsx}', {
ignore: ['src/customizations/**', 'src/addons/**'],
}),
(filename) => {
babel.transformFileSync(filename, {}, (err) => {
if (err) {
console.log(err);
}
});
},
);
}
/**
* Get messages from separate JSON files
* @function getMessages
* @return {Object} Object with messages
*/
function getMessages() {
return reduce(
concat(
{},
...map(
// We ignore the existing customized shadowed components ones, since most
// probably we won't be overriding them
// If so, we should do it in the config object or somewhere else
// We also ignore the addons folder since they are populated using
// their own locales files and taken care separatedly in this script
glob('build/messages/src/**/*.json', {
ignore: [
'build/messages/src/customizations/**',
'build/messages/src/addons/**',
],
}),
(filename) =>
map(JSON.parse(fs.readFileSync(filename, 'utf8')), (message) => ({
...message,
filename: filename.match(/build\/messages\/src\/(.*).json$/)[1],
})),
),
),
(current, value) => {
let result = current;
if (current.id) {
result = {
[current.id]: {
defaultMessage: current.defaultMessage,
filenames: [current.filename],
},
};
}
if (result[value.id]) {
result[value.id].filenames.push(value.filename);
} else {
result[value.id] = {
defaultMessage: value.defaultMessage,
filenames: [value.filename],
};
}
return result;
},
);
}
/**
* Convert messages to pot format
* @function messagesToPot
* @param {Object} messages Messages
* @return {string} Formatted pot string
*/
function messagesToPot(messages) {
return map(keys(messages).sort(), (key) =>
[
`#. Default: "${messages[key].defaultMessage.trim()}"`,
...map(messages[key].filenames, (filename) => `#: ${filename}`),
`msgid "${key}"`,
'msgstr ""',
].join('\n'),
).join('\n\n');
}
/**
* Pot header
* @function potHeader
* @return {string} Formatted pot header
*/
function potHeader() {
return `msgid ""
msgstr ""
"Project-Id-Version: Plone\\n"
"POT-Creation-Date: ${new Date().toISOString()}\\n"
"Last-Translator: Plone i18n <plone-i18n@lists.sourceforge.net>\\n"
"Language-Team: Plone i18n <plone-i18n@lists.sourceforge.net>\\n"
"Content-Type: text/plain; charset=utf-8\\n"
"Content-Transfer-Encoding: 8bit\\n"
"Plural-Forms: nplurals=1; plural=0;\\n"
"MIME-Version: 1.0\\n"
"Language-Code: en\\n"
"Language-Name: English\\n"
"Preferred-Encodings: utf-8\\n"
"Domain: volto\\n"
`;
}
/**
* Convert po files into json
* @function poToJson
* @return {undefined}
*/
function poToJson({ registry, addonMode }) {
const mergeMessages = (result, items, language) => {
items.forEach((item) => {
if (item.msgid in result) {
if (item.msgstr[0] !== '') {
result[item.msgid] = item.msgstr[0];
}
} else {
result[item.msgid] =
language === 'en'
? item.msgstr[0] ||
(item.comments[0] && item.comments[0].startsWith('. Default: ')
? item.comments[0].replace('. Default: ', '')
: item.comments[0] &&
item.comments[0].startsWith('defaultMessage:')
? item.comments[0].replace('defaultMessage: ', '')
: '')
: item.msgstr[0];
}
});
return result;
};
map(glob('locales/**/*.po'), (filename) => {
let { items } = Pofile.parse(fs.readFileSync(filename, 'utf8'));
const projectLocalesItems = Pofile.parse(
fs.readFileSync(filename, 'utf8'),
).items;
const lang = filename.match(/locales\/(.*)\/LC_MESSAGES\//)[1];
const result = {};
// Merge volto core locales
const lib = `node_modules/@plone/volto/${filename}`;
if (fs.existsSync(lib)) {
const libItems = Pofile.parse(fs.readFileSync(lib, 'utf8')).items;
items = [...libItems, ...items];
mergeMessages(result, items, lang);
}
if (!addonMode) {
// Merge addons locales - using getAddonDependencies because it preserves
// the order of the addons in the registry, even if they are add-on dependencies
// of an add-on
registry.getAddonDependencies().forEach((addonDep) => {
// What comes from getAddonDependencies is in the form of `@package/addon:profile`
const addon = addonDep.split(':')[0];
// Check if the addon is available in the registry, just in case
if (registry.packages[addon]) {
const addonlocale = `${registry.packages[addon].modulePath}/../${filename}`;
if (fs.existsSync(addonlocale)) {
const addonItems = Pofile.parse(
fs.readFileSync(addonlocale, 'utf8'),
).items;
mergeMessages(result, addonItems, lang);
if (require.main === module) {
// We only log it if called as script
console.log(`Merging ${addon} locales for ${lang}`);
}
}
}
});
}
// Merge project locales, the project customization wins
mergeMessages(result, projectLocalesItems, lang);
fs.writeFileSync(`locales/${lang}.json`, JSON.stringify(result));
});
}
/**
* Format header
* @function formatHeader
* @param {Array} comments Array of comments
* @param {Object} headers Object of header items
* @return {string} Formatted header
*/
function formatHeader(comments, headers) {
return [
...map(comments, (comment) => `#. ${comment}`),
'msgid ""',
'msgstr ""',
...map(keys(headers), (key) => `"${key}: ${headers[key]}\\n"`),
'',
].join('\n');
}
/**
* Sync po by the pot file
* @function syncPoByPot
* @return {undefined}
*/
function syncPoByPot() {
const pot = Pofile.parse(fs.readFileSync('locales/volto.pot', 'utf8'));
map(glob('locales/**/*.po'), (filename) => {
const po = Pofile.parse(fs.readFileSync(filename, 'utf8'));
fs.writeFileSync(
filename,
`${formatHeader(po.comments, po.headers)}
${map(pot.items, (item) => {
const poItem = find(po.items, { msgid: item.msgid });
return [
`#. ${item.extractedComments[0]}`,
`${map(item.references, (ref) => `#: ${ref}`).join('\n')}`,
`msgid "${item.msgid}"`,
`msgstr "${poItem ? poItem.msgstr : ''}"`,
].join('\n');
}).join('\n\n')}\n`,
);
});
}
function main({ addonMode }) {
console.log('Extracting messages from source files...');
extractMessages();
console.log('Synchronizing messages to pot file...');
// We only write the pot file if it's really different
const newPot = `${potHeader()}${messagesToPot(getMessages())}\n`.replace(
/"POT-Creation-Date:(.*)\\n"/,
'',
);
const oldPot = fs
.readFileSync('locales/volto.pot', 'utf8')
.replace(/"POT-Creation-Date:(.*)\\n"/, '');
if (newPot !== oldPot) {
fs.writeFileSync(
'locales/volto.pot',
`${potHeader()}${messagesToPot(getMessages())}\n`,
);
}
console.log('Synchronizing messages to po files...');
syncPoByPot();
if (!addonMode) {
let AddonRegistry, AddonConfigurationRegistry, registry;
try {
// Detect where is the registry (if we are in Volto 18 or above for either core and projects)
if (
fs.existsSync(
path.join(
projectRootPath,
'/node_modules/@plone/registry/dist/addon-registry/addon-registry.cjs',
),
)
) {
AddonRegistry = require(
path.join(
projectRootPath,
'/node_modules/@plone/registry/dist/addon-registry/addon-registry.cjs',
),
).AddonRegistry;
// Detect where is the registry (if we are in Volto 18-alpha.46 or below)
} else if (
fs.existsSync(
path.join(
projectRootPath,
'/node_modules/@plone/registry/src/addon-registry.js',
),
)
) {
AddonConfigurationRegistry = require(
path.join(
projectRootPath,
'/node_modules/@plone/registry/src/addon-registry',
),
);
} else {
// We are in Volto 17 or below
// Check if core Volto or project
if (
fs.existsSync(
path.join(projectRootPath, '/node_modules/@plone/volto'),
)
) {
// We are in a project
AddonConfigurationRegistry = require(
path.join(
projectRootPath,
'/node_modules/@plone/volto/addon-registry',
),
);
} else {
// We are in core (17 or below)
AddonConfigurationRegistry = require(
path.join(projectRootPath, 'addon-registry'),
);
}
}
} catch {
console.log(
chalk.red(
'Getting the addon registry failed. Are you executing i18n from inside an addon? Try the -a flag.',
),
);
process.exit();
}
console.log('Generating the language JSON files...');
if (AddonConfigurationRegistry) {
registry = new AddonConfigurationRegistry(projectRootPath);
} else if (AddonRegistry) {
registry = AddonRegistry.init(projectRootPath).registry;
}
poToJson({ registry, addonMode });
}
console.log('done!');
}
// This is the equivalent of `if __name__ == '__main__'` in Python :)
if (require.main === module) {
program.option('-a, --addon', 'run i18n script for addons');
program.parse(process.argv);
const options = program.opts();
main({ addonMode: options.addon });
}
module.exports = { poToJson };