Bug 25067: Move PO file manipulation code into gulp tasks
authorJulian Maurice <julian.maurice@biblibre.com>
Sun, 29 Mar 2020 12:56:20 +0000 (14:56 +0200)
committerJonathan Druart <jonathan.druart@bugs.koha-community.org>
Fri, 6 Nov 2020 08:46:11 +0000 (09:46 +0100)
misc/translator/translate was doing three different things:
- extract translatable strings
- create or update PO files
- install translated templates

This patch separates responsibilities by moving the string extraction
code into several 'xgettext-like' scripts and adds gulp tasks to
automate string extraction and PO files update

This has several benefits:

- gulp runs tasks in parallel, so it's a lot faster (updating all PO
  files is at least 10 times faster with my 4-cores CPU)

- there is no need for $KOHA_CONF to be defined
  LangInstaller.pm relied on $KOHA_CONF to get the different paths
  needed. I'm not sure why, since string extraction and PO update should
  work on source files, not installed files

- string extraction code can be more easily tested

This patch also brings a couple of fixes and improvements:

- TT string extraction (strings wrapped in [% t(...) %]) was done with
  Template::Parser and PPI, which was extremely slow, and had some
  problems (see bug 24797).
  This is now done with Locale::XGettext::TT2 (new dependency) which is
  a lot faster, and fixes bug 24797

- Fix header in 4 PO files

For backward compatibility, 'create' and 'update' commands of
misc/translator/translate can still be used and will execute the
corresponding gulp task

Test plan:
1. Run `yarn install` and install Locale::XGettext::TT2
2. Run `gulp po:update`
3. Verify the contents of updated PO files
4. Run `cd misc/translator && ./translate install <lang>`
5. Verify that all (templates, sysprefs, xslt, installer files) is
   correctly translated
6. Run `gulp po:create --lang <lang>` and verify that it created all PO
   files for that language
7. Run `prove t/misc/translator`

Signed-off-by: Bernardo Gonzalez Kriegel <bgkriegel@gmail.com>
Need to install yarn & gulp, no errors

Signed-off-by: Katrin Fischer <katrin.fischer.83@web.de>
Signed-off-by: Jonathan Druart <jonathan.druart@bugs.koha-community.org>
24 files changed:
cpanfile
docs/development/internationalization.md [new file with mode: 0644]
gulpfile.js
misc/translator/LangInstaller.pm
misc/translator/po/dz-pref.po
misc/translator/po/gd-pref.po
misc/translator/po/lv-pref.po
misc/translator/po/te-pref.po
misc/translator/tmpl_process3.pl
misc/translator/translate
misc/translator/xgettext-installer [new file with mode: 0755]
misc/translator/xgettext-pref [new file with mode: 0755]
misc/translator/xgettext-tt2 [new file with mode: 0755]
misc/translator/xgettext.pl
package.json
t/LangInstaller.t [deleted file]
t/LangInstaller/templates/simple.tt [deleted file]
t/misc/translator/sample.pref [new file with mode: 0644]
t/misc/translator/sample.tt [new file with mode: 0644]
t/misc/translator/sample.yml [new file with mode: 0644]
t/misc/translator/xgettext-installer.t [new file with mode: 0644]
t/misc/translator/xgettext-pref.t [new file with mode: 0644]
t/misc/translator/xgettext-tt2.t [new file with mode: 0755]
yarn.lock

index bed97a9..a65551f 100644 (file)
--- a/cpanfile
+++ b/cpanfile
@@ -146,6 +146,7 @@ recommends 'Gravatar::URL', '1.03';
 recommends 'HTTPD::Bench::ApacheBench', '0.73';
 recommends 'LWP::Protocol::https', '5.836';
 recommends 'Lingua::Ispell', '0.07';
 recommends 'HTTPD::Bench::ApacheBench', '0.73';
 recommends 'LWP::Protocol::https', '5.836';
 recommends 'Lingua::Ispell', '0.07';
+recommends 'Locale::XGettext::TT2', '0.7';
 recommends 'Module::Bundled::Files', '0.03';
 recommends 'Module::Load::Conditional', '0.38';
 recommends 'Module::Pluggable', '3.9';
 recommends 'Module::Bundled::Files', '0.03';
 recommends 'Module::Load::Conditional', '0.38';
 recommends 'Module::Pluggable', '3.9';
@@ -157,7 +158,6 @@ recommends 'Net::SFTP::Foreign', '1.73';
 recommends 'Net::Server', '0.97';
 recommends 'Net::Z3950::SimpleServer', '1.15';
 recommends 'PDF::FromHTML', '0.31';
 recommends 'Net::Server', '0.97';
 recommends 'Net::Z3950::SimpleServer', '1.15';
 recommends 'PDF::FromHTML', '0.31';
-recommends 'PPI', '1.215';
 recommends 'Parallel::ForkManager', '0.75';
 recommends 'Readonly', '0.01';
 recommends 'Readonly::XS', '0.01';
 recommends 'Parallel::ForkManager', '0.75';
 recommends 'Readonly', '0.01';
 recommends 'Readonly::XS', '0.01';
diff --git a/docs/development/internationalization.md b/docs/development/internationalization.md
new file mode 100644 (file)
index 0000000..0365fef
--- /dev/null
@@ -0,0 +1,121 @@
+# Internationalization
+
+This page documents how internationalization works in Koha.
+
+## Making strings translatable
+
+There are several ways of making a string translatable, depending on where it
+is located
+
+### In Template::Toolkit files (`*.tt`)
+
+The simplest way to make a string translatable in a template is to do nothing.
+Templates are parsed as HTML files and almost all text nodes are considered as
+translatable strings. This also includes some attributes like `title` and
+`placeholder`.
+
+This method has some downsides: you don't have full control over what would
+appear in PO files and you cannot use plural forms or context. In order to do
+that you have to use `i18n.inc`
+
+`i18n.inc` contains several macros that, when used, make a string translatable.
+The first thing to do is to make these macros available by adding
+
+    [% PROCESS 'i18n.inc' %]
+
+at the top of the template file. Then you can use those macros.
+
+The simplest one is `t(msgid)`
+
+    [% t('This is a translatable string') %]
+
+You can also use variable substitution with `tx(msgid, vars)`
+
+    [% tx('Hello, {name}', { name = 'World' }) %]
+
+You can use plural forms with `tn(msgid, msgid_plural, count)`
+
+    [% tn('a child', 'several children', number_of_children) %]
+
+You can add context, to help translators when a term is ambiguous, with
+`tp(msgctxt, msgid)`
+
+    [% tp('verb', 'order') %]
+    [% tp('noun', 'order') %]
+
+Or any combinations of the above
+
+    [% tnpx('bibliographic record', '{count} item', '{count} items', items_count, { count = items_count }) %]
+
+### In JavaScript files (`*.js`)
+
+Like in templates, you have several functions available. Just replace `t` by `__`.
+
+    __('This is a translatable string');
+    __npx('bibliographic record, '{count} item', '{count} items', items_count, { count: items_count });
+
+### In Perl files (`*.pl`, `*.pm`)
+
+You will have to add
+
+    use Koha::I18N;
+
+at the top of the file, and then the same functions as above will be available.
+
+    __('This is a translatable string');
+    __npx('bibliographic record, '{count} item', '{count} items', $items_count, count => $items_count);
+
+### In installer and preferences YAML files (`*.yml`)
+
+Nothing special to do here. All strings will be automatically translatable.
+
+## Manipulating PO files
+
+Once strings have been made translatable in source files, they have to be
+extracted into PO files and uploaded on https://translate.koha-community.org/
+so they can be translated.
+
+### Install gulp first
+
+The next sections rely on gulp. If it's not installed, run the following
+commands:
+
+    # as root
+    npm install gulp-cli -g
+
+    # as normal user, from the root of Koha repository
+    yarn
+
+### Create PO files for a new language
+
+If you want to add translations for a new language, you have to create the
+missing PO files. You can do that by executing the following command:
+
+    # Replace xx-XX by your language tag
+    gulp po:create --lang xx-XX
+
+New PO files will be available in `misc/translator/po`.
+
+### Update PO files with new strings
+
+When new features or bugfixes are added to Koha, new translatable strings can
+be added, other can be removed or modified, and the PO file become out of sync.
+
+To be able to translate the new or modified strings, you have to update PO
+files. This can be done by executing the following command:
+
+    # Update PO files for all languages
+    gulp po:update
+
+    # or only one language
+    gulp po:update --lang xx-XX
+
+### Only extract strings
+
+Creating or updating PO files automatically extract strings, but if for some
+reasons you want to only extract strings without touching PO files, you can run
+the following command:
+
+    gulp po:extract
+
+POT files will be available in `misc/translator`.
index d2164c9..1a37a15 100644 (file)
@@ -1,13 +1,24 @@
 /* eslint-env node */
 /* eslint no-console:"off" */
 
 /* eslint-env node */
 /* eslint no-console:"off" */
 
-const { dest, series, src, watch } = require('gulp');
+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 sass = require("gulp-sass");
 const cssnano = require("gulp-cssnano");
 const rtlcss = require('gulp-rtlcss');
 const sourcemaps = require('gulp-sourcemaps');
 const autoprefixer = require('gulp-autoprefixer');
 
 const sass = require("gulp-sass");
 const cssnano = require("gulp-cssnano");
 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));
 const rename = require('gulp-rename');
 
 const args = require('minimist')(process.argv.slice(2));
 const rename = require('gulp-rename');
 
@@ -62,8 +73,297 @@ function build() {
         .pipe(dest(css_base));
 }
 
         .pipe(dest(css_base));
 }
 
+const poTasks = {
+    'marc-MARC21': {
+        extract: po_extract_marc_marc21,
+        create: po_create_marc_marc21,
+        update: po_update_marc_marc21,
+    },
+    'marc-NORMARC': {
+        extract: po_extract_marc_normarc,
+        create: po_create_marc_normarc,
+        update: po_update_marc_normarc,
+    },
+    '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,
+    },
+};
+
+const poTypes = Object.keys(poTasks);
+
+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 -s', `Koha-marc-${type}.pot`))
+        .pipe(dest('misc/translator'))
+}
+
+function po_extract_marc_marc21 ()  { return po_extract_marc('MARC21') }
+function po_extract_marc_normarc () { return po_extract_marc('NORMARC') }
+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/columns.def',
+        '!koha-tmpl/intranet-tmpl/prog/en/**/*MARC21*',
+        '!koha-tmpl/intranet-tmpl/prog/en/**/*NORMARC*',
+        '!koha-tmpl/intranet-tmpl/prog/en/**/*UNIMARC*',
+        '!koha-tmpl/intranet-tmpl/prog/en/**/*marc21*',
+        '!koha-tmpl/intranet-tmpl/prog/en/**/*normarc*',
+        '!koha-tmpl/intranet-tmpl/prog/en/**/*unimarc*',
+    ];
+
+    return src(globs, { read: false, nocase: true })
+        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', '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/**/*NORMARC*',
+        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*UNIMARC*',
+        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*marc21*',
+        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*normarc*',
+        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*unimarc*',
+    ];
+
+    return src(globs, { read: false, nocase: true })
+        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', '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 --force-po';
+
+function po_extract_messages_js () {
+    const globs = [
+        '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_create_type (type) {
+    const access = util.promisify(fs.access);
+    const exec = util.promisify(child_process.exec);
+
+    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 pot = `misc/translator/Koha-${type}.pot`;
+
+        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_normarc ()      { return po_create_type('marc-NORMARC') }
+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_update_type (type) {
+    const msgmerge_opts = '--backup=off --quiet --sort-output --update';
+    const cmd = `msgmerge ${msgmerge_opts} <%= file.path %> misc/translator/Koha-${type}.pot`;
+    const languages = getLanguages();
+    const globs = languages.map(language => `misc/translator/po/${language}-${type}.po`);
+
+    return src(globs)
+        .pipe(exec(cmd, { continueOnError: true }))
+        .pipe(exec.reporter({ err: false, stdout: false }))
+}
+
+function po_update_marc_marc21 ()       { return po_update_type('marc-MARC21') }
+function po_update_marc_normarc ()      { return po_update_type('marc-NORMARC') }
+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') }
+
+/**
+ * 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);
+                    });
+                });
+            });
+        })
+    }
+
+    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('.po'))
+        .filter(filename => !filename.startsWith('.'))
+
+    const re = new RegExp('-(' + poTypes.join('|') + ')\.po$');
+    languages = filenames.map(filename => filename.replace(re, ''))
+
+    return Array.from(new Set(languages));
+}
+
 exports.build = build;
 exports.css = css;
 exports.build = build;
 exports.css = css;
+
+exports['po:create'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].create)));
+exports['po:update'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].update)));
+exports['po:extract'] = parallel(...poTypes.map(type => poTasks[type].extract));
+
 exports.default = function () {
     watch(css_base + "/src/**/*.scss", series('css'));
 }
 exports.default = function () {
     watch(css_base + "/src/**/*.scss", series('css'));
 }
index 456532d..5baddee 100644 (file)
@@ -22,36 +22,15 @@ use Modern::Perl;
 use C4::Context;
 # WARNING: Any other tested YAML library fails to work properly in this
 # script content
 use C4::Context;
 # WARNING: Any other tested YAML library fails to work properly in this
 # script content
-use YAML::Syck qw( Dump LoadFile DumpFile );
+use YAML::Syck qw( LoadFile DumpFile );
 use Locale::PO;
 use FindBin qw( $Bin );
 use File::Basename;
 use Locale::PO;
 use FindBin qw( $Bin );
 use File::Basename;
-use File::Find;
 use File::Path qw( make_path );
 use File::Copy;
 use File::Path qw( make_path );
 use File::Copy;
-use File::Slurp;
-use File::Spec;
-use File::Temp qw( tempdir tempfile );
-use Template::Parser;
-use PPI;
-
 
 $YAML::Syck::ImplicitTyping = 1;
 
 
 $YAML::Syck::ImplicitTyping = 1;
 
-
-# Default file header for .po syspref files
-my $default_pref_po_header = Locale::PO->new(-msgid => '', -msgstr =>
-    "Project-Id-Version: PACKAGE VERSION\\n" .
-    "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" .
-    "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" .
-    "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\\n" .
-    "MIME-Version: 1.0\\n" .
-    "Content-Type: text/plain; charset=UTF-8\\n" .
-    "Content-Transfer-Encoding: 8bit\\n" .
-    "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
-);
-
-
 sub set_lang {
     my ($self, $lang) = @_;
 
 sub set_lang {
     my ($self, $lang) = @_;
 
@@ -60,7 +39,6 @@ sub set_lang {
                             "/prog/$lang/modules/admin/preferences";
 }
 
                             "/prog/$lang/modules/admin/preferences";
 }
 
-
 sub new {
     my ($class, $lang, $pref_only, $verbose) = @_;
 
 sub new {
     my ($class, $lang, $pref_only, $verbose) = @_;
 
@@ -75,32 +53,16 @@ sub new {
     $self->{verbose}         = $verbose;
     $self->{process}         = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
     $self->{path_po}         = "$Bin/po";
     $self->{verbose}         = $verbose;
     $self->{process}         = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
     $self->{path_po}         = "$Bin/po";
-    $self->{po}              = { '' => $default_pref_po_header };
+    $self->{po}              = {};
     $self->{domain}          = 'Koha';
     $self->{domain}          = 'Koha';
-    $self->{cp}              = `which cp`;
-    $self->{msgmerge}        = `which msgmerge`;
     $self->{msgfmt}          = `which msgfmt`;
     $self->{msgfmt}          = `which msgfmt`;
-    $self->{msginit}         = `which msginit`;
-    $self->{msgattrib}       = `which msgattrib`;
-    $self->{xgettext}        = `which xgettext`;
-    $self->{sed}             = `which sed`;
     $self->{po2json}         = "$Bin/po2json";
     $self->{gzip}            = `which gzip`;
     $self->{gunzip}          = `which gunzip`;
     $self->{po2json}         = "$Bin/po2json";
     $self->{gzip}            = `which gzip`;
     $self->{gunzip}          = `which gunzip`;
-    chomp $self->{cp};
-    chomp $self->{msgmerge};
     chomp $self->{msgfmt};
     chomp $self->{msgfmt};
-    chomp $self->{msginit};
-    chomp $self->{msgattrib};
-    chomp $self->{xgettext};
-    chomp $self->{sed};
     chomp $self->{gzip};
     chomp $self->{gunzip};
 
     chomp $self->{gzip};
     chomp $self->{gunzip};
 
-    unless ($self->{xgettext}) {
-        die "Missing 'xgettext' executable. Have you installed the gettext package?\n";
-    }
-
     # Get all .pref file names
     opendir my $fh, $self->{path_pref_en};
     my @pref_files = grep { /\.pref$/ } readdir($fh);
     # Get all .pref file names
     opendir my $fh, $self->{path_pref_en};
     my @pref_files = grep { /\.pref$/ } readdir($fh);
@@ -175,7 +137,6 @@ sub new {
     bless $self, $class;
 }
 
     bless $self, $class;
 }
 
-
 sub po_filename {
     my $self   = shift;
     my $suffix = shift;
 sub po_filename {
     my $self   = shift;
     my $suffix = shift;
@@ -186,162 +147,92 @@ sub po_filename {
     return $trans_file;
 }
 
     return $trans_file;
 }
 
+sub get_trans_text {
+    my ($self, $msgid, $default) = @_;
 
 
-sub po_append {
-    my ($self, $id, $comment) = @_;
-    my $po = $self->{po};
-    my $p = $po->{$id};
-    if ( $p ) {
-        $p->comment( $p->comment . "\n" . $comment );
-    }
-    else {
-        $po->{$id} = Locale::PO->new(
-            -comment => $comment,
-            -msgid   => $id,
-            -msgstr  => ''
-        );
-    }
-}
-
-
-sub add_prefs {
-    my ($self, $comment, $prefs) = @_;
-
-    for my $pref ( @$prefs ) {
-        my $pref_name = '';
-        for my $element ( @$pref ) {
-            if ( ref( $element) eq 'HASH' ) {
-                $pref_name = $element->{pref};
-                last;
-            }
-        }
-        for my $element ( @$pref ) {
-            if ( ref( $element) eq 'HASH' ) {
-                while ( my ($key, $value) = each(%$element) ) {
-                    next unless $key eq 'choices' or $key eq 'multiple';
-                    next unless ref($value) eq 'HASH';
-                    for my $ckey ( keys %$value ) {
-                        my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
-                        $self->po_append( $id, $comment );
-                    }
-                }
-            }
-            elsif ( $element ) {
-                $self->po_append( $self->{file} . "#$pref_name# $element", $comment );
-            }
+    my $po = $self->{po}->{Locale::PO->quote($msgid)};
+    if ($po) {
+        my $msgstr = Locale::PO->dequote($po->msgstr);
+        if ($msgstr and length($msgstr) > 0) {
+            return $msgstr;
         }
     }
         }
     }
-}
-
-
-sub get_trans_text {
-    my ($self, $id) = @_;
 
 
-    my $po = $self->{po}->{$id};
-    return unless $po;
-    return Locale::PO->dequote($po->msgstr);
+    return $default;
 }
 
 }
 
+sub get_translated_tab_content {
+    my ($self, $file, $tab_content) = @_;
 
 
-sub update_tab_prefs {
-    my ($self, $pref, $prefs) = @_;
-
-    for my $p ( @$prefs ) {
-        my $pref_name = '';
-        next unless $p;
-        for my $element ( @$p ) {
-            if ( ref( $element) eq 'HASH' ) {
-                $pref_name = $element->{pref};
-                last;
-            }
-        }
-        for my $i ( 0..@$p-1 ) {
-            my $element = $p->[$i];
-            if ( ref( $element) eq 'HASH' ) {
-                while ( my ($key, $value) = each(%$element) ) {
-                    next unless $key eq 'choices' or $key eq 'multiple';
-                    next unless ref($value) eq 'HASH';
-                    for my $ckey ( keys %$value ) {
-                        my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
-                        my $text = $self->get_trans_text( $id );
-                        $value->{$ckey} = $text if $text;
-                    }
-                }
-            }
-            elsif ( $element ) {
-                my $id = $self->{file} . "#$pref_name# $element";
-                my $text = $self->get_trans_text( $id );
-                $p->[$i] = $text if $text;
-            }
-        }
+    if ( ref($tab_content) eq 'ARRAY' ) {
+        return $self->get_translated_prefs($file, $tab_content);
     }
     }
-}
 
 
+    my $translated_tab_content = {
+        map {
+            my $section = $_;
+            my $sysprefs = $tab_content->{$section};
+            my $msgid = sprintf('%s %s', $file, $section);
 
 
-sub get_po_from_prefs {
-    my $self = shift;
+            $self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs);
+        } keys %$tab_content
+    };
 
 
-    for my $file ( @{$self->{pref_files}} ) {
-        my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
-        $self->{file} = $file;
-        # Entries for tab titles
-        $self->po_append( $self->{file}, $_ ) for keys %$pref;
-        while ( my ($tab, $tab_content) = each %$pref ) {
-            if ( ref($tab_content) eq 'ARRAY' ) {
-                $self->add_prefs( $tab, $tab_content );
-                next;
-            }
-            while ( my ($section, $sysprefs) = each %$tab_content ) {
-                my $comment = "$tab > $section";
-                $self->po_append( $self->{file} . " " . $section, $comment );
-                $self->add_prefs( $comment, $sysprefs );
-            }
-        }
-    }
+    return $translated_tab_content;
 }
 
 }
 
+sub get_translated_prefs {
+    my ($self, $file, $sysprefs) = @_;
 
 
-sub save_po {
-    my $self = shift;
+    my $translated_prefs = [
+        map {
+            my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
+            my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
+
+            my $translated_syspref = [
+                map {
+                    $self->get_translated_pref($file, $pref_name, $_);
+                } @$_
+            ];
 
 
-    # Create file header if it doesn't already exist
-    my $po = $self->{po};
-    $po->{''} ||= $default_pref_po_header;
+            $translated_syspref;
+        } @$sysprefs
+    ];
 
 
-    # Write .po entries into a file put in Koha standard po directory
-    Locale::PO->save_file_fromhash( $self->po_filename("-pref.po"), $po );
-    say "Saved in file: ", $self->po_filename("-pref.po") if $self->{verbose};
+    return $translated_prefs;
 }
 
 }
 
+sub get_translated_pref {
+    my ($self, $file, $pref_name, $syspref) = @_;
 
 
-sub get_po_merged_with_en {
-    my $self = shift;
-
-    # Get po from current 'en' .pref files
-    $self->get_po_from_prefs();
-    my $po_current = $self->{po};
+    unless (ref($syspref)) {
+        $syspref //= '';
+        my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
+        return $self->get_trans_text($msgid, $syspref);
+    }
 
 
-    # Get po from previous generation
-    my $po_previous = Locale::PO->load_file_ashash( $self->po_filename("-pref.po") );
+    my $translated_pref = {
+        map {
+            my $key = $_;
+            my $value = $syspref->{$key};
 
 
-    for my $id ( keys %$po_current ) {
-        my $po =  $po_previous->{Locale::PO->quote($id)};
-        next unless $po;
-        my $text = Locale::PO->dequote( $po->msgstr );
-        $po_current->{$id}->msgstr( $text );
-    }
-}
+            my $translated_value = $value;
+            if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
+                $translated_value = {
+                    map {
+                        my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
+                        $_ => $self->get_trans_text($msgid, $value->{$_})
+                    } keys %$value
+                }
+            }
 
 
+            $key => $translated_value
+        } keys %$syspref
+    };
 
 
-sub update_prefs {
-    my $self = shift;
-    print "Update '", $self->{lang},
-          "' preferences .po file from 'en' .pref files\n" if $self->{verbose};
-    $self->get_po_merged_with_en();
-    $self->save_po();
+    return $translated_pref;
 }
 
 }
 
-
 sub install_prefs {
     my $self = shift;
 
 sub install_prefs {
     my $self = shift;
 
@@ -350,45 +241,24 @@ sub install_prefs {
         exit;
     }
 
         exit;
     }
 
-    # Get the language .po file merged with last modified 'en' preferences
-    $self->get_po_merged_with_en();
+    $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
 
     for my $file ( @{$self->{pref_files}} ) {
         my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
 
     for my $file ( @{$self->{pref_files}} ) {
         my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
-        $self->{file} = $file;
-        # First, keys are replaced (tab titles)
-        $pref = do {
-            my %pref = map { 
-                $self->get_trans_text( $self->{file} ) || $_ => $pref->{$_}
-            } keys %$pref;
-            \%pref;
+
+        my $translated_pref = {
+            map {
+                my $tab = $_;
+                my $tab_content = $pref->{$tab};
+
+                $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
+            } keys %$pref
         };
         };
-        while ( my ($tab, $tab_content) = each %$pref ) {
-            if ( ref($tab_content) eq 'ARRAY' ) {
-                $self->update_tab_prefs( $pref, $tab_content );
-                next;
-            }
-            while ( my ($section, $sysprefs) = each %$tab_content ) {
-                $self->update_tab_prefs( $pref, $sysprefs );
-            }
-            my $ntab = {};
-            for my $section ( keys %$tab_content ) {
-                my $id = $self->{file} . " $section";
-                my $text = $self->get_trans_text($id);
-                my $nsection = $text ? $text : $section;
-                if( exists $ntab->{$nsection} ) {
-                    # When translations collide (see BZ 18634)
-                    push @{$ntab->{$nsection}}, @{$tab_content->{$section}};
-                } else {
-                    $ntab->{$nsection} = $tab_content->{$section};
-                }
-            }
-            $pref->{$tab} = $ntab;
-        }
+
+
         my $file_trans = $self->{po_path_lang} . "/$file";
         print "Write $file\n" if $self->{verbose};
         my $file_trans = $self->{po_path_lang} . "/$file";
         print "Write $file\n" if $self->{verbose};
-        open my $fh, ">", $file_trans;
-        print $fh Dump($pref);
+        DumpFile($file_trans, $translated_pref);
     }
 }
 
     }
 }
 
@@ -429,180 +299,6 @@ sub install_tmpl {
     }
 }
 
     }
 }
 
-
-sub update_tmpl {
-    my ($self, $files) = @_;
-
-    say "Update templates" if $self->{verbose};
-    for my $trans ( @{$self->{interface}} ) {
-        my @files   = @$files;
-        my @nomarc = ();
-        print
-            "  Update templates '$trans->{name}'\n",
-            "    From: $trans->{dir}/en/\n",
-            "    To  : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
-                if $self->{verbose};
-
-        my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
-        # if processing MARC po file, only use corresponding files
-        my $marc      = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
-        # if not processing MARC po file, ignore all MARC files
-        @nomarc       = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ );      # hardcoded MARC variants
-
-        system
-            "$self->{process} update " .
-            "-i $trans_dir " .
-            "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
-            "$marc "     .
-            ( @files   ? ' -f ' . join ' -f ', @files : '') .
-            ( @nomarc  ? ' -n ' . join ' -n ', @nomarc : '');
-    }
-}
-
-
-sub create_prefs {
-    my $self = shift;
-
-    if ( -e $self->po_filename("-pref.po") ) {
-        say "Preferences .po file already exists. Delete it if you want to recreate it.";
-        return;
-    }
-    $self->get_po_from_prefs();
-    $self->save_po();
-}
-
-sub get_po_from_target {
-    my $self   = shift;
-    my $target = shift;
-
-    my $po;
-    my $po_head = Locale::PO->new;
-    $po_head->{msgid}  = "\"\"";
-    $po_head->{msgstr} = "".
-        "Project-Id-Version: Koha Project - Installation files\\n" .
-        "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" .
-        "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" .
-        "Language-Team: Koha Translation Team\\n" .
-        "Language: ".$self->{lang}."\\n" .
-        "MIME-Version: 1.0\\n" .
-        "Content-Type: text/plain; charset=UTF-8\\n" .
-        "Content-Transfer-Encoding: 8bit\\n";
-
-    my @dirs = @{ $target->{dirs} };
-    my $intradir = $self->{context}->config('intranetdir');
-    for my $dir ( @dirs ) {                                                     # each dir
-        opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
-        my @filelist = grep { $_ =~ m/\.yml/ } readdir($dh);                    # Just yaml files
-        close($dh);
-        for my $file ( @filelist ) {                                            # each file
-            my $yaml   = LoadFile( "$intradir/$dir/$file" );
-            my @tables = @{ $yaml->{'tables'} };
-            my $tablec;
-            for my $table ( @tables ) {                                         # each table
-                $tablec++;
-                my $table_name = ( keys %$table )[0];
-                my @translatable = @{ $table->{$table_name}->{translatable} };
-                my @rows = @{ $table->{$table_name}->{rows} };
-                my @multiline = @{ $table->{$table_name}->{'multiline'} };      # to check multiline values
-                my $rowc;
-                for my $row ( @rows ) {                                         # each row
-                    $rowc++;
-                    for my $field ( @translatable ) {                           # each field
-                        if ( @multiline and grep { $_ eq $field } @multiline ) {    # multiline fields, only notices ATM
-                            my $mulc;
-                            foreach my $line ( @{$row->{$field}} ) {
-                                $mulc++;
-                                next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ );                     # discard pure html, TT, empty
-                                $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/g;                                   # put placeholders
-                                next if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ or length($line) < 2 );     # discard non strings
-                                if ( not $po->{ $line } ) {
-                                    my $msg = Locale::PO->new(
-                                                -msgid => $line, -msgstr => '',
-                                                -reference => "$dir/$file:$table_name:$tablec:row:$rowc:mul:$mulc" );
-                                    $po->{ $line } = $msg;
-                                }
-                            }
-                        } else {
-                            if ( defined $row->{$field} and length($row->{$field}) > 1                         # discard null values and small strings
-                                 and not $po->{ $row->{$field} } ) {
-                                my $msg = Locale::PO->new(
-                                            -msgid => $row->{$field}, -msgstr => '',
-                                            -reference => "$dir/$file:$table_name:$tablec:row:$rowc" );
-                                $po->{ $row->{$field} } = $msg;
-                            }
-                        }
-                    }
-                }
-            }
-            my $desccount;
-            for my $description ( @{ $yaml->{'description'} } ) {
-                $desccount++;
-                if ( length($description) > 1 and not $po->{ $description } ) {
-                    my $msg = Locale::PO->new(
-                                -msgid => $description, -msgstr => '',
-                                -reference => "$dir/$file:description:$desccount" );
-                    $po->{ $description } = $msg;
-                }
-            }
-        }
-    }
-    $po->{''} = $po_head if ( $po );
-
-    return $po;
-}
-
-sub create_installer {
-    my $self = shift;
-    return unless ( $self->{installer} );
-
-    say "Create installer translation files\n" if $self->{verbose};
-
-    my @targets = @{ $self->{installer} };             # each installer target (common,marc21,unimarc)
-
-    for my $target ( @targets ) {
-        if ( -e $self->po_filename( $target->{suffix} ) ) {
-            say "$self->{lang}$target->{suffix} file already exists. Delete it if you want to recreate it.";
-            return;
-        }
-    }
-
-    for my $target ( @targets ) {
-        my $po = get_po_from_target( $self, $target );
-        # create output file only if there is something to write
-        if ( $po ) {
-            my $po_file = $self->po_filename( $target->{suffix} );
-            Locale::PO->save_file_fromhash( $po_file, $po );
-            say "Saved in file: ", $po_file if $self->{verbose};
-        }
-    }
-}
-
-sub update_installer {
-    my $self = shift;
-    return unless ( $self->{installer} );
-
-    say "Update installer translation files\n" if $self->{verbose};
-
-    my @targets = @{ $self->{installer} };             # each installer target (common,marc21,unimarc)
-
-    for my $target ( @targets ) {
-        return unless ( -e $self->po_filename( $target->{suffix} ) );
-        my $po = get_po_from_target( $self, $target );
-        # update file only if there is something to update
-        if ( $po ) {
-            my ( $fh, $po_temp ) = tempfile();
-            binmode( $fh, ":encoding(UTF-8)" );
-            Locale::PO->save_file_fromhash( $po_temp, $po );
-            my $po_file = $self->po_filename( $target->{suffix} );
-            eval {
-                my $st = system($self->{msgmerge}." ".($self->{verbose}?'':'-q').
-                         " -s $po_file $po_temp -o - | ".$self->{msgattrib}." --no-obsolete -o $po_file");
-            };
-            say "Updated file: ", $po_file if $self->{verbose};
-        }
-    }
-}
-
 sub translate_yaml {
     my $self   = shift;
     my $target = shift;
 sub translate_yaml {
     my $self   = shift;
     my $target = shift;
@@ -716,35 +412,6 @@ sub install_installer {
     }
 }
 
     }
 }
 
-sub create_tmpl {
-    my ($self, $files) = @_;
-
-    say "Create templates\n" if $self->{verbose};
-    for my $trans ( @{$self->{interface}} ) {
-        my @files   = @$files;
-        my @nomarc = ();
-        print
-            "  Create templates .po files for '$trans->{name}'\n",
-            "    From: $trans->{dir}/en/\n",
-            "    To  : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
-                if $self->{verbose};
-
-        my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
-        # if processing MARC po file, only use corresponding files
-        my $marc      = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
-        # if not processing MARC po file, ignore all MARC files
-        @nomarc       = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
-
-        system
-            "$self->{process} create " .
-            "-i $trans_dir " .
-            "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
-            "$marc " .
-            ( @files  ? ' -f ' . join ' -f ', @files   : '') .
-            ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
-    }
-}
-
 sub locale_name {
     my $self = shift;
 
 sub locale_name {
     my $self = shift;
 
@@ -758,250 +425,6 @@ sub locale_name {
     return $locale;
 }
 
     return $locale;
 }
 
-sub create_messages {
-    my $self = shift;
-
-    my $pot = "$Bin/$self->{domain}.pot";
-    my $po = "$self->{path_po}/$self->{lang}-messages.po";
-    my $js_pot = "$self->{domain}-js.pot";
-    my $js_po = "$self->{path_po}/$self->{lang}-messages-js.po";
-
-    unless ( -f $pot && -f $js_pot ) {
-        $self->extract_messages();
-    }
-
-    say "Create messages ($self->{lang})" if $self->{verbose};
-    my $locale = $self->locale_name();
-    system "$self->{msginit} -i $pot -o $po -l $locale --no-translator 2> /dev/null";
-    warn "Problems creating $pot ".$? if ( $? == -1 );
-    system "$self->{msginit} -i $js_pot -o $js_po -l $locale --no-translator 2> /dev/null";
-    warn "Problems creating $js_pot ".$? if ( $? == -1 );
-
-    # If msginit failed to correctly set Plural-Forms, set a default one
-    system "$self->{sed} --in-place "
-        . "--expression='s/Plural-Forms: nplurals=INTEGER; plural=EXPRESSION/Plural-Forms: nplurals=2; plural=(n != 1)/' "
-        . "$po $js_po";
-}
-
-sub update_messages {
-    my $self = shift;
-
-    my $pot = "$Bin/$self->{domain}.pot";
-    my $po = "$self->{path_po}/$self->{lang}-messages.po";
-    my $js_pot = "$self->{domain}-js.pot";
-    my $js_po = "$self->{path_po}/$self->{lang}-messages-js.po";
-
-    unless ( -f $pot && -f $js_pot ) {
-        $self->extract_messages();
-    }
-
-    if ( -f $po && -f $js_pot ) {
-        say "Update messages ($self->{lang})" if $self->{verbose};
-        system "$self->{msgmerge} --backup=off --quiet -U $po $pot";
-        system "$self->{msgmerge} --backup=off --quiet -U $js_po $js_pot";
-    } else {
-        $self->create_messages();
-    }
-}
-
-sub extract_messages_from_templates {
-    my ($self, $tempdir, $type, @files) = @_;
-
-    my $htdocs = $type eq 'intranet' ? 'intrahtdocs' : 'opachtdocs';
-    my $dir = $self->{context}->config($htdocs);
-    my @keywords = qw(t tx tn txn tnx tp tpx tnp tnpx);
-    my $parser = Template::Parser->new();
-
-    foreach my $file (@files) {
-        say "Extract messages from $file" if $self->{verbose};
-        my $template = read_file(File::Spec->catfile($dir, $file));
-
-        # No need to process a file that doesn't use the i18n.inc file.
-        next unless $template =~ /i18n\.inc/;
-
-        my $data = $parser->parse($template);
-        unless ($data) {
-            warn "Error at $file : " . $parser->error();
-            next;
-        }
-
-        my $destfile = $type eq 'intranet' ?
-            File::Spec->catfile($tempdir, 'koha-tmpl', 'intranet-tmpl', $file) :
-            File::Spec->catfile($tempdir, 'koha-tmpl', 'opac-tmpl', $file);
-
-        make_path(dirname($destfile));
-        open my $fh, '>', $destfile;
-
-        my @blocks = ($data->{BLOCK}, values %{ $data->{DEFBLOCKS} });
-        foreach my $block (@blocks) {
-            my $document = PPI::Document->new(\$block);
-
-            # [% t('foo') %] is compiled to
-            # $output .= $stash->get(['t', ['foo']]);
-            # We try to find all nodes corresponding to keyword (here 't')
-            my $nodes = $document->find(sub {
-                my ($topnode, $element) = @_;
-
-                # Filter out non-valid keywords
-                return 0 unless ($element->isa('PPI::Token::Quote::Single'));
-                return 0 unless (grep {$element->content eq qq{'$_'}} @keywords);
-
-                # keyword (e.g. 't') should be the first element of the arrayref
-                # passed to $stash->get()
-                return 0 if $element->sprevious_sibling;
-
-                return 0 unless $element->snext_sibling
-                    && $element->snext_sibling->snext_sibling
-                    && $element->snext_sibling->snext_sibling->isa('PPI::Structure::Constructor');
-
-                # Check that it's indeed a call to $stash->get()
-                my $statement = $element->statement->parent->statement->parent->statement;
-                return 0 unless grep { $_->isa('PPI::Token::Symbol') && $_->content eq '$stash' } $statement->children;
-                return 0 unless grep { $_->isa('PPI::Token::Operator') && $_->content eq '->' } $statement->children;
-                return 0 unless grep { $_->isa('PPI::Token::Word') && $_->content eq 'get' } $statement->children;
-
-                return 1;
-            });
-
-            next unless $nodes;
-
-            # Write the Perl equivalent of calls to t* functions family, so
-            # xgettext can extract the strings correctly
-            foreach my $node (@$nodes) {
-                my @args = map {
-                    $_->significant && !$_->isa('PPI::Token::Operator') ? $_->content : ()
-                } $node->snext_sibling->snext_sibling->find_first('PPI::Statement')->children;
-
-                my $keyword = $node->content;
-                $keyword =~ s/^'t(.*)'$/__$1/;
-
-                # Only keep required args to have a clean output
-                my @required_args = shift @args;
-                push @required_args, shift @args if $keyword =~ /n/;
-                push @required_args, shift @args if $keyword =~ /p/;
-
-                say $fh "$keyword(" . join(', ', @required_args) . ");";
-            }
-
-        }
-
-        close $fh;
-    }
-
-    return $tempdir;
-}
-
-sub extract_messages {
-    my $self = shift;
-
-    say "Extract messages into POT file" if $self->{verbose};
-
-    my $intranetdir = $self->{context}->config('intranetdir');
-    my $opacdir = $self->{context}->config('opacdir');
-
-    # Find common ancestor directory
-    my @intranetdirs = File::Spec->splitdir($intranetdir);
-    my @opacdirs = File::Spec->splitdir($opacdir);
-    my @basedirs;
-    while (@intranetdirs and @opacdirs) {
-        my ($dir1, $dir2) = (shift @intranetdirs, shift @opacdirs);
-        last if $dir1 ne $dir2;
-        push @basedirs, $dir1;
-    }
-    my $basedir = File::Spec->catdir(@basedirs);
-
-    my @files_to_scan;
-    my @directories_to_scan = ('.');
-    my @blacklist = map { File::Spec->catdir(@intranetdirs, $_) } qw(blib koha-tmpl skel tmp t);
-    while (@directories_to_scan) {
-        my $dir = shift @directories_to_scan;
-        opendir DIR, File::Spec->catdir($basedir, $dir) or die "Unable to open $dir: $!";
-        foreach my $entry (readdir DIR) {
-            next if $entry =~ /^\./;
-            my $relentry = File::Spec->catfile($dir, $entry);
-            my $abspath = File::Spec->catfile($basedir, $relentry);
-            if (-d $abspath and not grep { $_ eq $relentry } @blacklist) {
-                push @directories_to_scan, $relentry;
-            } elsif (-f $abspath and $relentry =~ /\.(pl|pm)$/) {
-                push @files_to_scan, $relentry;
-            }
-        }
-    }
-
-    my $intrahtdocs = $self->{context}->config('intrahtdocs');
-    my $opachtdocs = $self->{context}->config('opachtdocs');
-
-    my @intranet_tt_files;
-    find(sub {
-        if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) {
-            my $filename = $File::Find::name;
-            $filename =~ s|^$intrahtdocs/||;
-            push @intranet_tt_files, $filename;
-        }
-    }, $intrahtdocs);
-
-    my @opac_tt_files;
-    find(sub {
-        if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) {
-            my $filename = $File::Find::name;
-            $filename =~ s|^$opachtdocs/||;
-            push @opac_tt_files, $filename;
-        }
-    }, $opachtdocs);
-
-    my $tempdir = tempdir('Koha-translate-XXXX', TMPDIR => 1, CLEANUP => 1);
-    $self->extract_messages_from_templates($tempdir, 'intranet', @intranet_tt_files);
-    $self->extract_messages_from_templates($tempdir, 'opac', @opac_tt_files);
-
-    @intranet_tt_files = map { File::Spec->catfile('koha-tmpl', 'intranet-tmpl', $_) } @intranet_tt_files;
-    @opac_tt_files = map { File::Spec->catfile('koha-tmpl', 'opac-tmpl', $_) } @opac_tt_files;
-    my @tt_files = grep { -e File::Spec->catfile($tempdir, $_) } @intranet_tt_files, @opac_tt_files;
-
-    push @files_to_scan, @tt_files;
-
-    my $xgettext_common_args = "--force-po --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 ";
-    my $xgettext_cmd = "$self->{xgettext} -L Perl $xgettext_common_args "
-        . "-o $Bin/$self->{domain}.pot -D $tempdir -D $basedir";
-    $xgettext_cmd .= " $_" foreach (@files_to_scan);
-
-    if (system($xgettext_cmd) != 0) {
-        die "system call failed: $xgettext_cmd";
-    }
-
-    my @js_dirs = (
-        "$intrahtdocs/prog/js",
-        "$opachtdocs/bootstrap/js",
-    );
-
-    my @js_files;
-    find(sub {
-        if ($_ =~ m/\.js$/) {
-            my $filename = $File::Find::name;
-            $filename =~ s|^$intranetdir/||;
-            push @js_files, $filename;
-        }
-    }, @js_dirs);
-
-    $xgettext_cmd = "$self->{xgettext} -L JavaScript $xgettext_common_args "
-        . "-o $Bin/$self->{domain}-js.pot -D $intranetdir";
-    $xgettext_cmd .= " $_" foreach (@js_files);
-
-    if (system($xgettext_cmd) != 0) {
-        die "system call failed: $xgettext_cmd";
-    }
-
-    my $replace_charset_cmd = "$self->{sed} --in-place " .
-        "--expression='s/charset=CHARSET/charset=UTF-8/' " .
-        "$Bin/$self->{domain}.pot $Bin/$self->{domain}-js.pot";
-    if (system($replace_charset_cmd) != 0) {
-        die "system call failed: $replace_charset_cmd";
-    }
-}
-
 sub install_messages {
     my ($self) = @_;
 
 sub install_messages {
     my ($self) = @_;
 
@@ -1012,8 +435,9 @@ sub install_messages {
     my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
 
     unless ( -f $pofile && -f $js_pofile ) {
     my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
 
     unless ( -f $pofile && -f $js_pofile ) {
-        $self->create_messages();
+        die "PO files for language '$self->{lang}' do not exist";
     }
     }
+
     say "Install messages ($locale)" if $self->{verbose};
     make_path($modir);
     system "$self->{msgfmt} -o $mofile $pofile";
     say "Install messages ($locale)" if $self->{verbose};
     make_path($modir);
     system "$self->{msgfmt} -o $mofile $pofile";
@@ -1035,13 +459,6 @@ sub install_messages {
     }
 }
 
     }
 }
 
-sub remove_pot {
-    my $self = shift;
-
-    unlink "$Bin/$self->{domain}.pot";
-    unlink "$Bin/$self->{domain}-js.pot";
-}
-
 sub compress {
     my ($self, $files) = @_;
     my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
 sub compress {
     my ($self, $files) = @_;
     my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
@@ -1074,11 +491,15 @@ sub install {
     my ($self, $files) = @_;
     return unless $self->{lang};
     $self->uncompress();
     my ($self, $files) = @_;
     return unless $self->{lang};
     $self->uncompress();
-    $self->install_tmpl($files) unless $self->{pref_only};
-    $self->install_prefs();
-    $self->install_messages();
-    $self->remove_pot();
-    $self->install_installer();
+
+    if ($self->{pref_only}) {
+        $self->install_prefs();
+    } else {
+        $self->install_tmpl($files);
+        $self->install_prefs();
+        $self->install_messages();
+        $self->install_installer();
+    }
 }
 
 
 }
 
 
@@ -1090,34 +511,6 @@ sub get_all_langs {
     @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
 }
 
     @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
 }
 
-
-sub update {
-    my ($self, $files) = @_;
-    my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
-    for my $lang ( @langs ) {
-        $self->set_lang( $lang );
-        $self->uncompress();
-        $self->update_tmpl($files) unless $self->{pref_only};
-        $self->update_prefs();
-        $self->update_messages();
-        $self->update_installer();
-    }
-    $self->remove_pot();
-}
-
-
-sub create {
-    my ($self, $files) = @_;
-    return unless $self->{lang};
-    $self->create_tmpl($files) unless $self->{pref_only};
-    $self->create_prefs();
-    $self->create_messages();
-    $self->remove_pot();
-    $self->create_installer();
-}
-
-
-
 1;
 
 
 1;
 
 
index 8618b78..8f543cc 100644 (file)
@@ -1,5 +1,13 @@
 msgid ""
 msgid ""
-msgstr "Project-Id-Version: PACKAGE VERSION\\nPO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\nLast-Translator: FULL NAME <EMAIL@ADDRESS>\\nLanguage-Team: Koha Translate List <koha-translate@lists.koha-community.org>\\nMIME-Version: 1.0\\nContent-Type: text/plain; charset=UTF-8\\nContent-Transfer-Encoding: 8bit\\nPlural-Forms: nplurals=2; plural=(n > 1);\\n"
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Koha Translate List <koha-translate@lists.koha-community.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
 # Accounting
 msgid "accounting.pref"
 
 # Accounting
 msgid "accounting.pref"
index 8618b78..8f543cc 100644 (file)
@@ -1,5 +1,13 @@
 msgid ""
 msgid ""
-msgstr "Project-Id-Version: PACKAGE VERSION\\nPO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\nLast-Translator: FULL NAME <EMAIL@ADDRESS>\\nLanguage-Team: Koha Translate List <koha-translate@lists.koha-community.org>\\nMIME-Version: 1.0\\nContent-Type: text/plain; charset=UTF-8\\nContent-Transfer-Encoding: 8bit\\nPlural-Forms: nplurals=2; plural=(n > 1);\\n"
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Koha Translate List <koha-translate@lists.koha-community.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
 # Accounting
 msgid "accounting.pref"
 
 # Accounting
 msgid "accounting.pref"
index 8618b78..8f543cc 100644 (file)
@@ -1,5 +1,13 @@
 msgid ""
 msgid ""
-msgstr "Project-Id-Version: PACKAGE VERSION\\nPO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\nLast-Translator: FULL NAME <EMAIL@ADDRESS>\\nLanguage-Team: Koha Translate List <koha-translate@lists.koha-community.org>\\nMIME-Version: 1.0\\nContent-Type: text/plain; charset=UTF-8\\nContent-Transfer-Encoding: 8bit\\nPlural-Forms: nplurals=2; plural=(n > 1);\\n"
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Koha Translate List <koha-translate@lists.koha-community.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
 # Accounting
 msgid "accounting.pref"
 
 # Accounting
 msgid "accounting.pref"
index 8618b78..8f543cc 100644 (file)
@@ -1,5 +1,13 @@
 msgid ""
 msgid ""
-msgstr "Project-Id-Version: PACKAGE VERSION\\nPO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\nLast-Translator: FULL NAME <EMAIL@ADDRESS>\\nLanguage-Team: Koha Translate List <koha-translate@lists.koha-community.org>\\nMIME-Version: 1.0\\nContent-Type: text/plain; charset=UTF-8\\nContent-Transfer-Encoding: 8bit\\nPlural-Forms: nplurals=2; plural=(n > 1);\\n"
+msgstr ""
+"Project-Id-Version: PACKAGE VERSION\n"
+"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
+"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
+"Language-Team: Koha Translate List <koha-translate@lists.koha-community.org>\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=(n > 1);\n"
 
 # Accounting
 msgid "accounting.pref"
 
 # Accounting
 msgid "accounting.pref"
index 9221430..3c04618 100755 (executable)
@@ -204,11 +204,9 @@ sub usage {
     my($exitcode) = @_;
     my $h = $exitcode? *STDERR: *STDOUT;
     print $h <<EOF;
     my($exitcode) = @_;
     my $h = $exitcode? *STDERR: *STDOUT;
     print $h <<EOF;
-Usage: $0 create [OPTION]
-  or:  $0 update [OPTION]
-  or:  $0 install [OPTION]
+Usage: $0 install [OPTION]
   or:  $0 --help
   or:  $0 --help
-Create or update PO files from templates, or install translated templates.
+Install translated templates.
 
   -i, --input=SOURCE          Get or update strings from SOURCE directory(s).
                               On create or update can have multiple values.
 
   -i, --input=SOURCE          Get or update strings from SOURCE directory(s).
                               On create or update can have multiple values.
@@ -230,7 +228,6 @@ Create or update PO files from templates, or install translated templates.
       --help                  Display this help and exit
   -q, --quiet                 no output to screen (except for errors)
 
       --help                  Display this help and exit
   -q, --quiet                 no output to screen (except for errors)
 
-The -o option is ignored for the "create" and "update" actions.
 Try `perldoc $0` for perhaps more information.
 EOF
     exit($exitcode);
 Try `perldoc $0` for perhaps more information.
 EOF
     exit($exitcode);
@@ -265,12 +262,6 @@ GetOptions(
 VerboseWarnings::set_application_name($0);
 VerboseWarnings::set_pedantic_mode($pedantic_p);
 
 VerboseWarnings::set_application_name($0);
 VerboseWarnings::set_pedantic_mode($pedantic_p);
 
-# keep the buggy Locale::PO quiet if it says stupid things
-$SIG{__WARN__} = sub {
-    my($s) = @_;
-    print STDERR $s unless $s =~ /^Strange line in [^:]+: #~/s
-    };
-
 my $action = shift or usage_error('You must specify an ACTION.');
 usage_error('You must at least specify input and string list filenames.')
     if !@in_dirs || !defined $str_file;
 my $action = shift or usage_error('You must specify an ACTION.');
 usage_error('You must at least specify input and string list filenames.')
     if !@in_dirs || !defined $str_file;
@@ -344,89 +335,9 @@ if (!defined $charset_out) {
     $charset_out = TmplTokenizer::charset_canon('UTF-8');
     warn "Warning: Charset Out defaulting to $charset_out\n" unless ( $quiet );
 }
     $charset_out = TmplTokenizer::charset_canon('UTF-8');
     warn "Warning: Charset Out defaulting to $charset_out\n" unless ( $quiet );
 }
-my $xgettext = './xgettext.pl'; # actual text extractor script
 my $st;
 
 my $st;
 
-if ($action eq 'create')  {
-    # updates the list. As the list is empty, every entry will be added
-    if (!-s $str_file) {
-    warn "Removing empty file $str_file\n" unless ( $quiet );
-    unlink $str_file || die "$str_file: $!\n";
-    }
-    die "$str_file: Output file already exists\n" if -f $str_file;
-    my($tmph1, $tmpfile1) = tmpnam();
-    my($tmph2, $tmpfile2) = tmpnam();
-    close $tmph2; # We just want a name
-    # Generate the temporary file that acts as <MODULE>/POTFILES.in
-    for my $input (@in_files) {
-    print $tmph1 "$input\n";
-    }
-    close $tmph1;
-    warn "I $charset_in O $charset_out" unless ( $quiet );
-    # Generate the specified po file ($str_file)
-    $st = system ($xgettext, '-s', '-f', $tmpfile1, '-o', $tmpfile2,
-            (defined $charset_in? ('-I', $charset_in): ()),
-            (defined $charset_out? ('-O', $charset_out): ())
-    );
-    # Run msgmerge so that the pot file looks like a real pot file
-    # We need to help msgmerge a bit by pre-creating a dummy po file that has
-    # the headers and the "" msgid & msgstr. It will fill in the rest.
-    if ($st == 0) {
-    # Merge the temporary "pot file" with the specified po file ($str_file)
-    # FIXME: msgmerge(1) is a Unix dependency
-    # FIXME: need to check the return value
-    unless (-f $str_file) {
-        open(my $infh, '<', $tmpfile2);
-        open(my $outfh, '>', $str_file);
-        while (<$infh>) {
-        print $outfh $_;
-        last if /^\n/s;
-        }
-        close $infh;
-        close $outfh;
-    }
-    $st = system("msgmerge ".($quiet?'-q':'')." -s $str_file $tmpfile2 -o - | msgattrib --no-obsolete -o $str_file");
-    } else {
-    error_normal("Text extraction failed: $xgettext: $!\n", undef);
-    error_additional("Will not run msgmerge\n", undef);
-    }
-    unlink $tmpfile1 || warn_normal("$tmpfile1: unlink failed: $!\n", undef);
-    unlink $tmpfile2 || warn_normal("$tmpfile2: unlink failed: $!\n", undef);
-
-} elsif ($action eq 'update') {
-    my($tmph1, $tmpfile1) = tmpnam();
-    my($tmph2, $tmpfile2) = tmpnam();
-    close $tmph2; # We just want a name
-    # Generate the temporary file that acts as <MODULE>/POTFILES.in
-    for my $input (@in_files) {
-    print $tmph1 "$input\n";
-    }
-    close $tmph1;
-    # Generate the temporary file that acts as <MODULE>/<LANG>.pot
-    $st = system($xgettext, '-s', '-f', $tmpfile1, '-o', $tmpfile2,
-        '--po-mode',
-        (defined $charset_in? ('-I', $charset_in): ()),
-        (defined $charset_out? ('-O', $charset_out): ()));
-    if ($st == 0) {
-        # Merge the temporary "pot file" with the specified po file ($str_file)
-        # FIXME: msgmerge(1) is a Unix dependency
-        # FIXME: need to check the return value
-        if ( @filenames ) {
-            my ($tmph3, $tmpfile3) = tmpnam();
-            $st = system("msgcat $str_file $tmpfile2 > $tmpfile3");
-            $st = system("msgmerge ".($quiet?'-q':'')." -s $str_file $tmpfile3 -o - | msgattrib --no-obsolete -o $str_file")
-                unless $st;
-        } else {
-            $st = system("msgmerge ".($quiet?'-q':'')." -s $str_file $tmpfile2 -o - | msgattrib --no-obsolete -o $str_file");
-        }
-    } else {
-        error_normal("Text extraction failed: $xgettext: $!\n", undef);
-        error_additional("Will not run msgmerge\n", undef);
-    }
-    unlink $tmpfile1 || warn_normal("$tmpfile1: unlink failed: $!\n", undef);
-    unlink $tmpfile2 || warn_normal("$tmpfile2: unlink failed: $!\n", undef);
-
-} elsif ($action eq 'install') {
+if ($action eq 'install') {
     if(!defined($out_dir)) {
     usage_error("You must specify an output directory when using the install method.");
     }
     if(!defined($out_dir)) {
     usage_error("You must specify an output directory when using the install method.");
     }
@@ -554,14 +465,6 @@ translation, it can be suppressed with the %0.0s notation.
 Using the PO format also means translators can add their
 own comments in the translation files, if necessary.
 
 Using the PO format also means translators can add their
 own comments in the translation files, if necessary.
 
-=item -
-
-Create, update, and install actions are all based on the
-same scanner module. This ensures that update and install
-have the same idea of what is a translatable string;
-attribute names in tags, for example, will not be
-accidentally translated.
-
 =back
 
 =head1 NOTES
 =back
 
 =head1 NOTES
@@ -569,22 +472,8 @@ accidentally translated.
 Anchors are represented by an <AI<n>> notation.
 The meaning of this non-standard notation might not be obvious.
 
 Anchors are represented by an <AI<n>> notation.
 The meaning of this non-standard notation might not be obvious.
 
-The create action calls xgettext.pl to do the actual work;
-the update action calls xgettext.pl, msgmerge(1) and msgattrib(1)
-to do the actual work.
-
 =head1 BUGS
 
 =head1 BUGS
 
-xgettext.pl must be present in the current directory; both
-msgmerge(1) and msgattrib(1) must also be present in the search path.
-The script currently does not check carefully whether these
-dependent commands are present.
-
-Locale::PO(3) has a lot of bugs. It can neither parse nor
-generate GNU PO files properly; a couple of workarounds have
-been written in TmplTokenizer and more is likely to be needed
-(e.g., to get rid of the "Strange line" warning for #~).
-
 This script may not work in Windows.
 
 There are probably some other bugs too, since this has not been
 This script may not work in Windows.
 
 There are probably some other bugs too, since this has not been
@@ -592,12 +481,7 @@ tested very much.
 
 =head1 SEE ALSO
 
 
 =head1 SEE ALSO
 
-xgettext.pl,
 TmplTokenizer.pm,
 TmplTokenizer.pm,
-msgmerge(1),
 Locale::PO(3),
 Locale::PO(3),
-translator_doc.txt
-
-http://www.saas.nsw.edu.au/koha_wiki/index.php?page=DifficultTerms
 
 =cut
 
 =cut
index f53dd46..3c186d3 100755 (executable)
@@ -54,14 +54,13 @@ usage() if $#ARGV != 1 && $#ARGV != 0;
 
 my ($cmd, $lang) = @ARGV;
 $cmd = lc $cmd;
 
 my ($cmd, $lang) = @ARGV;
 $cmd = lc $cmd;
-if ( $cmd =~ /^(create|install|update|compress|uncompress)$/ ) {
+if ( $cmd =~ /^(install|compress|uncompress)$/ ) {
     my $installer = LangInstaller->new( $lang, $pref, $verbose );
     my $installer = LangInstaller->new( $lang, $pref, $verbose );
-    if ( $cmd ne 'create' and $lang and not grep( {$_ eq $lang} @{ $installer->{langs} } ) ) {
+    if ( $lang and not grep( {$_ eq $lang} @{ $installer->{langs} } ) ) {
         print "Unsupported language: $lang\n";
         exit;
     }
     if ( $all ) {
         print "Unsupported language: $lang\n";
         exit;
     }
     if ( $all ) {
-        usage() if $cmd eq 'create';
         for my $lang ( @{$installer->{langs}} ) {
             $installer->set_lang( $lang );
             $installer->$cmd(\@files);
         for my $lang ( @{$installer->{langs}} ) {
             $installer->set_lang( $lang );
             $installer->$cmd(\@files);
@@ -71,9 +70,19 @@ if ( $cmd =~ /^(create|install|update|compress|uncompress)$/ ) {
         $installer->$cmd(\@files);
     }
 
         $installer->$cmd(\@files);
     }
 
-    Koha::Caches->get_instance()->flush_all if $cmd ne 'update';
-}
-else {
+    Koha::Caches->get_instance()->flush_all;
+} elsif ($cmd eq 'create' or $cmd eq 'update') {
+    my $command = "gulp po:$cmd";
+    $command .= " --silent" if (!$verbose);
+    $command .= " --lang $lang" if ($lang);
+
+    if ($verbose) {
+        print STDERR "Deprecation notice: PO creation and update are now gulp tasks. See docs/development/internationalization.md\n";
+        print STDERR "Running `$command`\n";
+    }
+
+    system($command);
+} else {
     usage();
 }
 
     usage();
 }
 
@@ -85,12 +94,9 @@ translate - Handle templates and preferences translation
 
 =head1 SYNOPSYS
 
 
 =head1 SYNOPSYS
 
-  translate create fr-FR
-  translate update fr-FR
   translate install fr-FR
   translate install fr-FR -f search -f memberentry
   translate -p install fr-FR
   translate install fr-FR
   translate install fr-FR -f search -f memberentry
   translate -p install fr-FR
-  translate install
   translate compress [fr-FR]
   translate uncompress [fr-FR]
 
   translate compress [fr-FR]
   translate uncompress [fr-FR]
 
@@ -98,7 +104,7 @@ translate - Handle templates and preferences translation
 
 In Koha, three categories of information are translated based on standard GNU
 .po files: opac templates pages, intranet templates and system preferences. The
 
 In Koha, three categories of information are translated based on standard GNU
 .po files: opac templates pages, intranet templates and system preferences. The
-script is a wrapper. It allows to quickly create/update/install .po files for a
+script is a wrapper. It allows to quickly install .po files for a
 given language or for all available languages.
 
 =head1 USAGE
 given language or for all available languages.
 
 =head1 USAGE
@@ -107,38 +113,6 @@ Use the -v or --verbose parameter to make translator more verbose.
 
 =over
 
 
 =over
 
-=item translate create F<lang>
-
-Create 3 .po files in F</misc/translator/po> subdirectory: (1) from opac pages
-templates, (2) intranet templates, and (3) from preferences. English 'en'
-version of templates and preferences are used as references.
-
-=over
-
-=item F<lang>-opac-{theme}.po
-
-Contains extracted text from english (en) OPAC templates found in
-<KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
-
-=item F<lang>-intranet.po
-
-Contains extracted text from english (en) intranet templates found in
-<KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
-
-=item F<lang>-pref.po
-
-Contains extracted text from english (en) preferences. They are found in files
-located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
-directory.
-
-=back
-
-=item translate [-p] update F<lang>
-
-Update .po files in F<po> directory, named F<lang>-*.po. Without F<lang>, all
-available languages are updated. With -p option, only preferences .po file is
-updated.
-
 =item translate [-p|-f] install F<lang>
 
 Use .po files to translate the english version of templates and preferences files
 =item translate [-p|-f] install F<lang>
 
 Use .po files to translate the english version of templates and preferences files
diff --git a/misc/translator/xgettext-installer b/misc/translator/xgettext-installer
new file mode 100755 (executable)
index 0000000..fd9dc29
--- /dev/null
@@ -0,0 +1,158 @@
+#!/usr/bin/perl
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+=head1 NAME
+
+xgettext-installer - extract translatable strings from installer YAML files
+
+=head1 SYNOPSIS
+
+xgettext-installer [OPTION] [INPUTFILE]...
+
+=head1 OPTIONS
+
+=over
+
+=item B<-f, --files-from=FILE>
+
+get list of input files from FILE
+
+=item B<-o, --output=FILE>
+
+write output to the specified file
+
+=item B<-h, --help>
+
+display this help and exit
+
+=back
+
+=cut
+
+use Modern::Perl;
+
+use Getopt::Long;
+use Locale::PO;
+use Pod::Usage;
+use YAML::Syck qw(LoadFile);
+
+$YAML::Syck::ImplicitTyping = 1;
+
+my $output = 'messages.pot';
+my $files_from;
+my $help;
+
+GetOptions(
+    'output=s' => \$output,
+    'files-from=s' => \$files_from,
+    'help' => \$help,
+) or pod2usage(-verbose => 1, -exitval => 2);
+
+if ($help) {
+    pod2usage(-verbose => 1, -exitval => 0);
+}
+
+my @files = @ARGV;
+if ($files_from) {
+    open(my $fh, '<', $files_from) or die "Cannot open $files_from: $!";
+    push @files, <$fh>;
+    chomp @files;
+    close $fh;
+}
+
+my $pot = {
+    '' => Locale::PO->new(
+        -msgid  => '',
+        -msgstr =>
+            "Project-Id-Version: Koha\n"
+          . "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
+          . "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\n"
+          . "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\n"
+          . "MIME-Version: 1.0\n"
+          . "Content-Type: text/plain; charset=UTF-8\n"
+          . "Content-Transfer-Encoding: 8bit\n"
+    ),
+};
+
+for my $file (@files) {
+    my $yaml = LoadFile($file);
+    my @tables = @{ $yaml->{'tables'} };
+
+    my $tablec = 0;
+    for my $table (@tables) {
+        $tablec++;
+
+        my $table_name = ( keys %$table )[0];
+        my @translatable = @{ $table->{$table_name}->{translatable} };
+        my @rows = @{ $table->{$table_name}->{rows} };
+        my @multiline = @{ $table->{$table_name}->{'multiline'} };
+
+        my $rowc = 0;
+        for my $row (@rows) {
+            $rowc++;
+
+            for my $field (@translatable) {
+                if ( @multiline and grep { $_ eq $field } @multiline ) {
+                    # multiline fields, only notices ATM
+                    my $mulc;
+                    foreach my $line ( @{ $row->{$field} } ) {
+                        $mulc++;
+
+                        # discard pure html, TT, empty
+                        next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ );
+
+                        # put placeholders
+                        $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/g;
+
+                        # discard non strings
+                        next if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ or length($line) < 2 );
+                        if ( not $pot->{$line} ) {
+                            my $msg = new Locale::PO(
+                                -msgid  => $line,
+                                -msgstr => '',
+                                -reference => "$file:$table_name:$tablec:row:$rowc:mul:$mulc"
+                            );
+                            $pot->{$line} = $msg;
+                        }
+                    }
+                } elsif (defined $row->{$field} && length($row->{$field}) > 1 && !$pot->{ $row->{$field} }) {
+                    my $msg = new Locale::PO(
+                        -msgid     => $row->{$field},
+                        -msgstr    => '',
+                        -reference => "$file:$table_name:$tablec:row:$rowc"
+                    );
+                    $pot->{ $row->{$field} } = $msg;
+                }
+            }
+        }
+    }
+
+    my $desccount = 0;
+    for my $description ( @{ $yaml->{'description'} } ) {
+        $desccount++;
+        if ( length($description) > 1 and not $pot->{$description} ) {
+            my $msg = new Locale::PO(
+                -msgid     => $description,
+                -msgstr    => '',
+                -reference => "$file:description:$desccount"
+            );
+            $pot->{$description} = $msg;
+        }
+    }
+}
+
+Locale::PO->save_file_fromhash($output, $pot);
diff --git a/misc/translator/xgettext-pref b/misc/translator/xgettext-pref
new file mode 100755 (executable)
index 0000000..1113a27
--- /dev/null
@@ -0,0 +1,151 @@
+#!/usr/bin/perl
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+=head1 NAME
+
+xgettext-pref - extract translatable strings from system preferences YAML files
+
+=head1 SYNOPSIS
+
+xgettext-pref [OPTION] [INPUTFILE]...
+
+=head1 OPTIONS
+
+=over
+
+=item B<-f, --files-from=FILE>
+
+get list of input files from FILE
+
+=item B<-o, --output=FILE>
+
+write output to the specified file
+
+=item B<-h, --help>
+
+display this help and exit
+
+=back
+
+=cut
+
+use Modern::Perl;
+
+use File::Basename;
+use Getopt::Long;
+use Locale::PO;
+use Pod::Usage;
+use YAML::Syck qw(LoadFile);
+
+$YAML::Syck::ImplicitTyping = 1;
+
+my $output = 'messages.pot';
+my $files_from;
+my $help;
+
+GetOptions(
+    'output=s' => \$output,
+    'files-from=s' => \$files_from,
+    'help' => \$help,
+) or pod2usage(-verbose => 1, -exitval => 2);
+
+if ($help) {
+    pod2usage(-verbose => 1, -exitval => 0);
+}
+
+my @files = @ARGV;
+if ($files_from) {
+    open(my $fh, '<', $files_from) or die "Cannot open $files_from: $!";
+    push @files, <$fh>;
+    chomp @files;
+    close $fh;
+}
+
+my $pot = {
+    '' => Locale::PO->new(
+        -msgid  => '',
+        -msgstr => "Project-Id-Version: Koha\n"
+          . "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
+          . "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\n"
+          . "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\n"
+          . "MIME-Version: 1.0\n"
+          . "Content-Type: text/plain; charset=UTF-8\n"
+          . "Content-Transfer-Encoding: 8bit\n"
+    ),
+};
+
+for my $file (@files) {
+    my $pref = LoadFile($file);
+    while ( my ($tab, $tab_content) = each %$pref ) {
+        add_po(undef, basename($file));
+
+        if ( ref($tab_content) eq 'ARRAY' ) {
+            add_prefs( $file, $tab, $tab_content );
+        } else {
+            while ( my ($section, $sysprefs) = each %$tab_content ) {
+                my $context = "$tab > $section";
+                my $msgid = sprintf('%s %s', basename($file), $section);
+                add_po($tab, $msgid);
+                add_prefs( $file, $context, $sysprefs );
+            }
+        }
+    }
+}
+
+Locale::PO->save_file_fromhash($output, $pot);
+
+sub add_prefs {
+    my ($file, $context, $prefs) = @_;
+
+    for my $pref (@$prefs) {
+        my $pref_name = '';
+        for my $element (@$pref) {
+            if ( ref($element) eq 'HASH' ) {
+                $pref_name = $element->{pref};
+                last;
+            }
+        }
+        for my $element (@$pref) {
+            if ( ref($element) eq 'HASH' ) {
+                while ( my ( $key, $value ) = each(%$element) ) {
+                    next unless $key eq 'choices' or $key eq 'multiple';
+                    next unless ref($value) eq 'HASH';
+                    for my $ckey ( keys %$value ) {
+                        my $msgid = sprintf('%s#%s# %s', basename($file), $pref_name, $value->{$ckey});
+                        add_po( "$context > $pref_name", $msgid );
+                    }
+                }
+            }
+            elsif ($element) {
+                my $msgid = sprintf('%s#%s# %s', basename($file), $pref_name, $element);
+                add_po( "$context > $pref_name", $msgid );
+            }
+        }
+    }
+}
+
+sub add_po {
+    my ($comment, $msgid ) = @_;
+
+    return unless $msgid;
+
+    $pot->{$msgid} = Locale::PO->new(
+        -comment   => $comment,
+        -msgid     => $msgid,
+        -msgstr    => '',
+    );
+}
diff --git a/misc/translator/xgettext-tt2 b/misc/translator/xgettext-tt2
new file mode 100755 (executable)
index 0000000..02bb61f
--- /dev/null
@@ -0,0 +1,56 @@
+#!/usr/bin/perl
+
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
+use Modern::Perl;
+
+my $xgettext = Locale::XGettext::TT2::Koha->newFromArgv(\@ARGV);
+$xgettext->setOption('plug_in', '');
+$xgettext->run;
+$xgettext->output;
+
+package Locale::XGettext::TT2::Koha;
+
+use parent 'Locale::XGettext::TT2';
+
+sub defaultKeywords {
+    return [
+        't:1',
+        'tx:1',
+        'tn:1,2',
+        'tnx:1,2',
+        'txn:1,2',
+        'tp:1c,2',
+        'tpx:1c,2',
+        'tnp:1c,2,3',
+        'tnpx:1c,2,3',
+    ];
+}
+
+sub defaultFlags {
+    return [
+        'tx:1:perl-brace-format',
+        'tnx:1:perl-brace-format',
+        'tnx:2:perl-brace-format',
+        'txn:1:perl-brace-format',
+        'txn:2:perl-brace-format',
+        'tpx:2:perl-brace-format',
+        'tnpx:2:perl-brace-format',
+        'tnpx:3:perl-brace-format',
+    ],
+}
+
+1;
index f9ba3bf..e04c529 100755 (executable)
@@ -1,5 +1,20 @@
 #!/usr/bin/perl
 
 #!/usr/bin/perl
 
+# This file is part of Koha.
+#
+# Koha is free software; you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 3 of the License, or
+# (at your option) any later version.
+#
+# Koha is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with Koha; if not, see <http://www.gnu.org/licenses>.
+
 =head1 NAME
 
 xgettext.pl - xgettext(1)-like interface for .tt strings extraction
 =head1 NAME
 
 xgettext.pl - xgettext(1)-like interface for .tt strings extraction
@@ -173,7 +188,7 @@ EOF
     print $OUTPUT <<EOF;
 msgid ""
 msgstr ""
     print $OUTPUT <<EOF;
 msgid ""
 msgstr ""
-"Project-Id-Version: PACKAGE VERSION\\n"
+"Project-Id-Version: Koha\\n"
 "POT-Creation-Date: $time_pot\\n"
 "PO-Revision-Date: $time_po\\n"
 "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n"
 "POT-Creation-Date: $time_pot\\n"
 "PO-Revision-Date: $time_po\\n"
 "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n"
index da53cab..a209070 100644 (file)
     "bootstrap": "^4.5.2",
     "gulp": "^4.0.2",
     "gulp-autoprefixer": "^4.0.0",
     "bootstrap": "^4.5.2",
     "gulp": "^4.0.2",
     "gulp-autoprefixer": "^4.0.0",
+    "gulp-concat-po": "^1.0.0",
     "gulp-cssnano": "^2.1.2",
     "gulp-cssnano": "^2.1.2",
+    "gulp-exec": "^4.0.0",
     "gulp-rename": "^2.0.0",
     "gulp-rtlcss": "^1.4.1",
     "gulp-sass": "^3.1.0",
     "gulp-sourcemaps": "^2.6.1",
     "gulp-rename": "^2.0.0",
     "gulp-rtlcss": "^1.4.1",
     "gulp-sass": "^3.1.0",
     "gulp-sourcemaps": "^2.6.1",
+    "merge-stream": "^2.0.0",
     "minimist": "^1.2.5"
   },
   "scripts": {
     "minimist": "^1.2.5"
   },
   "scripts": {
diff --git a/t/LangInstaller.t b/t/LangInstaller.t
deleted file mode 100755 (executable)
index 6e122b3..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
-use Modern::Perl;
-
-use FindBin '$Bin';
-use lib "$Bin/../misc/translator";
-
-use Test::More tests => 39;
-use File::Temp qw(tempdir);
-use File::Slurp;
-use Locale::PO;
-
-use t::lib::Mocks;
-
-use_ok('LangInstaller');
-
-my $installer = LangInstaller->new();
-
-my $tempdir = tempdir(CLEANUP => 0);
-t::lib::Mocks::mock_config('intrahtdocs', "$Bin/LangInstaller/templates");
-my @files = ('simple.tt');
-$installer->extract_messages_from_templates($tempdir, 'intranet', @files);
-
-my $tempfile = "$tempdir/koha-tmpl/intranet-tmpl/simple.tt";
-ok(-e $tempfile, 'it has created a temporary file simple.tt');
-SKIP: {
-    skip "simple.tt does not exist", 37 unless -e $tempfile;
-
-    my $output = read_file($tempfile);
-    my $expected_output = <<'EOF';
-__('hello');
-__x('hello {name}');
-__n('item', 'items');
-__nx('{count} item', '{count} items');
-__p('context', 'hello');
-__px('context', 'hello {name}');
-__np('context', 'item', 'items');
-__npx('context', '{count} item', '{count} items');
-__npx('context', '{count} item', '{count} items');
-__x('status is {status}');
-__('active');
-__('inactive');
-__('Inside block');
-EOF
-
-    is($output, $expected_output, "Output of extract_messages_from_templates is as expected");
-
-    my $xgettext_cmd = "xgettext -L Perl --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 "
-        . "-o $tempdir/Koha.pot -D $tempdir koha-tmpl/intranet-tmpl/simple.tt";
-
-    system($xgettext_cmd);
-    my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
-
-    my @expected = (
-        {
-            msgid => '"hello"',
-        },
-        {
-            msgid => '"hello {name}"',
-        },
-        {
-            msgid => '"item"',
-            msgid_plural => '"items"',
-        },
-        {
-            msgid => '"{count} item"',
-            msgid_plural => '"{count} items"',
-        },
-        {
-            msgid => '"hello"',
-            msgctxt => '"context"',
-        },
-        {
-            msgid => '"hello {name}"',
-            msgctxt => '"context"',
-        },
-        {
-            msgid => '"item"',
-            msgid_plural => '"items"',
-            msgctxt => '"context"',
-        },
-        {
-            msgid => '"{count} item"',
-            msgid_plural => '"{count} items"',
-            msgctxt => '"context"',
-        },
-        {
-            msgid => '"status is {status}"',
-        },
-        {
-            msgid => '"active"',
-        },
-        {
-            msgid => '"inactive"',
-        },
-        {
-            msgid => '"Inside block"',
-        },
-    );
-
-    for (my $i = 0; $i < @expected; $i++) {
-        for my $key (qw(msgid msgid_plural msgctxt)) {
-            my $expected = $expected[$i]->{$key};
-            my $expected_str = defined $expected ? $expected : 'not defined';
-            is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
-        }
-    }
-}
diff --git a/t/LangInstaller/templates/simple.tt b/t/LangInstaller/templates/simple.tt
deleted file mode 100644 (file)
index dbc4ce3..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-[% USE raw %]
-[% PROCESS 'i18n.inc' %]
-[% t('hello') | $raw %]
-[% tx('hello {name}', { name = 'Bob' }) | $raw %]
-[% tn('item', 'items', count) | $raw %]
-[% tnx('{count} item', '{count} items', count, { count = count }) | $raw %]
-[% tp('context', 'hello') | $raw %]
-[% tpx('context', 'hello {name}', { name = 'Bob' }) | $raw %]
-[% tnp('context', 'item', 'items', count) | $raw %]
-[% tnpx('context', '{count} item', '{count} items', count, { count = count }) | $raw %]
-
-[% # it also works on multiple lines
-    tnpx (
-        'context',
-        '{count} item',
-        '{count} items',
-        count,
-        {
-            count = count,
-        }
-    ) | $raw
-%]
-
-[% # and t* calls can be nested
-    tx('status is {status}', {
-        status = active ? t('active') : t('inactive')
-    }) | $raw
-%]
-
-[%# but a TT comment won't get picked
-    t('not translatable')
-%]
-
-[% BLOCK %]
-    [% t('Inside block') | $raw %]
-[% END %]
diff --git a/t/misc/translator/sample.pref b/t/misc/translator/sample.pref
new file mode 100644 (file)
index 0000000..c42692b
--- /dev/null
@@ -0,0 +1,14 @@
+Section:
+    Subsection:
+        -
+            - pref: SamplePref
+              choices:
+                on: Do
+                off: Do not do
+            - that thing
+        -
+            - pref: MultiplePref
+              multiple:
+                foo: Foo ツ
+                bar: Bar
+                baz: Baz
diff --git a/t/misc/translator/sample.tt b/t/misc/translator/sample.tt
new file mode 100644 (file)
index 0000000..3fd4fca
--- /dev/null
@@ -0,0 +1,36 @@
+[% USE raw %]
+[% PROCESS 'i18n.inc' %]
+[% t('hello ツ') | $raw %]
+[% tx('hello {name}', { name = 'Bob' }) | $raw %]
+[% tn('item', 'items', count) | $raw %]
+[% tnx('{count} item', '{count} items', count, { count = count }) | $raw %]
+[% tp('context', 'hello') | $raw %]
+[% tpx('context', 'hello {name}', { name = 'Bob' }) | $raw %]
+[% tnp('context', 'item', 'items', count) | $raw %]
+[% tnpx('context', '{count} item', '{count} items', count, { count = count }) | $raw %]
+
+[% # it also works on multiple lines
+    tnpx (
+        'context',
+        '{count} item',
+        '{count} items',
+        count,
+        {
+            count = count,
+        }
+    ) | $raw
+%]
+
+[% # and t* calls can be nested
+    tx('status is {status}', {
+        status = active ? t('active') : t('inactive')
+    }) | $raw
+%]
+
+[%# but a TT comment won't get picked
+    t('not translatable')
+%]
+
+[% BLOCK %]
+    [% t('Inside block') | $raw %]
+[% END %]
diff --git a/t/misc/translator/sample.yml b/t/misc/translator/sample.yml
new file mode 100644 (file)
index 0000000..d6d675f
--- /dev/null
@@ -0,0 +1,15 @@
+description:
+  - "Sample installer file"
+
+tables:
+  - table1:
+        translatable: [ column1, column2 ]
+        multiline: [ column2 ]
+        rows:
+          - column1: foo ツ
+            column2:
+              - bar
+              - baz
+            column3: qux
+            column4:
+              - quux
diff --git a/t/misc/translator/xgettext-installer.t b/t/misc/translator/xgettext-installer.t
new file mode 100644 (file)
index 0000000..4043610
--- /dev/null
@@ -0,0 +1,32 @@
+#!/usr/bin/perl
+
+use Modern::Perl;
+
+use File::Slurp;
+use File::Temp qw(tempdir);
+use FindBin qw($Bin);
+use Locale::PO;
+use Test::More tests => 4;
+
+my $tempdir = tempdir(CLEANUP => 1);
+
+write_file("$tempdir/files", "$Bin/sample.yml");
+
+my $xgettext_cmd = "$Bin/../../../misc/translator/xgettext-installer "
+    . "-o $tempdir/Koha.pot -f $tempdir/files";
+
+system($xgettext_cmd);
+my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
+
+my @expected = (
+    { msgid => '"Sample installer file"' },
+    { msgid => '"bar"' },
+    { msgid => '"baz"' },
+    { msgid => '"foo ツ"' },
+);
+
+for (my $i = 0; $i < @expected; $i++) {
+    my $expected = $expected[$i]->{msgid};
+    my $expected_str = defined $expected ? $expected : 'not defined';
+    is($pot->[$i + 1]->msgid, $expected, "$i: msgid is $expected_str");
+}
diff --git a/t/misc/translator/xgettext-pref.t b/t/misc/translator/xgettext-pref.t
new file mode 100644 (file)
index 0000000..8c699fe
--- /dev/null
@@ -0,0 +1,54 @@
+#!/usr/bin/perl
+
+use Modern::Perl;
+
+use File::Slurp;
+use File::Temp qw(tempdir);
+use FindBin qw($Bin);
+use Locale::PO;
+use Test::More tests => 16;
+
+my $tempdir = tempdir(CLEANUP => 1);
+
+write_file("$tempdir/files", "$Bin/sample.pref");
+
+my $xgettext_cmd = "$Bin/../../../misc/translator/xgettext-pref "
+    . "-o $tempdir/Koha.pot -f $tempdir/files";
+
+system($xgettext_cmd);
+my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
+
+my @expected = (
+    {
+        msgid => '"sample.pref"',
+    },
+    {
+        msgid => '"sample.pref Subsection"',
+    },
+    {
+        msgid => '"sample.pref#MultiplePref# Bar"',
+    },
+    {
+        msgid => '"sample.pref#MultiplePref# Baz"',
+    },
+    {
+        msgid => '"sample.pref#MultiplePref# Foo ツ"',
+    },
+    {
+        msgid => '"sample.pref#SamplePref# Do"',
+    },
+    {
+        msgid => '"sample.pref#SamplePref# Do not do"',
+    },
+    {
+        msgid => '"sample.pref#SamplePref# that thing"',
+    },
+);
+
+for (my $i = 0; $i < @expected; $i++) {
+    for my $key (qw(msgid msgctxt)) {
+        my $expected = $expected[$i]->{$key};
+        my $expected_str = defined $expected ? $expected : 'not defined';
+        is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
+    }
+}
diff --git a/t/misc/translator/xgettext-tt2.t b/t/misc/translator/xgettext-tt2.t
new file mode 100755 (executable)
index 0000000..e2ae734
--- /dev/null
@@ -0,0 +1,74 @@
+#!/usr/bin/perl
+
+use Modern::Perl;
+
+use File::Slurp;
+use File::Temp qw(tempdir);
+use FindBin qw($Bin);
+use Locale::PO;
+use Test::More tests => 36;
+
+my $tempdir = tempdir(CLEANUP => 1);
+
+write_file("$tempdir/files", "$Bin/sample.tt");
+
+my $xgettext_cmd = "$Bin/../../../misc/translator/xgettext-tt2 --from-code=UTF-8 "
+    . "-o $tempdir/Koha.pot -f $tempdir/files";
+
+system($xgettext_cmd);
+my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
+
+my @expected = (
+    {
+        msgid => '"hello ツ"',
+    },
+    {
+        msgid => '"hello {name}"',
+    },
+    {
+        msgid => '"item"',
+        msgid_plural => '"items"',
+    },
+    {
+        msgid => '"{count} item"',
+        msgid_plural => '"{count} items"',
+    },
+    {
+        msgid => '"hello"',
+        msgctxt => '"context"',
+    },
+    {
+        msgid => '"hello {name}"',
+        msgctxt => '"context"',
+    },
+    {
+        msgid => '"item"',
+        msgid_plural => '"items"',
+        msgctxt => '"context"',
+    },
+    {
+        msgid => '"{count} item"',
+        msgid_plural => '"{count} items"',
+        msgctxt => '"context"',
+    },
+    {
+        msgid => '"status is {status}"',
+    },
+    {
+        msgid => '"active"',
+    },
+    {
+        msgid => '"inactive"',
+    },
+    {
+        msgid => '"Inside block"',
+    },
+);
+
+for (my $i = 0; $i < @expected; $i++) {
+    for my $key (qw(msgid msgid_plural msgctxt)) {
+        my $expected = $expected[$i]->{$key};
+        my $expected_str = defined $expected ? $expected : 'not defined';
+        is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
+    }
+}
index f8cb615..9ba09ab 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -1522,6 +1522,19 @@ gulp-cli@^2.2.0:
     v8flags "^3.2.0"
     yargs "^7.1.0"
 
     v8flags "^3.2.0"
     yargs "^7.1.0"
 
+gulp-concat-po@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/gulp-concat-po/-/gulp-concat-po-1.0.0.tgz#2fe7b2c12e45a566238e228f63396838013770ae"
+  integrity sha512-hFDZrUJcpw10TW3BfptL5W2FV/aMo3M+vxz9YQV4nlMBDAi8gs9/yZYZcYMYfl5XKhjpebSef8nyruoWdlX8Hw==
+  dependencies:
+    lodash.find "^4.6.0"
+    lodash.merge "^4.6.2"
+    lodash.uniq "^4.5.0"
+    plugin-error "^1.0.1"
+    pofile "^1.1.0"
+    through2 "^0.6.5"
+    vinyl "^2.2.0"
+
 gulp-cssnano@^2.1.2:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
 gulp-cssnano@^2.1.2:
   version "2.1.3"
   resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
@@ -1533,6 +1546,15 @@ gulp-cssnano@^2.1.2:
     plugin-error "^1.0.1"
     vinyl-sourcemaps-apply "^0.2.1"
 
     plugin-error "^1.0.1"
     vinyl-sourcemaps-apply "^0.2.1"
 
+gulp-exec@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/gulp-exec/-/gulp-exec-4.0.0.tgz#4b6b67be0200d620143f3198a64257b68b146bb6"
+  integrity sha512-A9JvTyB3P4huusd/43bTr6SDg3MqBxL9AQbLnsKSO6/91wVkHfxgeJZlgDMkqK8sMel4so8wcko4SZOeB1UCgA==
+  dependencies:
+    lodash.template "^4.4.0"
+    plugin-error "^1.0.1"
+    through2 "^3.0.1"
+
 gulp-rename@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c"
 gulp-rename@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c"
@@ -2205,6 +2227,11 @@ lodash.escape@^3.0.0:
   dependencies:
     lodash._root "^3.0.0"
 
   dependencies:
     lodash._root "^3.0.0"
 
+lodash.find@^4.6.0:
+  version "4.6.0"
+  resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
+  integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=
+
 lodash.isarguments@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
 lodash.isarguments@^3.0.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
@@ -2229,6 +2256,11 @@ lodash.memoize@^4.1.2:
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
 
   resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
   integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
 
+lodash.merge@^4.6.2:
+  version "4.6.2"
+  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
+  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
 lodash.restparam@^3.0.0:
   version "3.6.1"
   resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
 lodash.restparam@^3.0.0:
   version "3.6.1"
   resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
@@ -2249,6 +2281,14 @@ lodash.template@^3.0.0:
     lodash.restparam "^3.0.0"
     lodash.templatesettings "^3.0.0"
 
     lodash.restparam "^3.0.0"
     lodash.templatesettings "^3.0.0"
 
+lodash.template@^4.4.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
+  integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
+  dependencies:
+    lodash._reinterpolate "^3.0.0"
+    lodash.templatesettings "^4.0.0"
+
 lodash.templatesettings@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
 lodash.templatesettings@^3.0.0:
   version "3.1.1"
   resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
@@ -2257,6 +2297,13 @@ lodash.templatesettings@^3.0.0:
     lodash._reinterpolate "^3.0.0"
     lodash.escape "^3.0.0"
 
     lodash._reinterpolate "^3.0.0"
     lodash.escape "^3.0.0"
 
+lodash.templatesettings@^4.0.0:
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
+  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
+  dependencies:
+    lodash._reinterpolate "^3.0.0"
+
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
 lodash.uniq@^4.5.0:
   version "4.5.0"
   resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
@@ -2359,6 +2406,11 @@ meow@^3.7.0:
     redent "^1.0.0"
     trim-newlines "^1.0.0"
 
     redent "^1.0.0"
     trim-newlines "^1.0.0"
 
+merge-stream@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
+  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
+
 micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
 micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
   version "3.1.10"
   resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
@@ -2842,6 +2894,11 @@ plugin-error@^1.0.1:
     arr-union "^3.1.0"
     extend-shallow "^3.0.2"
 
     arr-union "^3.1.0"
     extend-shallow "^3.0.2"
 
+pofile@^1.1.0:
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.1.0.tgz#9ce84bbef5043ceb4f19bdc3520d85778fad4f94"
+  integrity sha512-6XYcNkXWGiJ2CVXogTP7uJ6ZXQCldYLZc16wgRp8tqRaBTTyIfF+TUT3EQJPXTLAT7OTPpTAoaFdoXKfaTRU1w==
+
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
 posix-character-classes@^0.1.0:
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
@@ -3177,6 +3234,25 @@ read-pkg@^1.0.0:
     normalize-package-data "^2.3.2"
     path-type "^1.0.0"
 
     normalize-package-data "^2.3.2"
     path-type "^1.0.0"
 
+"readable-stream@2 || 3":
+  version "3.6.0"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
+  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  dependencies:
+    inherits "^2.0.3"
+    string_decoder "^1.1.1"
+    util-deprecate "^1.0.1"
+
+"readable-stream@>=1.0.33-1 <1.1.0-0":
+  version "1.0.34"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
+  integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.1"
+    isarray "0.0.1"
+    string_decoder "~0.10.x"
+
 readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
 readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
   version "2.3.7"
   resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
@@ -3293,9 +3369,9 @@ replace-ext@0.0.1:
   integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
 
 replace-ext@^1.0.0:
   integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
 
 replace-ext@^1.0.0:
-  version "1.0.1"
-  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a"
-  integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
+  integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
 
 replace-homedir@^1.0.0:
   version "1.0.0"
 
 replace-homedir@^1.0.0:
   version "1.0.0"
@@ -3407,6 +3483,11 @@ safe-buffer@~5.1.0, safe-buffer@~5.1.1:
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
   resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
   integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
 
+safe-buffer@~5.2.0:
+  version "5.2.0"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
+  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
+
 safe-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
 safe-regex@^1.1.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
@@ -3668,6 +3749,13 @@ string-width@^3.0.0, string-width@^3.1.0:
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
     is-fullwidth-code-point "^2.0.0"
     strip-ansi "^5.1.0"
 
+string_decoder@^1.1.1:
+  version "1.3.0"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
+  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  dependencies:
+    safe-buffer "~5.2.0"
+
 string_decoder@~0.10.x:
   version "0.10.31"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
 string_decoder@~0.10.x:
   version "0.10.31"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
@@ -3790,6 +3878,21 @@ through2@2.X, through2@^2.0.0, through2@^2.0.3, through2@^2.0.5, through2@~2.0.0
     readable-stream "~2.3.6"
     xtend "~4.0.1"
 
     readable-stream "~2.3.6"
     xtend "~4.0.1"
 
+through2@^0.6.5:
+  version "0.6.5"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
+  integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
+  dependencies:
+    readable-stream ">=1.0.33-1 <1.1.0-0"
+    xtend ">=4.0.0 <4.1.0-0"
+
+through2@^3.0.1:
+  version "3.0.1"
+  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a"
+  integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==
+  dependencies:
+    readable-stream "2 || 3"
+
 time-stamp@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
 time-stamp@^1.0.0:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
@@ -3974,7 +4077,7 @@ use@^3.1.0:
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
   resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
   integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
 
-util-deprecate@~1.0.1:
+util-deprecate@^1.0.1, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
   integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
@@ -4070,7 +4173,7 @@ vinyl@^0.5.0:
     clone-stats "^0.0.1"
     replace-ext "0.0.1"
 
     clone-stats "^0.0.1"
     replace-ext "0.0.1"
 
-vinyl@^2.0.0:
+vinyl@^2.0.0, vinyl@^2.2.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
   integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
   integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
@@ -4133,7 +4236,7 @@ wrappy@1:
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
   resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
   integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
 
-xtend@~4.0.0, xtend@~4.0.1:
+"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1:
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
   version "4.0.2"
   resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
   integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==