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