Bug 27342: (QA follow-up) Fix test
[koha.git] / gulpfile.js
1 /* eslint-env node */
2 /* eslint no-console:"off" */
3
4 const { dest, parallel, series, src, watch } = require('gulp');
5
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');
12
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');
24
25 const STAFF_JS_BASE = "koha-tmpl/intranet-tmpl/prog/js";
26 const STAFF_CSS_BASE = "koha-tmpl/intranet-tmpl/prog/css";
27 const OPAC_JS_BASE = "koha-tmpl/opac-tmpl/bootstrap/js";
28 const OPAC_CSS_BASE = "koha-tmpl/opac-tmpl/bootstrap/css";
29
30 if (args.view == "opac") {
31     var css_base = OPAC_CSS_BASE;
32     var js_base = OPAC_JS_BASE;
33 } else {
34     var css_base = STAFF_CSS_BASE;
35     var js_base = STAFF_JS_BASE;
36 }
37
38 var sassOptions = {
39 }
40
41 // CSS processing for development
42 function css() {
43     var stream = src(css_base + "/src/**/*.scss")
44         .pipe(sourcemaps.init())
45         .pipe(sass(sassOptions).on('error', sass.logError))
46         .pipe(autoprefixer())
47         .pipe(dest(css_base));
48
49     if (args.view == "opac") {
50         stream = stream
51             .pipe(rtlcss())
52             .pipe(rename({
53                 suffix: '-rtl'
54             })) // Append "-rtl" to the filename.
55             .pipe(dest(css_base));
56     }
57
58     stream = stream.pipe(sourcemaps.write('./maps'))
59         .pipe(dest(css_base));
60
61     return stream;
62
63 }
64 // CSS processing for production
65 function build() {
66     sassOptions.outputStyle = "compressed";
67     var stream = src(css_base + "/src/**/*.scss")
68         .pipe(sass(sassOptions).on('error', sass.logError))
69         .pipe(autoprefixer())
70         .pipe(dest(css_base));
71
72     if( args.view == "opac" ){
73         stream = stream.pipe(rtlcss())
74         .pipe(rename({
75             suffix: '-rtl'
76         })) // Append "-rtl" to the filename.
77         .pipe(dest(css_base));
78     }
79
80     return stream;
81 }
82
83 const poTasks = {
84     'marc-MARC21': {
85         extract: po_extract_marc_marc21,
86         create: po_create_marc_marc21,
87         update: po_update_marc_marc21,
88     },
89     'marc-UNIMARC': {
90         extract: po_extract_marc_unimarc,
91         create: po_create_marc_unimarc,
92         update: po_update_marc_unimarc,
93     },
94     'staff-prog': {
95         extract: po_extract_staff,
96         create: po_create_staff,
97         update: po_update_staff,
98     },
99     'opac-bootstrap': {
100         extract: po_extract_opac,
101         create: po_create_opac,
102         update: po_update_opac,
103     },
104     'pref': {
105         extract: po_extract_pref,
106         create: po_create_pref,
107         update: po_update_pref,
108     },
109     'messages': {
110         extract: po_extract_messages,
111         create: po_create_messages,
112         update: po_update_messages,
113     },
114     'messages-js': {
115         extract: po_extract_messages_js,
116         create: po_create_messages_js,
117         update: po_update_messages_js,
118     },
119     'installer': {
120         extract: po_extract_installer,
121         create: po_create_installer,
122         update: po_update_installer,
123     },
124     'installer-MARC21': {
125         extract: po_extract_installer_marc21,
126         create: po_create_installer_marc21,
127         update: po_update_installer_marc21,
128     },
129     'installer-UNIMARC': {
130         extract: po_extract_installer_unimarc,
131         create: po_create_installer_unimarc,
132         update: po_update_installer_unimarc,
133     },
134 };
135
136 function getPoTasks () {
137     let tasks = [];
138
139     let all_tasks = Object.keys(poTasks);
140
141     if (args.task) {
142         tasks = [args.task].flat(Infinity);
143     } else {
144         return all_tasks;
145     }
146
147     let invalid_tasks = tasks.filter( function( el ) {
148         return all_tasks.indexOf( el ) < 0;
149     });
150
151     if ( invalid_tasks.length ) {
152         console.error("Invalid task");
153         return [];
154     }
155
156     return tasks;
157 }
158 const poTypes = getPoTasks();
159
160 function po_extract_marc (type) {
161     return src(`koha-tmpl/*-tmpl/*/en/**/*${type}*`, { read: false, nocase: true })
162         .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -F', `Koha-marc-${type}.pot`))
163         .pipe(dest('misc/translator'))
164 }
165
166 function po_extract_marc_marc21 ()  { return po_extract_marc('MARC21') }
167 function po_extract_marc_unimarc () { return po_extract_marc('UNIMARC') }
168
169 function po_extract_staff () {
170     const globs = [
171         'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
172         'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
173         'koha-tmpl/intranet-tmpl/prog/en/xslt/*.xsl',
174         '!koha-tmpl/intranet-tmpl/prog/en/**/*MARC21*',
175         '!koha-tmpl/intranet-tmpl/prog/en/**/*UNIMARC*',
176         '!koha-tmpl/intranet-tmpl/prog/en/**/*marc21*',
177         '!koha-tmpl/intranet-tmpl/prog/en/**/*unimarc*',
178     ];
179
180     return src(globs, { read: false, nocase: true })
181         .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -F', 'Koha-staff-prog.pot'))
182         .pipe(dest('misc/translator'))
183 }
184
185 function po_extract_opac () {
186     const globs = [
187         'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
188         'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
189         'koha-tmpl/opac-tmpl/bootstrap/en/xslt/*.xsl',
190         '!koha-tmpl/opac-tmpl/bootstrap/en/**/*MARC21*',
191         '!koha-tmpl/opac-tmpl/bootstrap/en/**/*UNIMARC*',
192         '!koha-tmpl/opac-tmpl/bootstrap/en/**/*marc21*',
193         '!koha-tmpl/opac-tmpl/bootstrap/en/**/*unimarc*',
194     ];
195
196     return src(globs, { read: false, nocase: true })
197         .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -F', 'Koha-opac-bootstrap.pot'))
198         .pipe(dest('misc/translator'))
199 }
200
201 const xgettext_options = '--from-code=UTF-8 --package-name Koha '
202     + '--package-version= -k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 '
203     + '-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ '
204     + '-kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3 --force-po';
205
206 function po_extract_messages_js () {
207     const globs = [
208         'koha-tmpl/intranet-tmpl/prog/js/**/*.js',
209         'koha-tmpl/opac-tmpl/bootstrap/js/**/*.js',
210     ];
211
212     return src(globs, { read: false, nocase: true })
213         .pipe(xgettext(`xgettext -L JavaScript ${xgettext_options}`, 'Koha-messages-js.pot'))
214         .pipe(dest('misc/translator'))
215 }
216
217 function po_extract_messages () {
218     const perlStream = src(['**/*.pl', '**/*.pm'], { read: false, nocase: true })
219         .pipe(xgettext(`xgettext -L Perl ${xgettext_options}`, 'Koha-perl.pot'))
220
221     const ttStream = src([
222             'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
223             'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
224             'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
225             'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
226         ], { read: false, nocase: true })
227         .pipe(xgettext('misc/translator/xgettext-tt2 --from-code=UTF-8', 'Koha-tt.pot'))
228
229     const headers = {
230         'Project-Id-Version': 'Koha',
231         'Content-Type': 'text/plain; charset=UTF-8',
232     };
233
234     return merge(perlStream, ttStream)
235         .pipe(concatPo('Koha-messages.pot', { headers }))
236         .pipe(dest('misc/translator'))
237 }
238
239 function po_extract_pref () {
240     return src('koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/*.pref', { read: false })
241         .pipe(xgettext('misc/translator/xgettext-pref', 'Koha-pref.pot'))
242         .pipe(dest('misc/translator'))
243 }
244
245 function po_extract_installer () {
246     const globs = [
247         'installer/data/mysql/en/mandatory/*.yml',
248         'installer/data/mysql/en/optional/*.yml',
249     ];
250
251     return src(globs, { read: false, nocase: true })
252         .pipe(xgettext('misc/translator/xgettext-installer', 'Koha-installer.pot'))
253         .pipe(dest('misc/translator'))
254 }
255
256 function po_extract_installer_marc (type) {
257     const globs = `installer/data/mysql/en/marcflavour/${type}/**/*.yml`;
258
259     return src(globs, { read: false, nocase: true })
260         .pipe(xgettext('misc/translator/xgettext-installer', `Koha-installer-${type}.pot`))
261         .pipe(dest('misc/translator'))
262 }
263
264 function po_extract_installer_marc21 ()  { return po_extract_installer_marc('MARC21') }
265
266 function po_extract_installer_unimarc ()  { return po_extract_installer_marc('UNIMARC') }
267
268 function po_create_type (type) {
269     const access = util.promisify(fs.access);
270     const exec = util.promisify(child_process.exec);
271
272     const pot = `misc/translator/Koha-${type}.pot`;
273
274     // Generate .pot only if it doesn't exist or --force-extract is given
275     const extract = () => stream.finished(poTasks[type].extract());
276     const p =
277         args['generate-pot'] === 'always' ? extract() :
278         args['generate-pot'] === 'auto' ? access(pot).catch(extract) :
279         args['generate-pot'] === 'never' ? Promise.resolve(0) :
280         Promise.reject(new Error('Invalid value for option --generate-pot: ' + args['generate-pot']))
281
282     return p.then(function () {
283         const languages = getLanguages();
284         const promises = [];
285         for (const language of languages) {
286             const locale = language.split('-').filter(s => s.length !== 4).join('_');
287             const po = `misc/translator/po/${language}-${type}.po`;
288
289             const promise = access(po)
290                 .catch(() => exec(`msginit -o ${po} -i ${pot} -l ${locale} --no-translator`))
291             promises.push(promise);
292         }
293
294         return Promise.all(promises);
295     });
296 }
297
298 function po_create_marc_marc21 ()       { return po_create_type('marc-MARC21') }
299 function po_create_marc_unimarc ()      { return po_create_type('marc-UNIMARC') }
300 function po_create_staff ()             { return po_create_type('staff-prog') }
301 function po_create_opac ()              { return po_create_type('opac-bootstrap') }
302 function po_create_pref ()              { return po_create_type('pref') }
303 function po_create_messages ()          { return po_create_type('messages') }
304 function po_create_messages_js ()       { return po_create_type('messages-js') }
305 function po_create_installer ()         { return po_create_type('installer') }
306 function po_create_installer_marc21 ()  { return po_create_type('installer-MARC21') }
307 function po_create_installer_unimarc () { return po_create_type('installer-UNIMARC') }
308
309 function po_update_type (type) {
310     const access = util.promisify(fs.access);
311     const exec = util.promisify(child_process.exec);
312
313     const pot = `misc/translator/Koha-${type}.pot`;
314
315     // Generate .pot only if it doesn't exist or --force-extract is given
316     const extract = () => stream.finished(poTasks[type].extract());
317     const p =
318         args['generate-pot'] === 'always' ? extract() :
319         args['generate-pot'] === 'auto' ? access(pot).catch(extract) :
320         args['generate-pot'] === 'never' ? Promise.resolve(0) :
321         Promise.reject(new Error('Invalid value for option --generate-pot: ' + args['generate-pot']))
322
323     return p.then(function () {
324         const languages = getLanguages();
325         const promises = [];
326         for (const language of languages) {
327             const po = `misc/translator/po/${language}-${type}.po`;
328             promises.push(exec(`msgmerge --backup=off --no-wrap --quiet -F --update ${po} ${pot}`));
329         }
330
331         return Promise.all(promises);
332     });
333 }
334
335 function po_update_marc_marc21 ()       { return po_update_type('marc-MARC21') }
336 function po_update_marc_unimarc ()      { return po_update_type('marc-UNIMARC') }
337 function po_update_staff ()             { return po_update_type('staff-prog') }
338 function po_update_opac ()              { return po_update_type('opac-bootstrap') }
339 function po_update_pref ()              { return po_update_type('pref') }
340 function po_update_messages ()          { return po_update_type('messages') }
341 function po_update_messages_js ()       { return po_update_type('messages-js') }
342 function po_update_installer ()         { return po_update_type('installer') }
343 function po_update_installer_marc21 ()  { return po_update_type('installer-MARC21') }
344 function po_update_installer_unimarc () { return po_update_type('installer-UNIMARC') }
345
346 /**
347  * Gulp plugin that executes xgettext-like command `cmd` on all files given as
348  * input, and then outputs the result as a POT file named `filename`.
349  * `cmd` should accept -o and -f options
350  */
351 function xgettext (cmd, filename) {
352     const filenames = [];
353
354     function transform (file, encoding, callback) {
355         filenames.push(path.relative(file.cwd, file.path));
356         callback();
357     }
358
359     function flush (callback) {
360         fs.mkdtemp(path.join(os.tmpdir(), 'koha-'), (err, folder) => {
361             const outputFilename = path.join(folder, filename);
362             const filesFilename = path.join(folder, 'files');
363             fs.writeFile(filesFilename, filenames.join(os.EOL), err => {
364                 if (err) return callback(err);
365
366                 const command = `${cmd} -o ${outputFilename} -f ${filesFilename}`;
367                 child_process.exec(command, err => {
368                     if (err) return callback(err);
369
370                     fs.readFile(outputFilename, (err, data) => {
371                         if (err) return callback(err);
372
373                         const file = new Vinyl();
374                         file.path = path.join(file.base, filename);
375                         file.contents = data;
376                         callback(null, file);
377                     });
378                 });
379             });
380         })
381     }
382
383     return through2.obj(transform, flush);
384 }
385
386 /**
387  * Return languages selected for PO-related tasks
388  *
389  * This can be either languages given on command-line with --lang option, or
390  * all the languages found in misc/translator/po otherwise
391  */
392 function getLanguages () {
393     if (Array.isArray(args.lang)) {
394         return args.lang;
395     }
396
397     if (args.lang) {
398         return [args.lang];
399     }
400
401     const filenames = fs.readdirSync('misc/translator/po/')
402         .filter(filename => filename.endsWith('-installer.po'))
403         .filter(filename => !filename.startsWith('.'))
404
405     const re = new RegExp('-installer.po');
406     languages = filenames.map(filename => filename.replace(re, ''))
407
408     return Array.from(new Set(languages));
409 }
410
411 exports.build = build;
412 exports.css = css;
413
414 if (args['_'][0].match("po:") && !fs.existsSync('misc/translator/po')) {
415     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.");
416     process.exit(1);
417 }
418
419 exports['po:create'] = parallel(...poTypes.map(type => poTasks[type].create));
420 exports['po:update'] = parallel(...poTypes.map(type => poTasks[type].update));
421 exports['po:extract'] = parallel(...poTypes.map(type => poTasks[type].extract));
422
423 exports.default = function () {
424     watch(css_base + "/src/**/*.scss", series('css'));
425 }