2 /* eslint no-console:"off" */
4 const { dest, parallel, series, src, watch } = require('gulp');
6 const child_process = require('child_process');
7 const fs = require('fs');
8 const os = require('os');
9 const path = require('path');
10 const util = require('util');
11 const stream = require('stream/promises');
13 const sass = require('gulp-sass')(require('sass'));
14 const rtlcss = require('gulp-rtlcss');
15 const sourcemaps = require('gulp-sourcemaps');
16 const autoprefixer = require('gulp-autoprefixer');
17 const concatPo = require('gulp-concat-po');
18 const exec = require('gulp-exec');
19 const merge = require('merge-stream');
20 const through2 = require('through2');
21 const Vinyl = require('vinyl');
22 const args = require('minimist')(process.argv.slice(2), { default: { 'generate-pot': 'always' } });
23 const rename = require('gulp-rename');
25 const STAFF_CSS_BASE = "koha-tmpl/intranet-tmpl/prog/css";
26 const OPAC_CSS_BASE = "koha-tmpl/opac-tmpl/bootstrap/css";
28 var CSS_BASE = args.view == "opac"
34 __dirname + '/node_modules',
35 __dirname + '/../node_modules'
39 // CSS processing for development
40 function css(css_base) {
41 css_base = css_base || CSS_BASE
42 var stream = src(css_base + "/src/**/*.scss")
43 .pipe(sourcemaps.init())
44 .pipe(sass(sassOptions).on('error', sass.logError))
46 .pipe(dest(css_base));
48 if (args.view == "opac") {
53 })) // Append "-rtl" to the filename.
54 .pipe(dest(css_base));
57 stream = stream.pipe(sourcemaps.write('./maps'))
58 .pipe(dest(css_base));
63 // CSS processing for production
64 function build(css_base) {
65 css_base = css_base || CSS_BASE;
66 sassOptions.outputStyle = "compressed";
67 var stream = src(css_base + "/src/**/*.scss")
68 .pipe(sass(sassOptions).on('error', sass.logError))
70 .pipe(dest(css_base));
72 if( args.view == "opac" ){
73 stream = stream.pipe(rtlcss())
76 })) // Append "-rtl" to the filename.
77 .pipe(dest(css_base));
84 return css(OPAC_CSS_BASE);
88 return css(STAFF_CSS_BASE);
93 extract: po_extract_marc_marc21,
94 create: po_create_marc_marc21,
95 update: po_update_marc_marc21,
98 extract: po_extract_marc_unimarc,
99 create: po_create_marc_unimarc,
100 update: po_update_marc_unimarc,
103 extract: po_extract_staff,
104 create: po_create_staff,
105 update: po_update_staff,
108 extract: po_extract_opac,
109 create: po_create_opac,
110 update: po_update_opac,
113 extract: po_extract_pref,
114 create: po_create_pref,
115 update: po_update_pref,
118 extract: po_extract_messages,
119 create: po_create_messages,
120 update: po_update_messages,
123 extract: po_extract_messages_js,
124 create: po_create_messages_js,
125 update: po_update_messages_js,
128 extract: po_extract_installer,
129 create: po_create_installer,
130 update: po_update_installer,
132 'installer-MARC21': {
133 extract: po_extract_installer_marc21,
134 create: po_create_installer_marc21,
135 update: po_update_installer_marc21,
137 'installer-UNIMARC': {
138 extract: po_extract_installer_unimarc,
139 create: po_create_installer_unimarc,
140 update: po_update_installer_unimarc,
144 function getPoTasks () {
147 let all_tasks = Object.keys(poTasks);
150 tasks = [args.task].flat(Infinity);
155 let invalid_tasks = tasks.filter( function( el ) {
156 return all_tasks.indexOf( el ) < 0;
159 if ( invalid_tasks.length ) {
160 console.error("Invalid task");
166 const poTypes = getPoTasks();
168 function po_extract_marc (type) {
169 return src(`koha-tmpl/*-tmpl/*/en/**/*${type}*`, { read: false, nocase: true })
170 .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -F', `Koha-marc-${type}.pot`))
171 .pipe(dest('misc/translator'))
174 function po_extract_marc_marc21 () { return po_extract_marc('MARC21') }
175 function po_extract_marc_unimarc () { return po_extract_marc('UNIMARC') }
177 function po_extract_staff () {
179 'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
180 'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
181 'koha-tmpl/intranet-tmpl/prog/en/xslt/*.xsl',
182 '!koha-tmpl/intranet-tmpl/prog/en/**/*MARC21*',
183 '!koha-tmpl/intranet-tmpl/prog/en/**/*UNIMARC*',
184 '!koha-tmpl/intranet-tmpl/prog/en/**/*marc21*',
185 '!koha-tmpl/intranet-tmpl/prog/en/**/*unimarc*',
188 return src(globs, { read: false, nocase: true })
189 .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -F', 'Koha-staff-prog.pot'))
190 .pipe(dest('misc/translator'))
193 function po_extract_opac () {
195 'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
196 'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
197 'koha-tmpl/opac-tmpl/bootstrap/en/xslt/*.xsl',
198 '!koha-tmpl/opac-tmpl/bootstrap/en/**/*MARC21*',
199 '!koha-tmpl/opac-tmpl/bootstrap/en/**/*UNIMARC*',
200 '!koha-tmpl/opac-tmpl/bootstrap/en/**/*marc21*',
201 '!koha-tmpl/opac-tmpl/bootstrap/en/**/*unimarc*',
204 return src(globs, { read: false, nocase: true })
205 .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -F', 'Koha-opac-bootstrap.pot'))
206 .pipe(dest('misc/translator'))
209 const xgettext_options = '--from-code=UTF-8 --package-name Koha '
210 + '--package-version= -k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 '
211 + '-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ '
212 + '-kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3 '
213 + '-k -k$__ -k$__x -k$__n:1,2 -k$__nx:1,2 -k$__xn:1,2 '
216 function po_extract_messages_js () {
218 'koha-tmpl/intranet-tmpl/prog/js/vue/**/*.vue',
219 'koha-tmpl/intranet-tmpl/prog/js/**/*.js',
220 'koha-tmpl/opac-tmpl/bootstrap/js/**/*.js',
223 return src(globs, { read: false, nocase: true })
224 .pipe(xgettext(`xgettext -L JavaScript ${xgettext_options}`, 'Koha-messages-js.pot'))
225 .pipe(dest('misc/translator'))
228 function po_extract_messages () {
229 const perlStream = src(['**/*.pl', '**/*.pm'], { read: false, nocase: true })
230 .pipe(xgettext(`xgettext -L Perl ${xgettext_options}`, 'Koha-perl.pot'))
232 const ttStream = src([
233 'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
234 'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
235 'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
236 'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
237 ], { read: false, nocase: true })
238 .pipe(xgettext('misc/translator/xgettext-tt2 --from-code=UTF-8', 'Koha-tt.pot'))
241 'Project-Id-Version': 'Koha',
242 'Content-Type': 'text/plain; charset=UTF-8',
245 return merge(perlStream, ttStream)
246 .pipe(concatPo('Koha-messages.pot', { headers }))
247 .pipe(dest('misc/translator'))
250 function po_extract_pref () {
251 return src('koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/*.pref', { read: false })
252 .pipe(xgettext('misc/translator/xgettext-pref', 'Koha-pref.pot'))
253 .pipe(dest('misc/translator'))
256 function po_extract_installer () {
258 'installer/data/mysql/en/mandatory/*.yml',
259 'installer/data/mysql/en/optional/*.yml',
262 return src(globs, { read: false, nocase: true })
263 .pipe(xgettext('misc/translator/xgettext-installer', 'Koha-installer.pot'))
264 .pipe(dest('misc/translator'))
267 function po_extract_installer_marc (type) {
268 const globs = `installer/data/mysql/en/marcflavour/${type}/**/*.yml`;
270 return src(globs, { read: false, nocase: true })
271 .pipe(xgettext('misc/translator/xgettext-installer', `Koha-installer-${type}.pot`))
272 .pipe(dest('misc/translator'))
275 function po_extract_installer_marc21 () { return po_extract_installer_marc('MARC21') }
277 function po_extract_installer_unimarc () { return po_extract_installer_marc('UNIMARC') }
279 function po_create_type (type) {
280 const access = util.promisify(fs.access);
281 const exec = util.promisify(child_process.exec);
283 const pot = `misc/translator/Koha-${type}.pot`;
285 // Generate .pot only if it doesn't exist or --force-extract is given
286 const extract = () => stream.finished(poTasks[type].extract());
288 args['generate-pot'] === 'always' ? extract() :
289 args['generate-pot'] === 'auto' ? access(pot).catch(extract) :
290 args['generate-pot'] === 'never' ? Promise.resolve(0) :
291 Promise.reject(new Error('Invalid value for option --generate-pot: ' + args['generate-pot']))
293 return p.then(function () {
294 const languages = getLanguages();
296 for (const language of languages) {
297 const locale = language.split('-').filter(s => s.length !== 4).join('_');
298 const po = `misc/translator/po/${language}-${type}.po`;
300 const promise = access(po)
301 .catch(() => exec(`msginit -o ${po} -i ${pot} -l ${locale} --no-translator`))
302 promises.push(promise);
305 return Promise.all(promises);
309 function po_create_marc_marc21 () { return po_create_type('marc-MARC21') }
310 function po_create_marc_unimarc () { return po_create_type('marc-UNIMARC') }
311 function po_create_staff () { return po_create_type('staff-prog') }
312 function po_create_opac () { return po_create_type('opac-bootstrap') }
313 function po_create_pref () { return po_create_type('pref') }
314 function po_create_messages () { return po_create_type('messages') }
315 function po_create_messages_js () { return po_create_type('messages-js') }
316 function po_create_installer () { return po_create_type('installer') }
317 function po_create_installer_marc21 () { return po_create_type('installer-MARC21') }
318 function po_create_installer_unimarc () { return po_create_type('installer-UNIMARC') }
320 function po_update_type (type) {
321 const access = util.promisify(fs.access);
322 const exec = util.promisify(child_process.exec);
324 const pot = `misc/translator/Koha-${type}.pot`;
326 // Generate .pot only if it doesn't exist or --force-extract is given
327 const extract = () => stream.finished(poTasks[type].extract());
329 args['generate-pot'] === 'always' ? extract() :
330 args['generate-pot'] === 'auto' ? access(pot).catch(extract) :
331 args['generate-pot'] === 'never' ? Promise.resolve(0) :
332 Promise.reject(new Error('Invalid value for option --generate-pot: ' + args['generate-pot']))
334 return p.then(function () {
335 const languages = getLanguages();
337 for (const language of languages) {
338 const po = `misc/translator/po/${language}-${type}.po`;
339 promises.push(exec(`msgmerge --backup=off --no-wrap --quiet -F --update ${po} ${pot}`));
342 return Promise.all(promises);
346 function po_update_marc_marc21 () { return po_update_type('marc-MARC21') }
347 function po_update_marc_unimarc () { return po_update_type('marc-UNIMARC') }
348 function po_update_staff () { return po_update_type('staff-prog') }
349 function po_update_opac () { return po_update_type('opac-bootstrap') }
350 function po_update_pref () { return po_update_type('pref') }
351 function po_update_messages () { return po_update_type('messages') }
352 function po_update_messages_js () { return po_update_type('messages-js') }
353 function po_update_installer () { return po_update_type('installer') }
354 function po_update_installer_marc21 () { return po_update_type('installer-MARC21') }
355 function po_update_installer_unimarc () { return po_update_type('installer-UNIMARC') }
358 * Gulp plugin that executes xgettext-like command `cmd` on all files given as
359 * input, and then outputs the result as a POT file named `filename`.
360 * `cmd` should accept -o and -f options
362 function xgettext (cmd, filename) {
363 const filenames = [];
365 function transform (file, encoding, callback) {
366 filenames.push(path.relative(file.cwd, file.path));
370 function flush (callback) {
371 fs.mkdtemp(path.join(os.tmpdir(), 'koha-'), (err, folder) => {
372 const outputFilename = path.join(folder, filename);
373 const filesFilename = path.join(folder, 'files');
374 fs.writeFile(filesFilename, filenames.join(os.EOL), err => {
375 if (err) return callback(err);
377 const command = `${cmd} -o ${outputFilename} -f ${filesFilename}`;
378 child_process.exec(command, err => {
379 if (err) return callback(err);
381 fs.readFile(outputFilename, (err, data) => {
382 if (err) return callback(err);
384 const file = new Vinyl();
385 file.path = path.join(file.base, filename);
386 file.contents = data;
387 callback(null, file);
389 fs.rmSync(folder, { recursive: true });
395 return through2.obj(transform, flush);
399 * Return languages selected for PO-related tasks
401 * This can be either languages given on command-line with --lang option, or
402 * all the languages found in misc/translator/po otherwise
404 function getLanguages () {
405 if (Array.isArray(args.lang)) {
413 const filenames = fs.readdirSync('misc/translator/po/')
414 .filter(filename => filename.endsWith('-installer.po'))
415 .filter(filename => !filename.startsWith('.'))
417 const re = new RegExp('-installer.po');
418 languages = filenames.map(filename => filename.replace(re, ''))
420 return Array.from(new Set(languages));
423 exports.build = function(next){build(); next();};
424 exports.css = function(next){css(); next();};
425 exports.opac_css = opac_css;
426 exports.staff_css = staff_css;
427 exports.watch = function () {
428 watch(OPAC_CSS_BASE + "/src/**/*.scss", series('opac_css'));
429 watch(STAFF_CSS_BASE + "/src/**/*.scss", series('staff_css'));
432 if (args['_'][0].match("po:") && !fs.existsSync('misc/translator/po')) {
433 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.");
437 exports['po:create'] = parallel(...poTypes.map(type => poTasks[type].create));
438 exports['po:update'] = parallel(...poTypes.map(type => poTasks[type].update));
439 exports['po:extract'] = parallel(...poTypes.map(type => poTasks[type].extract));