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