Skip to content

Commit

Permalink
Update cockpit-*.js
Browse files Browse the repository at this point in the history
  • Loading branch information
imobachgs committed Jan 15, 2024
1 parent 2a3434a commit c7b0873
Show file tree
Hide file tree
Showing 2 changed files with 181 additions and 145 deletions.
263 changes: 141 additions & 122 deletions web/src/lib/cockpit-po-plugin.js
Original file line number Diff line number Diff line change
@@ -1,140 +1,159 @@
const path = require("path");
const glob = require("glob");
const fs = require("fs");
const gettext_parser = require('gettext-parser');
const Jed = require('jed');
const webpack = require('webpack');
import fs from "fs";
import glob from "glob";
import path from "path";

const srcdir = process.env.SRCDIR || path.resolve(__dirname, '..', '..');
import Jed from "jed";
import gettext_parser from "gettext-parser";

module.exports = class {
constructor(options) {
if (!options)
options = {};
this.subdir = options.subdir || '';
this.reference_patterns = options.reference_patterns;
this.wrapper = options.wrapper || 'cockpit.locale(PO_DATA);';
}
const config = {};

get_po_files(compilation) {
try {
const linguas_file = path.resolve(srcdir, "po/LINGUAS");
const linguas = fs.readFileSync(linguas_file, 'utf8').match(/\S+/g);
compilation.fileDependencies.add(linguas_file); // Only after reading the file
return linguas.map(lang => path.resolve(srcdir, 'po', lang + '.po'));
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
const DEFAULT_WRAPPER = 'cockpit.locale(PO_DATA);';

/* No LINGUAS file? Fall back to globbing.
* Note: we won't detect .po files being added in this case.
*/
return glob.sync(path.resolve(srcdir, 'po/*.po'));
function get_po_files() {
try {
const linguas_file = path.resolve(config.srcdir, "po/LINGUAS");
const linguas = fs.readFileSync(linguas_file, 'utf8').match(/\S+/g);
return linguas.map(lang => path.resolve(config.srcdir, 'po', lang + '.po'));
} catch (error) {
if (error.code !== 'ENOENT') {
throw error;
}
}

apply(compiler) {
compiler.hooks.thisCompilation.tap('CockpitPoPlugin', compilation => {
compilation.hooks.processAssets.tapPromise(
{
name: 'CockpitPoPlugin',
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
() => Promise.all(this.get_po_files(compilation).map(f => this.buildFile(f, compilation)))
);
});
/* No LINGUAS file? Fall back to globbing.
* Note: we won't detect .po files being added in this case.
*/
return glob.sync(path.resolve(config.srcdir, 'po/*.po'));
}

get_plural_expr(statement) {
try {
/* Check that the plural forms isn't being sneaky since we build a function here */
Jed.PF.parse(statement);
} catch (ex) {
console.error("bad plural forms: " + ex.message);
process.exit(1);
}

const expr = statement.replace(/nplurals=[1-9]; plural=([^;]*);?$/, '(n) => $1');
if (expr === statement) {
console.error("bad plural forms: " + statement);
process.exit(1);
}

return expr;
}

function get_plural_expr(statement) {
try {
/* Check that the plural forms isn't being sneaky since we build a function here */
Jed.PF.parse(statement);
} catch (ex) {
console.error("bad plural forms: " + ex.message);
process.exit(1);
}

build_patterns(compilation, extras) {
const patterns = [
// all translations for that page, including manifest.json and *.html
`pkg/${this.subdir}.*`,
];

// add translations from libraries outside of page directory
compilation.getStats().compilation.fileDependencies.forEach(path => {
if (path.startsWith(srcdir) && path.indexOf('node_modules/') < 0)
patterns.push(path.slice(srcdir.length + 1));
});

Array.prototype.push.apply(patterns, extras);

return patterns.map((p) => new RegExp(`^${p}:[0-9]+$`));
const expr = statement.replace(/nplurals=[1-9]; plural=([^;]*);?$/, '(n) => $1');
if (expr === statement) {
console.error("bad plural forms: " + statement);
process.exit(1);
}

check_reference_patterns(patterns, references) {
for (const reference of references) {
for (const pattern of patterns) {
if (reference.match(pattern)) {
return true;
return expr;
}

function buildFile(po_file, subdir, webpack_module, webpack_compilation, filename, filter) {
return new Promise((resolve, reject) => {
// Read the PO file, remove fuzzy/disabled lines to avoid tripping up the validator
const po_data = fs.readFileSync(po_file, 'utf8')
.split('\n')
.filter(line => !line.startsWith('#~'))
.join('\n');
const parsed = gettext_parser.po.parse(po_data, { defaultCharset: 'utf8', validation: true });
delete parsed.translations[""][""]; // second header copy

const rtl_langs = ["ar", "fa", "he", "ur"];
const dir = rtl_langs.includes(parsed.headers.Language) ? "rtl" : "ltr";

// cockpit.js only looks at "plural-forms" and "language"
const chunks = [
'{\n',
' "": {\n',
` "plural-forms": ${get_plural_expr(parsed.headers['Plural-Forms'])},\n`,
` "language": "${parsed.headers.Language}",\n`,
` "language-direction": "${dir}"\n`,
' }'
];
for (const [msgctxt, context] of Object.entries(parsed.translations)) {
const context_prefix = msgctxt ? msgctxt + '\u0004' : ''; /* for cockpit.ngettext */

for (const [msgid, translation] of Object.entries(context)) {
/* Only include msgids which appear in this source directory */
const references = translation.comments.reference.split(/\s/);
if (!references.some(str => str.startsWith(`pkg/${subdir}`) || str.startsWith(config.src_directory) || str.startsWith(`pkg/lib`)))
continue;

if (translation.comments.flag?.match(/\bfuzzy\b/))
continue;

if (!references.some(filter))
continue;

const key = JSON.stringify(context_prefix + msgid);
// cockpit.js always ignores the first item
chunks.push(`,\n ${key}: [\n null`);
for (const str of translation.msgstr) {
chunks.push(',\n ' + JSON.stringify(str));
}
chunks.push('\n ]');
}
}
chunks.push('\n}');

const wrapper = config.wrapper?.(subdir) || DEFAULT_WRAPPER;
const output = wrapper.replace('PO_DATA', chunks.join('')) + '\n';

const out_path = path.join(subdir ? (subdir + '/') : '', filename);
if (webpack_compilation)
webpack_compilation.emitAsset(out_path, new webpack_module.sources.RawSource(output));
else
fs.writeFileSync(path.resolve(config.outdir, out_path), output);
return resolve();
});
}

function init(options) {
config.srcdir = process.env.SRCDIR || './';
config.subdirs = options.subdirs || [''];
config.src_directory = options.src_directory || 'src';
config.wrapper = options.wrapper;
config.outdir = options.outdir || './dist';
}

function run(webpack_module, webpack_compilation) {
const promises = [];
for (const subdir of config.subdirs) {
for (const po_file of get_po_files()) {
if (webpack_compilation)
webpack_compilation.fileDependencies.add(po_file);
const lang = path.basename(po_file).slice(0, -3);
promises.push(Promise.all([
// Separate translations for the manifest.json file and normal pages
buildFile(po_file, subdir, webpack_module, webpack_compilation,
`po.${lang}.js`, str => !str.includes('manifest.json')),
buildFile(po_file, subdir, webpack_module, webpack_compilation,
`po.manifest.${lang}.js`, str => str.includes('manifest.json'))
]));
}
}
return Promise.all(promises);
}

export const cockpitPoEsbuildPlugin = options => ({
name: 'cockpitPoEsbuildPlugin',
setup(build) {
init({ ...options, outdir: build.initialOptions.outdir });
build.onEnd(async result => { result.errors.length === 0 && await run() });
},
});

export class CockpitPoWebpackPlugin {
constructor(options) {
init(options || {});
}

buildFile(po_file, compilation) {
compilation.fileDependencies.add(po_file);

return new Promise((resolve, reject) => {
const patterns = this.build_patterns(compilation, this.reference_patterns);

const parsed = gettext_parser.po.parse(fs.readFileSync(po_file), 'utf8');
delete parsed.translations[""][""]; // second header copy

// cockpit.js only looks at "plural-forms" and "language"
const chunks = [
'{\n',
' "": {\n',
` "plural-forms": ${this.get_plural_expr(parsed.headers['plural-forms'])},\n`,
` "language": "${parsed.headers.language}"\n`,
' }'
];
for (const [msgctxt, context] of Object.entries(parsed.translations)) {
const context_prefix = msgctxt ? msgctxt + '\u0004' : ''; /* for cockpit.ngettext */

for (const [msgid, translation] of Object.entries(context)) {
const references = translation.comments.reference.split(/\s/);
if (!this.check_reference_patterns(patterns, references))
continue;

if (translation.comments.flag && translation.comments.flag.match(/\bfuzzy\b/))
continue;

const key = JSON.stringify(context_prefix + msgid);
// cockpit.js always ignores the first item
chunks.push(`,\n ${key}: [\n null`);
for (const str of translation.msgstr) {
chunks.push(',\n ' + JSON.stringify(str));
}
chunks.push('\n ]');
}
}
chunks.push('\n}');

const output = this.wrapper.replace('PO_DATA', chunks.join('')) + '\n';

const lang = path.basename(po_file).slice(0, -3);
compilation.emitAsset(this.subdir + 'po.' + lang + '.js', new webpack.sources.RawSource(output));
resolve();
apply(compiler) {
compiler.hooks.thisCompilation.tap('CockpitPoWebpackPlugin', async compilation => {
const webpack = (await import('webpack')).default;
compilation.hooks.processAssets.tapPromise(
{
name: 'CockpitPoWebpackPlugin',
stage: webpack.Compilation.PROCESS_ASSETS_STAGE_ADDITIONAL,
},
() => run(webpack, compilation)
);
});
}
};
}
63 changes: 40 additions & 23 deletions web/src/lib/cockpit-rsync-plugin.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,49 @@
const child_process = require("child_process");
import child_process from "child_process";

module.exports = class {
constructor(options) {
if (!options)
options = {};
this.dest = options.dest || "";
this.source = options.source || "dist/";
const config = {};

function init(options) {
config.dest = options.dest || "";
config.source = options.source || "dist/";
config.ssh_host = process.env.RSYNC || process.env.RSYNC_DEVEL;

// ensure the target directory exists
if (process.env.RSYNC)
child_process.spawnSync("ssh", [process.env.RSYNC, "mkdir", "-p", "/usr/local/share/cockpit/"], { stdio: "inherit" });
// ensure the target directory exists
if (config.ssh_host) {
config.rsync_dir = process.env.RSYNC ? "/usr/local/share/cockpit/" : "~/.local/share/cockpit/";
child_process.spawnSync("ssh", [config.ssh_host, "mkdir", "-p", config.rsync_dir], { stdio: "inherit" });
}
}

apply(compiler) {
compiler.hooks.afterEmit.tapAsync('WebpackHookPlugin', (compilation, callback) => {
if (process.env.RSYNC) {
const proc = child_process.spawn("rsync", ["--recursive", "--info=PROGRESS2", "--delete",
this.source, process.env.RSYNC + ":/usr/local/share/cockpit/" + this.dest], { stdio: "inherit" });
proc.on('close', (code) => {
if (code !== 0) {
process.exit(1);
} else {
callback();
}
});
function run(callback) {
if (config.ssh_host) {
const proc = child_process.spawn("rsync", ["--recursive", "--info=PROGRESS2", "--delete",
config.source, config.ssh_host + ":" + config.rsync_dir + config.dest], { stdio: "inherit" });
proc.on('close', (code) => {
if (code !== 0) {
process.exit(1);
} else {
callback();
}
});
} else {
callback();
}
}

export const cockpitRsyncEsbuildPlugin = options => ({
name: 'cockpitRsyncPlugin',
setup(build) {
init(options || {});
build.onEnd(result => result.errors.length === 0 ? run(() => {}) : {});
},
});

export class CockpitRsyncWebpackPlugin {
constructor(options) {
init(options || {});
}

apply(compiler) {
compiler.hooks.afterEmit.tapAsync('WebpackHookPlugin', (_compilation, callback) => run(callback));
}
};
}

0 comments on commit c7b0873

Please sign in to comment.