/* eslint-env node */ /* eslint no-console:"off" */ const { dest, parallel, series, src, watch } = require("gulp"); const child_process = require("child_process"); const fs = require("fs"); const os = require("os"); const path = require("path"); const util = require("util"); const stream = require("stream/promises"); const sass = require("gulp-sass")(require("sass")); const tildeImporter = require("node-sass-tilde-importer"); const rtlcss = require("gulp-rtlcss"); const sourcemaps = require("gulp-sourcemaps"); const autoprefixer = require("gulp-autoprefixer"); const concatPo = require("gulp-concat-po"); const exec = require("gulp-exec"); const merge = require("merge-stream"); const through2 = require("through2"); const Vinyl = require("vinyl"); const args = require("minimist")(process.argv.slice(2), { default: { "generate-pot": "always" }, }); const rename = require("gulp-rename"); const STAFF_CSS_BASE = "koha-tmpl/intranet-tmpl/prog/css"; const OPAC_CSS_BASE = "koha-tmpl/opac-tmpl/bootstrap/css"; var CSS_BASE = args.view == "opac" ? OPAC_CSS_BASE : STAFF_CSS_BASE; var sassOptions = { importer: tildeImporter, includePaths: [__dirname + "/node_modules", __dirname + "/../node_modules"], }; // CSS processing for development function css(css_base) { css_base = css_base || CSS_BASE; var stream = src(css_base + "/src/**/*.scss", { sourcemaps: true }); if (args.view == "opac") { stream = stream .pipe(sass(sassOptions).on("error", sass.logError)) .pipe(autoprefixer()) .pipe(dest(css_base)) .pipe(rtlcss()) .pipe( rename({ suffix: "-rtl", }) ) // Append "-rtl" to the filename. .pipe(dest(css_base, { sourcemaps: "./maps" })); } else { stream = stream .pipe(sass(sassOptions).on("error", sass.logError)) .pipe(autoprefixer()) .pipe(dest(css_base)) .pipe(rtlcss()) .pipe( rename({ suffix: "-rtl", }) ) // Append "-rtl" to the filename. .pipe(dest(css_base, { sourcemaps: "./maps" })); } return stream; } // CSS processing for production function build(css_base) { css_base = css_base || CSS_BASE; sassOptions.outputStyle = "compressed"; var stream = src(css_base + "/src/**/*.scss") .pipe(sass(sassOptions).on("error", sass.logError)) .pipe(autoprefixer()) .pipe(dest(css_base)) .pipe(rtlcss()) .pipe( rename({ suffix: "-rtl", }) ) // Append "-rtl" to the filename. .pipe(dest(css_base)); return stream; } function opac_css() { return css(OPAC_CSS_BASE); } function staff_css() { return css(STAFF_CSS_BASE); } const poTasks = { "marc-MARC21": { extract: po_extract_marc_marc21, create: po_create_marc_marc21, update: po_update_marc_marc21, }, "marc-UNIMARC": { extract: po_extract_marc_unimarc, create: po_create_marc_unimarc, update: po_update_marc_unimarc, }, "staff-prog": { extract: po_extract_staff, create: po_create_staff, update: po_update_staff, }, "opac-bootstrap": { extract: po_extract_opac, create: po_create_opac, update: po_update_opac, }, pref: { extract: po_extract_pref, create: po_create_pref, update: po_update_pref, }, messages: { extract: po_extract_messages, create: po_create_messages, update: po_update_messages, }, "messages-js": { extract: po_extract_messages_js, create: po_create_messages_js, update: po_update_messages_js, }, installer: { extract: po_extract_installer, create: po_create_installer, update: po_update_installer, }, "installer-MARC21": { extract: po_extract_installer_marc21, create: po_create_installer_marc21, update: po_update_installer_marc21, }, "installer-UNIMARC": { extract: po_extract_installer_unimarc, create: po_create_installer_unimarc, update: po_update_installer_unimarc, }, }; function getPoTasks() { let tasks = []; let all_tasks = Object.keys(poTasks); if (args.task) { tasks = [args.task].flat(Infinity); } else { return all_tasks; } let invalid_tasks = tasks.filter(function (el) { return all_tasks.indexOf(el) < 0; }); if (invalid_tasks.length) { console.error("Invalid task"); return []; } return tasks; } const poTypes = getPoTasks(); function po_extract_marc(type) { return src(`koha-tmpl/*-tmpl/*/en/**/*${type}*`, { read: false, nocase: true, }) .pipe( xgettext( "misc/translator/xgettext.pl --charset=UTF-8 -F", `Koha-marc-${type}.pot` ) ) .pipe(dest("misc/translator")); } function po_extract_marc_marc21() { return po_extract_marc("MARC21"); } function po_extract_marc_unimarc() { return po_extract_marc("UNIMARC"); } function po_extract_staff() { const globs = [ "koha-tmpl/intranet-tmpl/prog/en/**/*.tt", "koha-tmpl/intranet-tmpl/prog/en/**/*.inc", "koha-tmpl/intranet-tmpl/prog/en/xslt/*.xsl", "!koha-tmpl/intranet-tmpl/prog/en/**/*MARC21*", "!koha-tmpl/intranet-tmpl/prog/en/**/*UNIMARC*", "!koha-tmpl/intranet-tmpl/prog/en/**/*marc21*", "!koha-tmpl/intranet-tmpl/prog/en/**/*unimarc*", ]; return src(globs, { read: false, nocase: true }) .pipe( xgettext( "misc/translator/xgettext.pl --charset=UTF-8 -F", "Koha-staff-prog.pot" ) ) .pipe(dest("misc/translator")); } function po_extract_opac() { const globs = [ "koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt", "koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc", "koha-tmpl/opac-tmpl/bootstrap/en/xslt/*.xsl", "!koha-tmpl/opac-tmpl/bootstrap/en/**/*MARC21*", "!koha-tmpl/opac-tmpl/bootstrap/en/**/*UNIMARC*", "!koha-tmpl/opac-tmpl/bootstrap/en/**/*marc21*", "!koha-tmpl/opac-tmpl/bootstrap/en/**/*unimarc*", ]; return src(globs, { read: false, nocase: true }) .pipe( xgettext( "misc/translator/xgettext.pl --charset=UTF-8 -F", "Koha-opac-bootstrap.pot" ) ) .pipe(dest("misc/translator")); } const xgettext_options = "--from-code=UTF-8 --package-name Koha " + "--package-version= -k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 " + "-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ " + "-kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3 " + "-k -k$__ -k$__x -k$__n:1,2 -k$__nx:1,2 -k$__xn:1,2 " + "--force-po"; function po_extract_messages_js() { const globs = [ "koha-tmpl/intranet-tmpl/prog/js/vue/**/*.vue", "koha-tmpl/intranet-tmpl/prog/js/**/*.js", "koha-tmpl/opac-tmpl/bootstrap/js/**/*.js", ]; return src(globs, { read: false, nocase: true }) .pipe( xgettext( `xgettext -L JavaScript ${xgettext_options}`, "Koha-messages-js.pot" ) ) .pipe(dest("misc/translator")); } function po_extract_messages() { const perlStream = src(["**/*.pl", "**/*.pm"], { read: false, nocase: true, }).pipe(xgettext(`xgettext -L Perl ${xgettext_options}`, "Koha-perl.pot")); const ttStream = src( [ "koha-tmpl/intranet-tmpl/prog/en/**/*.tt", "koha-tmpl/intranet-tmpl/prog/en/**/*.inc", "koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt", "koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc", ], { read: false, nocase: true } ).pipe( xgettext( "misc/translator/xgettext-tt2 --from-code=UTF-8", "Koha-tt.pot" ) ); const headers = { "Project-Id-Version": "Koha", "Content-Type": "text/plain; charset=UTF-8", }; return merge(perlStream, ttStream) .pipe(concatPo("Koha-messages.pot", { headers })) .pipe(dest("misc/translator")); } function po_extract_pref() { return src( "koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/*.pref", { read: false } ) .pipe(xgettext("misc/translator/xgettext-pref", "Koha-pref.pot")) .pipe(dest("misc/translator")); } function po_extract_installer() { const globs = [ "installer/data/mysql/en/mandatory/*.yml", "installer/data/mysql/en/optional/*.yml", ]; return src(globs, { read: false, nocase: true }) .pipe( xgettext("misc/translator/xgettext-installer", "Koha-installer.pot") ) .pipe(dest("misc/translator")); } function po_extract_installer_marc(type) { const globs = `installer/data/mysql/en/marcflavour/${type}/**/*.yml`; return src(globs, { read: false, nocase: true }) .pipe( xgettext( "misc/translator/xgettext-installer", `Koha-installer-${type}.pot` ) ) .pipe(dest("misc/translator")); } function po_extract_installer_marc21() { return po_extract_installer_marc("MARC21"); } function po_extract_installer_unimarc() { return po_extract_installer_marc("UNIMARC"); } function po_create_type(type) { const access = util.promisify(fs.access); const exec = util.promisify(child_process.exec); const pot = `misc/translator/Koha-${type}.pot`; // Generate .pot only if it doesn't exist or --force-extract is given const extract = () => stream.finished(poTasks[type].extract()); const p = args["generate-pot"] === "always" ? extract() : args["generate-pot"] === "auto" ? access(pot).catch(extract) : args["generate-pot"] === "never" ? Promise.resolve(0) : Promise.reject( new Error( "Invalid value for option --generate-pot: " + args["generate-pot"] ) ); return p.then(function () { const languages = getLanguages(); const promises = []; for (const language of languages) { const locale = language .split("-") .filter(s => s.length !== 4) .join("_"); const po = `misc/translator/po/${language}-${type}.po`; const promise = access(po).catch(() => exec(`msginit -o ${po} -i ${pot} -l ${locale} --no-translator`) ); promises.push(promise); } return Promise.all(promises); }); } function po_create_marc_marc21() { return po_create_type("marc-MARC21"); } function po_create_marc_unimarc() { return po_create_type("marc-UNIMARC"); } function po_create_staff() { return po_create_type("staff-prog"); } function po_create_opac() { return po_create_type("opac-bootstrap"); } function po_create_pref() { return po_create_type("pref"); } function po_create_messages() { return po_create_type("messages"); } function po_create_messages_js() { return po_create_type("messages-js"); } function po_create_installer() { return po_create_type("installer"); } function po_create_installer_marc21() { return po_create_type("installer-MARC21"); } function po_create_installer_unimarc() { return po_create_type("installer-UNIMARC"); } function po_update_type(type) { const access = util.promisify(fs.access); const exec = util.promisify(child_process.exec); const pot = `misc/translator/Koha-${type}.pot`; // Generate .pot only if it doesn't exist or --force-extract is given const extract = () => stream.finished(poTasks[type].extract()); const p = args["generate-pot"] === "always" ? extract() : args["generate-pot"] === "auto" ? access(pot).catch(extract) : args["generate-pot"] === "never" ? Promise.resolve(0) : Promise.reject( new Error( "Invalid value for option --generate-pot: " + args["generate-pot"] ) ); return p.then(function () { const languages = getLanguages(); const promises = []; for (const language of languages) { const po = `misc/translator/po/${language}-${type}.po`; promises.push( exec( `msgmerge --backup=off --no-wrap --quiet -F --update ${po} ${pot}` ) ); } return Promise.all(promises); }); } function po_update_marc_marc21() { return po_update_type("marc-MARC21"); } function po_update_marc_unimarc() { return po_update_type("marc-UNIMARC"); } function po_update_staff() { return po_update_type("staff-prog"); } function po_update_opac() { return po_update_type("opac-bootstrap"); } function po_update_pref() { return po_update_type("pref"); } function po_update_messages() { return po_update_type("messages"); } function po_update_messages_js() { return po_update_type("messages-js"); } function po_update_installer() { return po_update_type("installer"); } function po_update_installer_marc21() { return po_update_type("installer-MARC21"); } function po_update_installer_unimarc() { return po_update_type("installer-UNIMARC"); } /** * Gulp plugin that executes xgettext-like command `cmd` on all files given as * input, and then outputs the result as a POT file named `filename`. * `cmd` should accept -o and -f options */ function xgettext(cmd, filename) { const filenames = []; function transform(file, encoding, callback) { filenames.push(path.relative(file.cwd, file.path)); callback(); } function flush(callback) { fs.mkdtemp(path.join(os.tmpdir(), "koha-"), (err, folder) => { const outputFilename = path.join(folder, filename); const filesFilename = path.join(folder, "files"); fs.writeFile(filesFilename, filenames.join(os.EOL), err => { if (err) return callback(err); const command = `${cmd} -o ${outputFilename} -f ${filesFilename}`; child_process.exec(command, err => { if (err) return callback(err); fs.readFile(outputFilename, (err, data) => { if (err) return callback(err); const file = new Vinyl(); file.path = path.join(file.base, filename); file.contents = data; callback(null, file); fs.rmSync(folder, { recursive: true }); }); }); }); }); } return through2.obj(transform, flush); } /** * Return languages selected for PO-related tasks * * This can be either languages given on command-line with --lang option, or * all the languages found in misc/translator/po otherwise */ function getLanguages() { if (Array.isArray(args.lang)) { return args.lang; } if (args.lang) { return [args.lang]; } const filenames = fs .readdirSync("misc/translator/po/") .filter(filename => filename.endsWith("-installer.po")) .filter(filename => !filename.startsWith(".")); const re = new RegExp("-installer.po"); languages = filenames.map(filename => filename.replace(re, "")); return Array.from(new Set(languages)); } exports.build = function (next) { build(); next(); }; exports.css = function (next) { css(); next(); }; exports.opac_css = opac_css; exports.staff_css = staff_css; exports.watch = function () { watch(OPAC_CSS_BASE + "/src/**/*.scss", series("opac_css")); watch(STAFF_CSS_BASE + "/src/**/*.scss", series("staff_css")); }; if (args["_"][0].match("po:") && !fs.existsSync("misc/translator/po")) { console.log( "misc/translator/po does not exist. You should clone koha-l10n there. See https://wiki.koha-community.org/wiki/Translation_files for more details." ); process.exit(1); } exports["po:create"] = parallel(...poTypes.map(type => poTasks[type].create)); exports["po:update"] = parallel(...poTypes.map(type => poTasks[type].update)); exports["po:extract"] = parallel(...poTypes.map(type => poTasks[type].extract));