View | Details | Raw Unified | Return to bug 25067
Collapse All | Expand All

(-)a/cpanfile (-1 / +1 lines)
Lines 144-149 recommends 'Gravatar::URL', '1.03'; Link Here
144
recommends 'HTTPD::Bench::ApacheBench', '0.73';
144
recommends 'HTTPD::Bench::ApacheBench', '0.73';
145
recommends 'LWP::Protocol::https', '5.836';
145
recommends 'LWP::Protocol::https', '5.836';
146
recommends 'Lingua::Ispell', '0.07';
146
recommends 'Lingua::Ispell', '0.07';
147
recommends 'Locale::XGettext::TT2', '0.7';
147
recommends 'Module::Bundled::Files', '0.03';
148
recommends 'Module::Bundled::Files', '0.03';
148
recommends 'Module::Load::Conditional', '0.38';
149
recommends 'Module::Load::Conditional', '0.38';
149
recommends 'Module::Pluggable', '3.9';
150
recommends 'Module::Pluggable', '3.9';
Lines 155-161 recommends 'Net::SFTP::Foreign', '1.73'; Link Here
155
recommends 'Net::Server', '0.97';
156
recommends 'Net::Server', '0.97';
156
recommends 'Net::Z3950::SimpleServer', '1.15';
157
recommends 'Net::Z3950::SimpleServer', '1.15';
157
recommends 'PDF::FromHTML', '0.31';
158
recommends 'PDF::FromHTML', '0.31';
158
recommends 'PPI', '1.215';
159
recommends 'Parallel::ForkManager', '0.75';
159
recommends 'Parallel::ForkManager', '0.75';
160
recommends 'Readonly', '0.01';
160
recommends 'Readonly', '0.01';
161
recommends 'Readonly::XS', '0.01';
161
recommends 'Readonly::XS', '0.01';
(-)a/docs/development/internationalization.md (+121 lines)
Line 0 Link Here
1
# Internationalization
2
3
This page documents how internationalization works in Koha.
4
5
## Making strings translatable
6
7
There are several ways of making a string translatable, depending on where it
8
is located
9
10
### In Template::Toolkit files (`*.tt`)
11
12
The simplest way to make a string translatable in a template is to do nothing.
13
Templates are parsed as HTML files and almost all text nodes are considered as
14
translatable strings. This also includes some attributes like `title` and
15
`placeholder`.
16
17
This method has some downsides: you don't have full control over what would
18
appear in PO files and you cannot use plural forms or context. In order to do
19
that you have to use `i18n.inc`
20
21
`i18n.inc` contains several macros that, when used, make a string translatable.
22
The first thing to do is to make these macros available by adding
23
24
    [% PROCESS 'i18n.inc' %]
25
26
at the top of the template file. Then you can use those macros.
27
28
The simplest one is `t(msgid)`
29
30
    [% t('This is a translatable string') %]
31
32
You can also use variable substitution with `tx(msgid, vars)`
33
34
    [% tx('Hello, {name}', { name = 'World' }) %]
35
36
You can use plural forms with `tn(msgid, msgid_plural, count)`
37
38
    [% tn('a child', 'several children', number_of_children) %]
39
40
You can add context, to help translators when a term is ambiguous, with
41
`tp(msgctxt, msgid)`
42
43
    [% tp('verb', 'order') %]
44
    [% tp('noun', 'order') %]
45
46
Or any combinations of the above
47
48
    [% tnpx('bibliographic record', '{count} item', '{count} items', items_count, { count = items_count }) %]
49
50
### In JavaScript files (`*.js`)
51
52
Like in templates, you have several functions available. Just replace `t` by `__`.
53
54
    __('This is a translatable string');
55
    __npx('bibliographic record, '{count} item', '{count} items', items_count, { count: items_count });
56
57
### In Perl files (`*.pl`, `*.pm`)
58
59
You will have to add
60
61
    use Koha::I18N;
62
63
at the top of the file, and then the same functions as above will be available.
64
65
    __('This is a translatable string');
66
    __npx('bibliographic record, '{count} item', '{count} items', $items_count, count => $items_count);
67
68
### In installer and preferences YAML files (`*.yml`)
69
70
Nothing special to do here. All strings will be automatically translatable.
71
72
## Manipulating PO files
73
74
Once strings have been made translatable in source files, they have to be
75
extracted into PO files and uploaded on https://translate.koha-community.org/
76
so they can be translated.
77
78
### Install gulp first
79
80
The next sections rely on gulp. If it's not installed, run the following
81
commands:
82
83
    # as root
84
    npm install gulp-cli -g
85
86
    # as normal user, from the root of Koha repository
87
    yarn
88
89
### Create PO files for a new language
90
91
If you want to add translations for a new language, you have to create the
92
missing PO files. You can do that by executing the following command:
93
94
    # Replace xx-XX by your language tag
95
    gulp po:create --lang xx-XX
96
97
New PO files will be available in `misc/translator/po`.
98
99
### Update PO files with new strings
100
101
When new features or bugfixes are added to Koha, new translatable strings can
102
be added, other can be removed or modified, and the PO file become out of sync.
103
104
To be able to translate the new or modified strings, you have to update PO
105
files. This can be done by executing the following command:
106
107
    # Update PO files for all languages
108
    gulp po:update
109
110
    # or only one language
111
    gulp po:update --lang xx-XX
112
113
### Only extract strings
114
115
Creating or updating PO files automatically extract strings, but if for some
116
reasons you want to only extract strings without touching PO files, you can run
117
the following command:
118
119
    gulp po:extract
120
121
POT files will be available in `misc/translator`.
(-)a/gulpfile.js (-3 / +232 lines)
Lines 10-20 try { Link Here
10
    process.exit(1);
10
    process.exit(1);
11
}
11
}
12
12
13
const gutil = require( "gulp-util" );
13
const child_process = require('child_process');
14
const fs = require('fs');
15
const os = require('os');
16
const path = require('path');
17
const util = require('util');
18
19
const minimist = require('minimist');
14
const sass = require("gulp-sass");
20
const sass = require("gulp-sass");
15
const cssnano = require("gulp-cssnano");
21
const cssnano = require("gulp-cssnano");
16
const sourcemaps = require('gulp-sourcemaps');
22
const sourcemaps = require('gulp-sourcemaps');
17
const autoprefixer = require('gulp-autoprefixer');
23
const autoprefixer = require('gulp-autoprefixer');
24
const concatPo = require('gulp-concat-po');
25
const exec = require('gulp-exec');
26
const merge = require('merge-stream');
27
const through2 = require('through2');
28
const Vinyl = require('vinyl');
18
29
19
const STAFF_JS_BASE = "koha-tmpl/intranet-tmpl/prog/js";
30
const STAFF_JS_BASE = "koha-tmpl/intranet-tmpl/prog/js";
20
const STAFF_CSS_BASE = "koha-tmpl/intranet-tmpl/prog/css";
31
const STAFF_CSS_BASE = "koha-tmpl/intranet-tmpl/prog/css";
Lines 26-32 var sassOptions = { Link Here
26
    precision: 3
37
    precision: 3
27
}
38
}
28
39
29
if( gutil.env.view == "opac" ){
40
const options = minimist(process.argv.slice(2), {
41
    string: ['view', 'lang'],
42
});
43
44
if( options.view == "opac" ){
30
    var css_base = OPAC_CSS_BASE;
45
    var css_base = OPAC_CSS_BASE;
31
    var js_base = OPAC_JS_BASE;
46
    var js_base = OPAC_JS_BASE;
32
} else {
47
} else {
Lines 58-61 gulp.task('build', function() { Link Here
58
73
59
gulp.task('watch', function(){
74
gulp.task('watch', function(){
60
    gulp.watch( css_base + "/src/**/*.scss", ['css'] );
75
    gulp.watch( css_base + "/src/**/*.scss", ['css'] );
61
});
76
});
77
78
const poTypes = [
79
    'marc-MARC21', 'marc-NORMARC', 'marc-UNIMARC',
80
    'staff-prog', 'opac-bootstrap', 'pref', 'messages', 'messages-js',
81
    'installer', 'installer-MARC21', 'installer-UNIMARC',
82
];
83
84
gulp.task('po:create', ['po:extract'], function () {
85
    const access = util.promisify(fs.access);
86
    const exec = util.promisify(child_process.exec);
87
88
    const languages = getLanguages();
89
    const promises = [];
90
    for (const language of languages) {
91
        const locale = language.split('-').filter(s => s.length !== 4).join('_');
92
        for (const type of poTypes) {
93
            const po = `misc/translator/po/${language}-${type}.po`;
94
            const pot = `misc/translator/Koha-${type}.pot`;
95
96
            const promise = access(po)
97
                .catch(() => exec(`msginit -o ${po} -i ${pot} -l ${locale} --no-translator`))
98
            promises.push(promise);
99
        }
100
    }
101
102
    return Promise.all(promises);
103
});
104
105
for (const type of poTypes) {
106
    gulp.task(`po:update:${type}`, [`po:extract:${type}`], po_update(type));
107
}
108
gulp.task('po:update', poTypes.map(type => `po:update:${type}`));
109
110
function po_update (type) {
111
    const msgmerge_opts = '--backup=off --quiet --sort-output --update';
112
    const cmd = `msgmerge ${msgmerge_opts} <%= file.path %> misc/translator/Koha-${type}.pot`;
113
    const languages = getLanguages();
114
    const globs = languages.map(language => `misc/translator/po/${language}-${type}.po`);
115
116
    return () => gulp.src(globs)
117
        .pipe(exec(cmd, { continueOnError: true }))
118
        .pipe(exec.reporter({ err: false, stdout: false }))
119
}
120
121
gulp.task('po:extract', poTypes.map(type => `po:extract:${type}`))
122
123
gulp.task('po:extract:marc-MARC21', po_extract_marc('MARC21'))
124
gulp.task('po:extract:marc-NORMARC', po_extract_marc('NORMARC'))
125
gulp.task('po:extract:marc-UNIMARC', po_extract_marc('UNIMARC'))
126
127
function po_extract_marc (type) {
128
    return () => gulp.src(`koha-tmpl/*-tmpl/*/en/**/*${type}*`, { read: false, nocase: true })
129
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', `Koha-marc-${type}.pot`))
130
        .pipe(gulp.dest('misc/translator'))
131
}
132
133
gulp.task('po:extract:staff-prog', function () {
134
    const globs = [
135
        'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
136
        'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
137
        '!koha-tmpl/intranet-tmpl/prog/en/**/*MARC21*',
138
        '!koha-tmpl/intranet-tmpl/prog/en/**/*NORMARC*',
139
        '!koha-tmpl/intranet-tmpl/prog/en/**/*UNIMARC*',
140
    ];
141
142
    return gulp.src(globs, { read: false, nocase: true })
143
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', 'Koha-staff-prog.pot'))
144
        .pipe(gulp.dest('misc/translator'))
145
})
146
147
gulp.task('po:extract:opac-bootstrap', function () {
148
    const globs = [
149
        'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
150
        'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
151
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*MARC21*',
152
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*NORMARC*',
153
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*UNIMARC*',
154
    ];
155
156
    return gulp.src(globs, { read: false, nocase: true })
157
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', 'Koha-opac-bootstrap.pot'))
158
        .pipe(gulp.dest('misc/translator'))
159
})
160
161
const xgettext_options = '--from-code=UTF-8 --package-name Koha '
162
    + '--package-version= -k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 '
163
    + '-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ '
164
    + '-kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3 --force-po';
165
166
gulp.task('po:extract:messages-js', function () {
167
    const globs = [
168
        'koha-tmpl/intranet-tmpl/prog/js/**/*.js',
169
        'koha-tmpl/opac-tmpl/bootstrap/js/**/*.js',
170
    ];
171
172
    return gulp.src(globs, { read: false, nocase: true })
173
        .pipe(xgettext(`xgettext -L JavaScript ${xgettext_options}`, 'Koha-messages-js.pot'))
174
        .pipe(gulp.dest('misc/translator'))
175
})
176
177
gulp.task('po:extract:messages', function () {
178
    const perlStream = gulp.src(['**/*.pl', '**/*.pm'], { read: false, nocase: true })
179
        .pipe(xgettext(`xgettext -L Perl ${xgettext_options}`, 'Koha-perl.pot'))
180
181
    const ttStream = gulp.src([
182
            'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
183
            'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
184
            'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
185
            'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
186
        ], { read: false, nocase: true })
187
        .pipe(xgettext('misc/translator/xgettext-tt2 --from-code=UTF-8', 'Koha-tt.pot'))
188
189
    const headers = {
190
        'Project-Id-Version': 'Koha',
191
        'Content-Type': 'text/plain; charset=UTF-8',
192
    };
193
194
    return merge(perlStream, ttStream)
195
        .pipe(concatPo('Koha-messages.pot', { headers }))
196
        .pipe(gulp.dest('misc/translator'))
197
});
198
199
gulp.task('po:extract:pref', function () {
200
    return gulp.src('koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/*.pref', { read: false })
201
        .pipe(xgettext('misc/translator/xgettext-pref', 'Koha-pref.pot'))
202
        .pipe(gulp.dest('misc/translator'))
203
});
204
205
gulp.task('po:extract:installer', function () {
206
    const globs = [,
207
        'installer/data/mysql/en/mandatory/*.yml',
208
        'installer/data/mysql/en/optional/*.yml',
209
    ];
210
211
    return gulp.src(globs, { read: false, nocase: true })
212
        .pipe(xgettext('misc/translator/xgettext-installer', 'Koha-installer.pot'))
213
        .pipe(gulp.dest('misc/translator'))
214
});
215
216
gulp.task('po:extract:installer-MARC21', po_extract_installer_marc('MARC21'))
217
gulp.task('po:extract:installer-UNIMARC', po_extract_installer_marc('UNIMARC'))
218
219
function po_extract_installer_marc (type) {
220
    const globs = `installer/data/mysql/en/marcflavour/${type}/**/*.yml`;
221
222
    return () => gulp.src(globs, { read: false, nocase: true })
223
        .pipe(xgettext('misc/translator/xgettext-installer', `Koha-installer-${type}.pot`))
224
        .pipe(gulp.dest('misc/translator'))
225
}
226
227
/**
228
 * Gulp plugin that executes xgettext-like command `cmd` on all files given as
229
 * input, and then outputs the result as a POT file named `filename`.
230
 * `cmd` should accept -o and -f options
231
 */
232
function xgettext (cmd, filename) {
233
    const filenames = [];
234
235
    function transform (file, encoding, callback) {
236
        filenames.push(path.relative(file.cwd, file.path));
237
        callback();
238
    }
239
240
    function flush (callback) {
241
        fs.mkdtemp(path.join(os.tmpdir(), 'koha-'), (err, folder) => {
242
            const outputFilename = path.join(folder, filename);
243
            const filesFilename = path.join(folder, 'files');
244
            fs.writeFile(filesFilename, filenames.join(os.EOL), err => {
245
                if (err) return callback(err);
246
247
                const command = `${cmd} -o ${outputFilename} -f ${filesFilename}`;
248
                child_process.exec(command, err => {
249
                    if (err) return callback(err);
250
251
                    fs.readFile(outputFilename, (err, data) => {
252
                        if (err) return callback(err);
253
254
                        const file = new Vinyl();
255
                        file.path = path.join(file.base, filename);
256
                        file.contents = data;
257
                        callback(null, file);
258
                    });
259
                });
260
            });
261
        })
262
    }
263
264
    return through2.obj(transform, flush);
265
}
266
267
/**
268
 * Return languages selected for PO-related tasks
269
 *
270
 * This can be either languages given on command-line with --lang option, or
271
 * all the languages found in misc/translator/po otherwise
272
 */
273
function getLanguages () {
274
    if (Array.isArray(options.lang)) {
275
        return options.lang;
276
    }
277
278
    if (options.lang) {
279
        return [options.lang];
280
    }
281
282
    const filenames = fs.readdirSync('misc/translator/po')
283
        .filter(filename => filename.endsWith('.po'))
284
        .filter(filename => !filename.startsWith('.'))
285
286
    const re = new RegExp('-(' + poTypes.join('|') + ')\.po$');
287
    languages = filenames.map(filename => filename.replace(re, ''))
288
289
    return Array.from(new Set(languages));
290
}
(-)a/misc/translator/LangInstaller.pm (-692 / +89 lines)
Lines 22-57 use Modern::Perl; Link Here
22
use C4::Context;
22
use C4::Context;
23
# WARNING: Any other tested YAML library fails to work properly in this
23
# WARNING: Any other tested YAML library fails to work properly in this
24
# script content
24
# script content
25
use YAML::Syck qw( Dump LoadFile DumpFile );
25
use YAML::Syck qw( LoadFile DumpFile );
26
use Locale::PO;
26
use Locale::PO;
27
use FindBin qw( $Bin );
27
use FindBin qw( $Bin );
28
use File::Basename;
28
use File::Basename;
29
use File::Find;
30
use File::Path qw( make_path );
29
use File::Path qw( make_path );
31
use File::Copy;
30
use File::Copy;
32
use File::Slurp;
33
use File::Spec;
34
use File::Temp qw( tempdir tempfile );
35
use Template::Parser;
36
use PPI;
37
38
31
39
$YAML::Syck::ImplicitTyping = 1;
32
$YAML::Syck::ImplicitTyping = 1;
40
33
41
42
# Default file header for .po syspref files
43
my $default_pref_po_header = Locale::PO->new(-msgid => '', -msgstr =>
44
    "Project-Id-Version: PACKAGE VERSION\\n" .
45
    "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" .
46
    "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" .
47
    "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\\n" .
48
    "MIME-Version: 1.0\\n" .
49
    "Content-Type: text/plain; charset=UTF-8\\n" .
50
    "Content-Transfer-Encoding: 8bit\\n" .
51
    "Plural-Forms: nplurals=2; plural=(n > 1);\\n"
52
);
53
54
55
sub set_lang {
34
sub set_lang {
56
    my ($self, $lang) = @_;
35
    my ($self, $lang) = @_;
57
36
Lines 60-66 sub set_lang { Link Here
60
                            "/prog/$lang/modules/admin/preferences";
39
                            "/prog/$lang/modules/admin/preferences";
61
}
40
}
62
41
63
64
sub new {
42
sub new {
65
    my ($class, $lang, $pref_only, $verbose) = @_;
43
    my ($class, $lang, $pref_only, $verbose) = @_;
66
44
Lines 75-106 sub new { Link Here
75
    $self->{verbose}         = $verbose;
53
    $self->{verbose}         = $verbose;
76
    $self->{process}         = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
54
    $self->{process}         = "$Bin/tmpl_process3.pl " . ($verbose ? '' : '-q');
77
    $self->{path_po}         = "$Bin/po";
55
    $self->{path_po}         = "$Bin/po";
78
    $self->{po}              = { '' => $default_pref_po_header };
56
    $self->{po}              = {};
79
    $self->{domain}          = 'Koha';
57
    $self->{domain}          = 'Koha';
80
    $self->{cp}              = `which cp`;
81
    $self->{msgmerge}        = `which msgmerge`;
82
    $self->{msgfmt}          = `which msgfmt`;
58
    $self->{msgfmt}          = `which msgfmt`;
83
    $self->{msginit}         = `which msginit`;
84
    $self->{msgattrib}       = `which msgattrib`;
85
    $self->{xgettext}        = `which xgettext`;
86
    $self->{sed}             = `which sed`;
87
    $self->{po2json}         = "$Bin/po2json";
59
    $self->{po2json}         = "$Bin/po2json";
88
    $self->{gzip}            = `which gzip`;
60
    $self->{gzip}            = `which gzip`;
89
    $self->{gunzip}          = `which gunzip`;
61
    $self->{gunzip}          = `which gunzip`;
90
    chomp $self->{cp};
91
    chomp $self->{msgmerge};
92
    chomp $self->{msgfmt};
62
    chomp $self->{msgfmt};
93
    chomp $self->{msginit};
94
    chomp $self->{msgattrib};
95
    chomp $self->{xgettext};
96
    chomp $self->{sed};
97
    chomp $self->{gzip};
63
    chomp $self->{gzip};
98
    chomp $self->{gunzip};
64
    chomp $self->{gunzip};
99
65
100
    unless ($self->{xgettext}) {
101
        die "Missing 'xgettext' executable. Have you installed the gettext package?\n";
102
    }
103
104
    # Get all .pref file names
66
    # Get all .pref file names
105
    opendir my $fh, $self->{path_pref_en};
67
    opendir my $fh, $self->{path_pref_en};
106
    my @pref_files = grep { /\.pref$/ } readdir($fh);
68
    my @pref_files = grep { /\.pref$/ } readdir($fh);
Lines 175-181 sub new { Link Here
175
    bless $self, $class;
137
    bless $self, $class;
176
}
138
}
177
139
178
179
sub po_filename {
140
sub po_filename {
180
    my $self   = shift;
141
    my $self   = shift;
181
    my $suffix = shift;
142
    my $suffix = shift;
Lines 186-347 sub po_filename { Link Here
186
    return $trans_file;
147
    return $trans_file;
187
}
148
}
188
149
150
sub get_trans_text {
151
    my ($self, $msgctxt, $msgid) = @_;
189
152
190
sub po_append {
153
    my $key = ($msgctxt // '') . ";$msgid";
191
    my ($self, $id, $comment) = @_;
192
    my $po = $self->{po};
193
    my $p = $po->{$id};
194
    if ( $p ) {
195
        $p->comment( $p->comment . "\n" . $comment );
196
    }
197
    else {
198
        $po->{$id} = Locale::PO->new(
199
            -comment => $comment,
200
            -msgid   => $id,
201
            -msgstr  => ''
202
        );
203
    }
204
}
205
206
154
207
sub add_prefs {
155
    my $po = $self->{po}->{$key};
208
    my ($self, $comment, $prefs) = @_;
156
    if ($po) {
157
        my $msgstr = Locale::PO->dequote($po->msgstr);
209
158
210
    for my $pref ( @$prefs ) {
159
        return $msgstr || $msgid;
211
        my $pref_name = '';
212
        for my $element ( @$pref ) {
213
            if ( ref( $element) eq 'HASH' ) {
214
                $pref_name = $element->{pref};
215
                last;
216
            }
217
        }
218
        for my $element ( @$pref ) {
219
            if ( ref( $element) eq 'HASH' ) {
220
                while ( my ($key, $value) = each(%$element) ) {
221
                    next unless $key eq 'choices' or $key eq 'multiple';
222
                    next unless ref($value) eq 'HASH';
223
                    for my $ckey ( keys %$value ) {
224
                        my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
225
                        $self->po_append( $id, $comment );
226
                    }
227
                }
228
            }
229
            elsif ( $element ) {
230
                $self->po_append( $self->{file} . "#$pref_name# $element", $comment );
231
            }
232
        }
233
    }
160
    }
234
}
235
161
236
162
    return $msgid;
237
sub get_trans_text {
238
    my ($self, $id) = @_;
239
240
    my $po = $self->{po}->{$id};
241
    return unless $po;
242
    return Locale::PO->dequote($po->msgstr);
243
}
163
}
244
164
165
sub get_translated_tab_content {
166
    my ($self, $tab, $tab_content) = @_;
245
167
246
sub update_tab_prefs {
168
    if ( ref($tab_content) eq 'ARRAY' ) {
247
    my ($self, $pref, $prefs) = @_;
169
        return $self->get_translated_prefs($tab, $tab_content);
248
249
    for my $p ( @$prefs ) {
250
        my $pref_name = '';
251
        next unless $p;
252
        for my $element ( @$p ) {
253
            if ( ref( $element) eq 'HASH' ) {
254
                $pref_name = $element->{pref};
255
                last;
256
            }
257
        }
258
        for my $i ( 0..@$p-1 ) {
259
            my $element = $p->[$i];
260
            if ( ref( $element) eq 'HASH' ) {
261
                while ( my ($key, $value) = each(%$element) ) {
262
                    next unless $key eq 'choices' or $key eq 'multiple';
263
                    next unless ref($value) eq 'HASH';
264
                    for my $ckey ( keys %$value ) {
265
                        my $id = $self->{file} . "#$pref_name# " . $value->{$ckey};
266
                        my $text = $self->get_trans_text( $id );
267
                        $value->{$ckey} = $text if $text;
268
                    }
269
                }
270
            }
271
            elsif ( $element ) {
272
                my $id = $self->{file} . "#$pref_name# $element";
273
                my $text = $self->get_trans_text( $id );
274
                $p->[$i] = $text if $text;
275
            }
276
        }
277
    }
170
    }
278
}
279
171
172
    my $translated_tab_content = {
173
        map {
174
            my $section = $_;
175
            my $sysprefs = $tab_content->{$section};
176
            my $context = "$tab > $section";
280
177
281
sub get_po_from_prefs {
178
            $self->get_trans_text($tab, $section) => $self->get_translated_prefs($context, $sysprefs);
282
    my $self = shift;
179
        } keys %$tab_content
180
    };
283
181
284
    for my $file ( @{$self->{pref_files}} ) {
182
    return $translated_tab_content;
285
        my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
286
        $self->{file} = $file;
287
        # Entries for tab titles
288
        $self->po_append( $self->{file}, $_ ) for keys %$pref;
289
        while ( my ($tab, $tab_content) = each %$pref ) {
290
            if ( ref($tab_content) eq 'ARRAY' ) {
291
                $self->add_prefs( $tab, $tab_content );
292
                next;
293
            }
294
            while ( my ($section, $sysprefs) = each %$tab_content ) {
295
                my $comment = "$tab > $section";
296
                $self->po_append( $self->{file} . " " . $section, $comment );
297
                $self->add_prefs( $comment, $sysprefs );
298
            }
299
        }
300
    }
301
}
183
}
302
184
185
sub get_translated_prefs {
186
    my ($self, $context, $sysprefs) = @_;
303
187
304
sub save_po {
188
    my $translated_prefs = [
305
    my $self = shift;
189
        map {
190
            my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
191
            my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
306
192
307
    # Create file header if it doesn't already exist
193
            my $translated_syspref = [
308
    my $po = $self->{po};
194
                map {
309
    $po->{''} ||= $default_pref_po_header;
195
                    $self->get_translated_pref("$context > $pref_name", $_ );
196
                } @$_
197
            ];
310
198
311
    # Write .po entries into a file put in Koha standard po directory
199
            $translated_syspref;
312
    Locale::PO->save_file_fromhash( $self->po_filename("-pref.po"), $po );
200
        } @$sysprefs
313
    say "Saved in file: ", $self->po_filename("-pref.po") if $self->{verbose};
201
    ];
314
}
315
202
203
    return $translated_prefs;
204
}
316
205
317
sub get_po_merged_with_en {
206
sub get_translated_pref {
318
    my $self = shift;
207
    my ($self, $context, $syspref) = @_;
319
208
320
    # Get po from current 'en' .pref files
209
    unless (ref($syspref)) {
321
    $self->get_po_from_prefs();
210
        return $self->get_trans_text($context, $syspref // '');
322
    my $po_current = $self->{po};
211
    }
323
212
324
    # Get po from previous generation
213
    my $translated_pref = {
325
    my $po_previous = Locale::PO->load_file_ashash( $self->po_filename("-pref.po") );
214
        map {
215
            my $key = $_;
216
            my $value = $syspref->{$key};
326
217
327
    for my $id ( keys %$po_current ) {
218
            my $translated_value = $value;
328
        my $po =  $po_previous->{Locale::PO->quote($id)};
219
            if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
329
        next unless $po;
220
                $translated_value = {
330
        my $text = Locale::PO->dequote( $po->msgstr );
221
                    map {
331
        $po_current->{$id}->msgstr( $text );
222
                        $_ => $self->get_trans_text($context, $value->{$_})
332
    }
223
                    } keys %$value
333
}
224
                }
225
            }
334
226
227
            $key => $translated_value
228
        } keys %$syspref
229
    };
335
230
336
sub update_prefs {
231
    return $translated_pref;
337
    my $self = shift;
338
    print "Update '", $self->{lang},
339
          "' preferences .po file from 'en' .pref files\n" if $self->{verbose};
340
    $self->get_po_merged_with_en();
341
    $self->save_po();
342
}
232
}
343
233
344
345
sub install_prefs {
234
sub install_prefs {
346
    my $self = shift;
235
    my $self = shift;
347
236
Lines 350-394 sub install_prefs { Link Here
350
        exit;
239
        exit;
351
    }
240
    }
352
241
353
    # Get the language .po file merged with last modified 'en' preferences
242
    my @po_entries = @{ Locale::PO->load_file_asarray($self->po_filename("-pref.po"), 'utf8') };
354
    $self->get_po_merged_with_en();
243
    $self->{po} = { map {
244
        my $msgctxt = $_->msgctxt ? Locale::PO->dequote($_->msgctxt) : '';
245
        my $msgid = Locale::PO->dequote($_->msgid);
246
247
        "$msgctxt;$msgid" => $_;
248
    } @po_entries };
355
249
356
    for my $file ( @{$self->{pref_files}} ) {
250
    for my $file ( @{$self->{pref_files}} ) {
357
        my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
251
        my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
358
        $self->{file} = $file;
252
359
        # First, keys are replaced (tab titles)
253
        my $translated_pref = {
360
        $pref = do {
254
            map {
361
            my %pref = map { 
255
                my $tab = $_;
362
                $self->get_trans_text( $self->{file} ) || $_ => $pref->{$_}
256
                my $tab_content = $pref->{$tab};
363
            } keys %$pref;
257
364
            \%pref;
258
                $self->get_trans_text(undef, $tab) => $self->get_translated_tab_content($tab, $tab_content);
259
            } keys %$pref
365
        };
260
        };
366
        while ( my ($tab, $tab_content) = each %$pref ) {
261
367
            if ( ref($tab_content) eq 'ARRAY' ) {
262
368
                $self->update_tab_prefs( $pref, $tab_content );
369
                next;
370
            }
371
            while ( my ($section, $sysprefs) = each %$tab_content ) {
372
                $self->update_tab_prefs( $pref, $sysprefs );
373
            }
374
            my $ntab = {};
375
            for my $section ( keys %$tab_content ) {
376
                my $id = $self->{file} . " $section";
377
                my $text = $self->get_trans_text($id);
378
                my $nsection = $text ? $text : $section;
379
                if( exists $ntab->{$nsection} ) {
380
                    # When translations collide (see BZ 18634)
381
                    push @{$ntab->{$nsection}}, @{$tab_content->{$section}};
382
                } else {
383
                    $ntab->{$nsection} = $tab_content->{$section};
384
                }
385
            }
386
            $pref->{$tab} = $ntab;
387
        }
388
        my $file_trans = $self->{po_path_lang} . "/$file";
263
        my $file_trans = $self->{po_path_lang} . "/$file";
389
        print "Write $file\n" if $self->{verbose};
264
        print "Write $file\n" if $self->{verbose};
390
        open my $fh, ">", $file_trans;
265
        DumpFile($file_trans, $translated_pref);
391
        print $fh Dump($pref);
392
    }
266
    }
393
}
267
}
394
268
Lines 429-608 sub install_tmpl { Link Here
429
    }
303
    }
430
}
304
}
431
305
432
433
sub update_tmpl {
434
    my ($self, $files) = @_;
435
436
    say "Update templates" if $self->{verbose};
437
    for my $trans ( @{$self->{interface}} ) {
438
        my @files   = @$files;
439
        my @nomarc = ();
440
        print
441
            "  Update templates '$trans->{name}'\n",
442
            "    From: $trans->{dir}/en/\n",
443
            "    To  : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
444
                if $self->{verbose};
445
446
        my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
447
        # if processing MARC po file, only use corresponding files
448
        my $marc      = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
449
        # if not processing MARC po file, ignore all MARC files
450
        @nomarc       = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ );      # hardcoded MARC variants
451
452
        system
453
            "$self->{process} update " .
454
            "-i $trans_dir " .
455
            "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
456
            "$marc "     .
457
            ( @files   ? ' -f ' . join ' -f ', @files : '') .
458
            ( @nomarc  ? ' -n ' . join ' -n ', @nomarc : '');
459
    }
460
}
461
462
463
sub create_prefs {
464
    my $self = shift;
465
466
    if ( -e $self->po_filename("-pref.po") ) {
467
        say "Preferences .po file already exists. Delete it if you want to recreate it.";
468
        return;
469
    }
470
    $self->get_po_from_prefs();
471
    $self->save_po();
472
}
473
474
sub get_po_from_target {
475
    my $self   = shift;
476
    my $target = shift;
477
478
    my $po;
479
    my $po_head = new Locale::PO;
480
    $po_head->{msgid}  = "\"\"";
481
    $po_head->{msgstr} = "".
482
        "Project-Id-Version: Koha Project - Installation files\\n" .
483
        "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\\n" .
484
        "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n" .
485
        "Language-Team: Koha Translation Team\\n" .
486
        "Language: ".$self->{lang}."\\n" .
487
        "MIME-Version: 1.0\\n" .
488
        "Content-Type: text/plain; charset=UTF-8\\n" .
489
        "Content-Transfer-Encoding: 8bit\\n";
490
491
    my @dirs = @{ $target->{dirs} };
492
    my $intradir = $self->{context}->config('intranetdir');
493
    for my $dir ( @dirs ) {                                                     # each dir
494
        opendir( my $dh, "$intradir/$dir" ) or die ("Can't open $intradir/$dir");
495
        my @filelist = grep { $_ =~ m/\.yml/ } readdir($dh);                    # Just yaml files
496
        close($dh);
497
        for my $file ( @filelist ) {                                            # each file
498
            my $yaml   = LoadFile( "$intradir/$dir/$file" );
499
            my @tables = @{ $yaml->{'tables'} };
500
            my $tablec;
501
            for my $table ( @tables ) {                                         # each table
502
                $tablec++;
503
                my $table_name = ( keys %$table )[0];
504
                my @translatable = @{ $table->{$table_name}->{translatable} };
505
                my @rows = @{ $table->{$table_name}->{rows} };
506
                my @multiline = @{ $table->{$table_name}->{'multiline'} };      # to check multiline values
507
                my $rowc;
508
                for my $row ( @rows ) {                                         # each row
509
                    $rowc++;
510
                    for my $field ( @translatable ) {                           # each field
511
                        if ( @multiline and grep { $_ eq $field } @multiline ) {    # multiline fields, only notices ATM
512
                            my $mulc;
513
                            foreach my $line ( @{$row->{$field}} ) {
514
                                $mulc++;
515
                                next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ );                     # discard pure html, TT, empty
516
                                $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/g;                                   # put placeholders
517
                                next if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ or length($line) < 2 );     # discard non strings
518
                                if ( not $po->{ $line } ) {
519
                                    my $msg = new Locale::PO(
520
                                                -msgid => $line, -msgstr => '',
521
                                                -reference => "$dir/$file:$table_name:$tablec:row:$rowc:mul:$mulc" );
522
                                    $po->{ $line } = $msg;
523
                                }
524
                            }
525
                        } else {
526
                            if ( defined $row->{$field} and length($row->{$field}) > 1                         # discard null values and small strings
527
                                 and not $po->{ $row->{$field} } ) {
528
                                my $msg = new Locale::PO(
529
                                            -msgid => $row->{$field}, -msgstr => '',
530
                                            -reference => "$dir/$file:$table_name:$tablec:row:$rowc" );
531
                                $po->{ $row->{$field} } = $msg;
532
                            }
533
                        }
534
                    }
535
                }
536
            }
537
            my $desccount;
538
            for my $description ( @{ $yaml->{'description'} } ) {
539
                $desccount++;
540
                if ( length($description) > 1 and not $po->{ $description } ) {
541
                    my $msg = new Locale::PO(
542
                                -msgid => $description, -msgstr => '',
543
                                -reference => "$dir/$file:description:$desccount" );
544
                    $po->{ $description } = $msg;
545
                }
546
            }
547
        }
548
    }
549
    $po->{''} = $po_head if ( $po );
550
551
    return $po;
552
}
553
554
sub create_installer {
555
    my $self = shift;
556
    return unless ( $self->{installer} );
557
558
    say "Create installer translation files\n" if $self->{verbose};
559
560
    my @targets = @{ $self->{installer} };             # each installer target (common,marc21,unimarc)
561
562
    for my $target ( @targets ) {
563
        if ( -e $self->po_filename( $target->{suffix} ) ) {
564
            say "$self->{lang}$target->{suffix} file already exists. Delete it if you want to recreate it.";
565
            return;
566
        }
567
    }
568
569
    for my $target ( @targets ) {
570
        my $po = get_po_from_target( $self, $target );
571
        # create output file only if there is something to write
572
        if ( $po ) {
573
            my $po_file = $self->po_filename( $target->{suffix} );
574
            Locale::PO->save_file_fromhash( $po_file, $po );
575
            say "Saved in file: ", $po_file if $self->{verbose};
576
        }
577
    }
578
}
579
580
sub update_installer {
581
    my $self = shift;
582
    return unless ( $self->{installer} );
583
584
    say "Update installer translation files\n" if $self->{verbose};
585
586
    my @targets = @{ $self->{installer} };             # each installer target (common,marc21,unimarc)
587
588
    for my $target ( @targets ) {
589
        return unless ( -e $self->po_filename( $target->{suffix} ) );
590
        my $po = get_po_from_target( $self, $target );
591
        # update file only if there is something to update
592
        if ( $po ) {
593
            my ( $fh, $po_temp ) = tempfile();
594
            binmode( $fh, ":encoding(UTF-8)" );
595
            Locale::PO->save_file_fromhash( $po_temp, $po );
596
            my $po_file = $self->po_filename( $target->{suffix} );
597
            eval {
598
                my $st = system($self->{msgmerge}." ".($self->{verbose}?'':'-q').
599
                         " -s $po_file $po_temp -o - | ".$self->{msgattrib}." --no-obsolete -o $po_file");
600
            };
601
            say "Updated file: ", $po_file if $self->{verbose};
602
        }
603
    }
604
}
605
606
sub translate_yaml {
306
sub translate_yaml {
607
    my $self   = shift;
307
    my $self   = shift;
608
    my $target = shift;
308
    my $target = shift;
Lines 716-750 sub install_installer { Link Here
716
    }
416
    }
717
}
417
}
718
418
719
sub create_tmpl {
720
    my ($self, $files) = @_;
721
722
    say "Create templates\n" if $self->{verbose};
723
    for my $trans ( @{$self->{interface}} ) {
724
        my @files   = @$files;
725
        my @nomarc = ();
726
        print
727
            "  Create templates .po files for '$trans->{name}'\n",
728
            "    From: $trans->{dir}/en/\n",
729
            "    To  : $self->{path_po}/$self->{lang}$trans->{suffix}\n"
730
                if $self->{verbose};
731
732
        my $trans_dir = join("/en/ -i ",split(" ",$trans->{dir}))."/en/"; # multiple source dirs
733
        # if processing MARC po file, only use corresponding files
734
        my $marc      = ( $trans->{name} =~ /MARC/ )?"-m \"$trans->{name}\"":"";            # for MARC translations
735
        # if not processing MARC po file, ignore all MARC files
736
        @nomarc       = ( 'marc21', 'unimarc', 'normarc' ) if ( $trans->{name} !~ /MARC/ ); # hardcoded MARC variants
737
738
        system
739
            "$self->{process} create " .
740
            "-i $trans_dir " .
741
            "-s $self->{path_po}/$self->{lang}$trans->{suffix} -r " .
742
            "$marc " .
743
            ( @files  ? ' -f ' . join ' -f ', @files   : '') .
744
            ( @nomarc ? ' -n ' . join ' -n ', @nomarc : '');
745
    }
746
}
747
748
sub locale_name {
419
sub locale_name {
749
    my $self = shift;
420
    my $self = shift;
750
421
Lines 758-1007 sub locale_name { Link Here
758
    return $locale;
429
    return $locale;
759
}
430
}
760
431
761
sub create_messages {
762
    my $self = shift;
763
764
    my $pot = "$Bin/$self->{domain}.pot";
765
    my $po = "$self->{path_po}/$self->{lang}-messages.po";
766
    my $js_pot = "$self->{domain}-js.pot";
767
    my $js_po = "$self->{path_po}/$self->{lang}-messages-js.po";
768
769
    unless ( -f $pot && -f $js_pot ) {
770
        $self->extract_messages();
771
    }
772
773
    say "Create messages ($self->{lang})" if $self->{verbose};
774
    my $locale = $self->locale_name();
775
    system "$self->{msginit} -i $pot -o $po -l $locale --no-translator 2> /dev/null";
776
    warn "Problems creating $pot ".$? if ( $? == -1 );
777
    system "$self->{msginit} -i $js_pot -o $js_po -l $locale --no-translator 2> /dev/null";
778
    warn "Problems creating $js_pot ".$? if ( $? == -1 );
779
780
    # If msginit failed to correctly set Plural-Forms, set a default one
781
    system "$self->{sed} --in-place "
782
        . "--expression='s/Plural-Forms: nplurals=INTEGER; plural=EXPRESSION/Plural-Forms: nplurals=2; plural=(n != 1)/' "
783
        . "$po $js_po";
784
}
785
786
sub update_messages {
787
    my $self = shift;
788
789
    my $pot = "$Bin/$self->{domain}.pot";
790
    my $po = "$self->{path_po}/$self->{lang}-messages.po";
791
    my $js_pot = "$self->{domain}-js.pot";
792
    my $js_po = "$self->{path_po}/$self->{lang}-messages-js.po";
793
794
    unless ( -f $pot && -f $js_pot ) {
795
        $self->extract_messages();
796
    }
797
798
    if ( -f $po && -f $js_pot ) {
799
        say "Update messages ($self->{lang})" if $self->{verbose};
800
        system "$self->{msgmerge} --backup=off --quiet -U $po $pot";
801
        system "$self->{msgmerge} --backup=off --quiet -U $js_po $js_pot";
802
    } else {
803
        $self->create_messages();
804
    }
805
}
806
807
sub extract_messages_from_templates {
808
    my ($self, $tempdir, $type, @files) = @_;
809
810
    my $htdocs = $type eq 'intranet' ? 'intrahtdocs' : 'opachtdocs';
811
    my $dir = $self->{context}->config($htdocs);
812
    my @keywords = qw(t tx tn txn tnx tp tpx tnp tnpx);
813
    my $parser = Template::Parser->new();
814
815
    foreach my $file (@files) {
816
        say "Extract messages from $file" if $self->{verbose};
817
        my $template = read_file(File::Spec->catfile($dir, $file));
818
819
        # No need to process a file that doesn't use the i18n.inc file.
820
        next unless $template =~ /i18n\.inc/;
821
822
        my $data = $parser->parse($template);
823
        unless ($data) {
824
            warn "Error at $file : " . $parser->error();
825
            next;
826
        }
827
828
        my $destfile = $type eq 'intranet' ?
829
            File::Spec->catfile($tempdir, 'koha-tmpl', 'intranet-tmpl', $file) :
830
            File::Spec->catfile($tempdir, 'koha-tmpl', 'opac-tmpl', $file);
831
832
        make_path(dirname($destfile));
833
        open my $fh, '>', $destfile;
834
835
        my @blocks = ($data->{BLOCK}, values %{ $data->{DEFBLOCKS} });
836
        foreach my $block (@blocks) {
837
            my $document = PPI::Document->new(\$block);
838
839
            # [% t('foo') %] is compiled to
840
            # $output .= $stash->get(['t', ['foo']]);
841
            # We try to find all nodes corresponding to keyword (here 't')
842
            my $nodes = $document->find(sub {
843
                my ($topnode, $element) = @_;
844
845
                # Filter out non-valid keywords
846
                return 0 unless ($element->isa('PPI::Token::Quote::Single'));
847
                return 0 unless (grep {$element->content eq qq{'$_'}} @keywords);
848
849
                # keyword (e.g. 't') should be the first element of the arrayref
850
                # passed to $stash->get()
851
                return 0 if $element->sprevious_sibling;
852
853
                return 0 unless $element->snext_sibling
854
                    && $element->snext_sibling->snext_sibling
855
                    && $element->snext_sibling->snext_sibling->isa('PPI::Structure::Constructor');
856
857
                # Check that it's indeed a call to $stash->get()
858
                my $statement = $element->statement->parent->statement->parent->statement;
859
                return 0 unless grep { $_->isa('PPI::Token::Symbol') && $_->content eq '$stash' } $statement->children;
860
                return 0 unless grep { $_->isa('PPI::Token::Operator') && $_->content eq '->' } $statement->children;
861
                return 0 unless grep { $_->isa('PPI::Token::Word') && $_->content eq 'get' } $statement->children;
862
863
                return 1;
864
            });
865
866
            next unless $nodes;
867
868
            # Write the Perl equivalent of calls to t* functions family, so
869
            # xgettext can extract the strings correctly
870
            foreach my $node (@$nodes) {
871
                my @args = map {
872
                    $_->significant && !$_->isa('PPI::Token::Operator') ? $_->content : ()
873
                } $node->snext_sibling->snext_sibling->find_first('PPI::Statement')->children;
874
875
                my $keyword = $node->content;
876
                $keyword =~ s/^'t(.*)'$/__$1/;
877
878
                # Only keep required args to have a clean output
879
                my @required_args = shift @args;
880
                push @required_args, shift @args if $keyword =~ /n/;
881
                push @required_args, shift @args if $keyword =~ /p/;
882
883
                say $fh "$keyword(" . join(', ', @required_args) . ");";
884
            }
885
886
        }
887
888
        close $fh;
889
    }
890
891
    return $tempdir;
892
}
893
894
sub extract_messages {
895
    my $self = shift;
896
897
    say "Extract messages into POT file" if $self->{verbose};
898
899
    my $intranetdir = $self->{context}->config('intranetdir');
900
    my $opacdir = $self->{context}->config('opacdir');
901
902
    # Find common ancestor directory
903
    my @intranetdirs = File::Spec->splitdir($intranetdir);
904
    my @opacdirs = File::Spec->splitdir($opacdir);
905
    my @basedirs;
906
    while (@intranetdirs and @opacdirs) {
907
        my ($dir1, $dir2) = (shift @intranetdirs, shift @opacdirs);
908
        last if $dir1 ne $dir2;
909
        push @basedirs, $dir1;
910
    }
911
    my $basedir = File::Spec->catdir(@basedirs);
912
913
    my @files_to_scan;
914
    my @directories_to_scan = ('.');
915
    my @blacklist = map { File::Spec->catdir(@intranetdirs, $_) } qw(blib koha-tmpl skel tmp t);
916
    while (@directories_to_scan) {
917
        my $dir = shift @directories_to_scan;
918
        opendir DIR, File::Spec->catdir($basedir, $dir) or die "Unable to open $dir: $!";
919
        foreach my $entry (readdir DIR) {
920
            next if $entry =~ /^\./;
921
            my $relentry = File::Spec->catfile($dir, $entry);
922
            my $abspath = File::Spec->catfile($basedir, $relentry);
923
            if (-d $abspath and not grep { $_ eq $relentry } @blacklist) {
924
                push @directories_to_scan, $relentry;
925
            } elsif (-f $abspath and $relentry =~ /\.(pl|pm)$/) {
926
                push @files_to_scan, $relentry;
927
            }
928
        }
929
    }
930
931
    my $intrahtdocs = $self->{context}->config('intrahtdocs');
932
    my $opachtdocs = $self->{context}->config('opachtdocs');
933
934
    my @intranet_tt_files;
935
    find(sub {
936
        if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) {
937
            my $filename = $File::Find::name;
938
            $filename =~ s|^$intrahtdocs/||;
939
            push @intranet_tt_files, $filename;
940
        }
941
    }, $intrahtdocs);
942
943
    my @opac_tt_files;
944
    find(sub {
945
        if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.(tt|inc)$/) {
946
            my $filename = $File::Find::name;
947
            $filename =~ s|^$opachtdocs/||;
948
            push @opac_tt_files, $filename;
949
        }
950
    }, $opachtdocs);
951
952
    my $tempdir = tempdir('Koha-translate-XXXX', TMPDIR => 1, CLEANUP => 1);
953
    $self->extract_messages_from_templates($tempdir, 'intranet', @intranet_tt_files);
954
    $self->extract_messages_from_templates($tempdir, 'opac', @opac_tt_files);
955
956
    @intranet_tt_files = map { File::Spec->catfile('koha-tmpl', 'intranet-tmpl', $_) } @intranet_tt_files;
957
    @opac_tt_files = map { File::Spec->catfile('koha-tmpl', 'opac-tmpl', $_) } @opac_tt_files;
958
    my @tt_files = grep { -e File::Spec->catfile($tempdir, $_) } @intranet_tt_files, @opac_tt_files;
959
960
    push @files_to_scan, @tt_files;
961
962
    my $xgettext_common_args = "--force-po --from-code=UTF-8 "
963
        . "--package-name=Koha --package-version='' "
964
        . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 "
965
        . "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ -kN__n:1,2 "
966
        . "-kN__p:1c,2 -kN__np:1c,2,3 ";
967
    my $xgettext_cmd = "$self->{xgettext} -L Perl $xgettext_common_args "
968
        . "-o $Bin/$self->{domain}.pot -D $tempdir -D $basedir";
969
    $xgettext_cmd .= " $_" foreach (@files_to_scan);
970
971
    if (system($xgettext_cmd) != 0) {
972
        die "system call failed: $xgettext_cmd";
973
    }
974
975
    my @js_dirs = (
976
        "$intrahtdocs/prog/js",
977
        "$opachtdocs/bootstrap/js",
978
    );
979
980
    my @js_files;
981
    find(sub {
982
        if ($_ =~ m/\.js$/) {
983
            my $filename = $File::Find::name;
984
            $filename =~ s|^$intranetdir/||;
985
            push @js_files, $filename;
986
        }
987
    }, @js_dirs);
988
989
    $xgettext_cmd = "$self->{xgettext} -L JavaScript $xgettext_common_args "
990
        . "-o $Bin/$self->{domain}-js.pot -D $intranetdir";
991
    $xgettext_cmd .= " $_" foreach (@js_files);
992
993
    if (system($xgettext_cmd) != 0) {
994
        die "system call failed: $xgettext_cmd";
995
    }
996
997
    my $replace_charset_cmd = "$self->{sed} --in-place " .
998
        "--expression='s/charset=CHARSET/charset=UTF-8/' " .
999
        "$Bin/$self->{domain}.pot $Bin/$self->{domain}-js.pot";
1000
    if (system($replace_charset_cmd) != 0) {
1001
        die "system call failed: $replace_charset_cmd";
1002
    }
1003
}
1004
1005
sub install_messages {
432
sub install_messages {
1006
    my ($self) = @_;
433
    my ($self) = @_;
1007
434
Lines 1012-1019 sub install_messages { Link Here
1012
    my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
439
    my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
1013
440
1014
    unless ( -f $pofile && -f $js_pofile ) {
441
    unless ( -f $pofile && -f $js_pofile ) {
1015
        $self->create_messages();
442
        die "PO files for language '$self->{lang}' do not exist";
1016
    }
443
    }
444
1017
    say "Install messages ($locale)" if $self->{verbose};
445
    say "Install messages ($locale)" if $self->{verbose};
1018
    make_path($modir);
446
    make_path($modir);
1019
    system "$self->{msgfmt} -o $mofile $pofile";
447
    system "$self->{msgfmt} -o $mofile $pofile";
Lines 1035-1047 sub install_messages { Link Here
1035
    }
463
    }
1036
}
464
}
1037
465
1038
sub remove_pot {
1039
    my $self = shift;
1040
1041
    unlink "$Bin/$self->{domain}.pot";
1042
    unlink "$Bin/$self->{domain}-js.pot";
1043
}
1044
1045
sub compress {
466
sub compress {
1046
    my ($self, $files) = @_;
467
    my ($self, $files) = @_;
1047
    my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
468
    my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
Lines 1074-1084 sub install { Link Here
1074
    my ($self, $files) = @_;
495
    my ($self, $files) = @_;
1075
    return unless $self->{lang};
496
    return unless $self->{lang};
1076
    $self->uncompress();
497
    $self->uncompress();
1077
    $self->install_tmpl($files) unless $self->{pref_only};
498
1078
    $self->install_prefs();
499
    if ($self->{pref_only}) {
1079
    $self->install_messages();
500
        $self->install_prefs();
1080
    $self->remove_pot();
501
    } else {
1081
    $self->install_installer();
502
        $self->install_tmpl($files);
503
        $self->install_prefs();
504
        $self->install_messages();
505
        $self->install_installer();
506
    }
1082
}
507
}
1083
508
1084
509
Lines 1090-1123 sub get_all_langs { Link Here
1090
    @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
515
    @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
1091
}
516
}
1092
517
1093
1094
sub update {
1095
    my ($self, $files) = @_;
1096
    my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
1097
    for my $lang ( @langs ) {
1098
        $self->set_lang( $lang );
1099
        $self->uncompress();
1100
        $self->update_tmpl($files) unless $self->{pref_only};
1101
        $self->update_prefs();
1102
        $self->update_messages();
1103
        $self->update_installer();
1104
    }
1105
    $self->remove_pot();
1106
}
1107
1108
1109
sub create {
1110
    my ($self, $files) = @_;
1111
    return unless $self->{lang};
1112
    $self->create_tmpl($files) unless $self->{pref_only};
1113
    $self->create_prefs();
1114
    $self->create_messages();
1115
    $self->remove_pot();
1116
    $self->create_installer();
1117
}
1118
1119
1120
1121
1;
518
1;
1122
519
1123
520
(-)a/misc/translator/po/dz-pref.po (-1 / +9 lines)
Lines 1-5 Link Here
1
msgid ""
1
msgid ""
2
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"
2
msgstr ""
3
"Project-Id-Version: PACKAGE VERSION\n"
4
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
5
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
6
"Language-Team: Koha Translate List <koha-translate@lists.koha-community.org>\n"
7
"MIME-Version: 1.0\n"
8
"Content-Type: text/plain; charset=UTF-8\n"
9
"Content-Transfer-Encoding: 8bit\n"
10
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
3
11
4
# Accounting
12
# Accounting
5
msgid "accounting.pref"
13
msgid "accounting.pref"
(-)a/misc/translator/po/gd-pref.po (-1 / +9 lines)
Lines 1-5 Link Here
1
msgid ""
1
msgid ""
2
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"
2
msgstr ""
3
"Project-Id-Version: PACKAGE VERSION\n"
4
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
5
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
6
"Language-Team: Koha Translate List <koha-translate@lists.koha-community.org>\n"
7
"MIME-Version: 1.0\n"
8
"Content-Type: text/plain; charset=UTF-8\n"
9
"Content-Transfer-Encoding: 8bit\n"
10
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
3
11
4
# Accounting
12
# Accounting
5
msgid "accounting.pref"
13
msgid "accounting.pref"
(-)a/misc/translator/po/lv-pref.po (-1 / +9 lines)
Lines 1-5 Link Here
1
msgid ""
1
msgid ""
2
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"
2
msgstr ""
3
"Project-Id-Version: PACKAGE VERSION\n"
4
"PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
5
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
6
"Language-Team: Koha Translate List <koha-translate@lists.koha-community.org>\n"
7
"MIME-Version: 1.0\n"
8
"Content-Type: text/plain; charset=UTF-8\n"
9
"Content-Transfer-Encoding: 8bit\n"
10
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
3
11
4
# Accounting
12
# Accounting
5
msgid "accounting.pref"
13
msgid "accounting.pref"
(-)a/misc/translator/tmpl_process3.pl (-119 / +3 lines)
Lines 204-214 sub usage { Link Here
204
    my($exitcode) = @_;
204
    my($exitcode) = @_;
205
    my $h = $exitcode? *STDERR: *STDOUT;
205
    my $h = $exitcode? *STDERR: *STDOUT;
206
    print $h <<EOF;
206
    print $h <<EOF;
207
Usage: $0 create [OPTION]
207
Usage: $0 install [OPTION]
208
  or:  $0 update [OPTION]
209
  or:  $0 install [OPTION]
210
  or:  $0 --help
208
  or:  $0 --help
211
Create or update PO files from templates, or install translated templates.
209
Install translated templates.
212
210
213
  -i, --input=SOURCE          Get or update strings from SOURCE directory(s).
211
  -i, --input=SOURCE          Get or update strings from SOURCE directory(s).
214
                              On create or update can have multiple values.
212
                              On create or update can have multiple values.
Lines 230-236 Create or update PO files from templates, or install translated templates. Link Here
230
      --help                  Display this help and exit
228
      --help                  Display this help and exit
231
  -q, --quiet                 no output to screen (except for errors)
229
  -q, --quiet                 no output to screen (except for errors)
232
230
233
The -o option is ignored for the "create" and "update" actions.
234
Try `perldoc $0` for perhaps more information.
231
Try `perldoc $0` for perhaps more information.
235
EOF
232
EOF
236
    exit($exitcode);
233
    exit($exitcode);
Lines 265-276 GetOptions( Link Here
265
VerboseWarnings::set_application_name($0);
262
VerboseWarnings::set_application_name($0);
266
VerboseWarnings::set_pedantic_mode($pedantic_p);
263
VerboseWarnings::set_pedantic_mode($pedantic_p);
267
264
268
# keep the buggy Locale::PO quiet if it says stupid things
269
$SIG{__WARN__} = sub {
270
    my($s) = @_;
271
    print STDERR $s unless $s =~ /^Strange line in [^:]+: #~/s
272
    };
273
274
my $action = shift or usage_error('You must specify an ACTION.');
265
my $action = shift or usage_error('You must specify an ACTION.');
275
usage_error('You must at least specify input and string list filenames.')
266
usage_error('You must at least specify input and string list filenames.')
276
    if !@in_dirs || !defined $str_file;
267
    if !@in_dirs || !defined $str_file;
Lines 344-432 if (!defined $charset_out) { Link Here
344
    $charset_out = TmplTokenizer::charset_canon('UTF-8');
335
    $charset_out = TmplTokenizer::charset_canon('UTF-8');
345
    warn "Warning: Charset Out defaulting to $charset_out\n" unless ( $quiet );
336
    warn "Warning: Charset Out defaulting to $charset_out\n" unless ( $quiet );
346
}
337
}
347
my $xgettext = './xgettext.pl'; # actual text extractor script
348
my $st;
338
my $st;
349
339
350
if ($action eq 'create')  {
340
if ($action eq 'install') {
351
    # updates the list. As the list is empty, every entry will be added
352
    if (!-s $str_file) {
353
    warn "Removing empty file $str_file\n" unless ( $quiet );
354
    unlink $str_file || die "$str_file: $!\n";
355
    }
356
    die "$str_file: Output file already exists\n" if -f $str_file;
357
    my($tmph1, $tmpfile1) = tmpnam();
358
    my($tmph2, $tmpfile2) = tmpnam();
359
    close $tmph2; # We just want a name
360
    # Generate the temporary file that acts as <MODULE>/POTFILES.in
361
    for my $input (@in_files) {
362
    print $tmph1 "$input\n";
363
    }
364
    close $tmph1;
365
    warn "I $charset_in O $charset_out" unless ( $quiet );
366
    # Generate the specified po file ($str_file)
367
    $st = system ($xgettext, '-s', '-f', $tmpfile1, '-o', $tmpfile2,
368
            (defined $charset_in? ('-I', $charset_in): ()),
369
            (defined $charset_out? ('-O', $charset_out): ())
370
    );
371
    # Run msgmerge so that the pot file looks like a real pot file
372
    # We need to help msgmerge a bit by pre-creating a dummy po file that has
373
    # the headers and the "" msgid & msgstr. It will fill in the rest.
374
    if ($st == 0) {
375
    # Merge the temporary "pot file" with the specified po file ($str_file)
376
    # FIXME: msgmerge(1) is a Unix dependency
377
    # FIXME: need to check the return value
378
    unless (-f $str_file) {
379
        open(my $infh, '<', $tmpfile2);
380
        open(my $outfh, '>', $str_file);
381
        while (<$infh>) {
382
        print $outfh $_;
383
        last if /^\n/s;
384
        }
385
        close $infh;
386
        close $outfh;
387
    }
388
    $st = system("msgmerge ".($quiet?'-q':'')." -s $str_file $tmpfile2 -o - | msgattrib --no-obsolete -o $str_file");
389
    } else {
390
    error_normal("Text extraction failed: $xgettext: $!\n", undef);
391
    error_additional("Will not run msgmerge\n", undef);
392
    }
393
    unlink $tmpfile1 || warn_normal("$tmpfile1: unlink failed: $!\n", undef);
394
    unlink $tmpfile2 || warn_normal("$tmpfile2: unlink failed: $!\n", undef);
395
396
} elsif ($action eq 'update') {
397
    my($tmph1, $tmpfile1) = tmpnam();
398
    my($tmph2, $tmpfile2) = tmpnam();
399
    close $tmph2; # We just want a name
400
    # Generate the temporary file that acts as <MODULE>/POTFILES.in
401
    for my $input (@in_files) {
402
    print $tmph1 "$input\n";
403
    }
404
    close $tmph1;
405
    # Generate the temporary file that acts as <MODULE>/<LANG>.pot
406
    $st = system($xgettext, '-s', '-f', $tmpfile1, '-o', $tmpfile2,
407
        '--po-mode',
408
        (defined $charset_in? ('-I', $charset_in): ()),
409
        (defined $charset_out? ('-O', $charset_out): ()));
410
    if ($st == 0) {
411
        # Merge the temporary "pot file" with the specified po file ($str_file)
412
        # FIXME: msgmerge(1) is a Unix dependency
413
        # FIXME: need to check the return value
414
        if ( @filenames ) {
415
            my ($tmph3, $tmpfile3) = tmpnam();
416
            $st = system("msgcat $str_file $tmpfile2 > $tmpfile3");
417
            $st = system("msgmerge ".($quiet?'-q':'')." -s $str_file $tmpfile3 -o - | msgattrib --no-obsolete -o $str_file")
418
                unless $st;
419
        } else {
420
            $st = system("msgmerge ".($quiet?'-q':'')." -s $str_file $tmpfile2 -o - | msgattrib --no-obsolete -o $str_file");
421
        }
422
    } else {
423
        error_normal("Text extraction failed: $xgettext: $!\n", undef);
424
        error_additional("Will not run msgmerge\n", undef);
425
    }
426
    unlink $tmpfile1 || warn_normal("$tmpfile1: unlink failed: $!\n", undef);
427
    unlink $tmpfile2 || warn_normal("$tmpfile2: unlink failed: $!\n", undef);
428
429
} elsif ($action eq 'install') {
430
    if(!defined($out_dir)) {
341
    if(!defined($out_dir)) {
431
    usage_error("You must specify an output directory when using the install method.");
342
    usage_error("You must specify an output directory when using the install method.");
432
    }
343
    }
Lines 554-567 translation, it can be suppressed with the %0.0s notation. Link Here
554
Using the PO format also means translators can add their
465
Using the PO format also means translators can add their
555
own comments in the translation files, if necessary.
466
own comments in the translation files, if necessary.
556
467
557
=item -
558
559
Create, update, and install actions are all based on the
560
same scanner module. This ensures that update and install
561
have the same idea of what is a translatable string;
562
attribute names in tags, for example, will not be
563
accidentally translated.
564
565
=back
468
=back
566
469
567
=head1 NOTES
470
=head1 NOTES
Lines 569-590 accidentally translated. Link Here
569
Anchors are represented by an <AI<n>> notation.
472
Anchors are represented by an <AI<n>> notation.
570
The meaning of this non-standard notation might not be obvious.
473
The meaning of this non-standard notation might not be obvious.
571
474
572
The create action calls xgettext.pl to do the actual work;
573
the update action calls xgettext.pl, msgmerge(1) and msgattrib(1)
574
to do the actual work.
575
576
=head1 BUGS
475
=head1 BUGS
577
476
578
xgettext.pl must be present in the current directory; both
579
msgmerge(1) and msgattrib(1) must also be present in the search path.
580
The script currently does not check carefully whether these
581
dependent commands are present.
582
583
Locale::PO(3) has a lot of bugs. It can neither parse nor
584
generate GNU PO files properly; a couple of workarounds have
585
been written in TmplTokenizer and more is likely to be needed
586
(e.g., to get rid of the "Strange line" warning for #~).
587
588
This script may not work in Windows.
477
This script may not work in Windows.
589
478
590
There are probably some other bugs too, since this has not been
479
There are probably some other bugs too, since this has not been
Lines 592-603 tested very much. Link Here
592
481
593
=head1 SEE ALSO
482
=head1 SEE ALSO
594
483
595
xgettext.pl,
596
TmplTokenizer.pm,
484
TmplTokenizer.pm,
597
msgmerge(1),
598
Locale::PO(3),
485
Locale::PO(3),
599
translator_doc.txt
600
601
http://www.saas.nsw.edu.au/koha_wiki/index.php?page=DifficultTerms
602
486
603
=cut
487
=cut
(-)a/misc/translator/translate (-39 / +3 lines)
Lines 54-67 usage() if $#ARGV != 1 && $#ARGV != 0; Link Here
54
54
55
my ($cmd, $lang) = @ARGV;
55
my ($cmd, $lang) = @ARGV;
56
$cmd = lc $cmd;
56
$cmd = lc $cmd;
57
if ( $cmd =~ /^(create|install|update|compress|uncompress)$/ ) {
57
if ( $cmd =~ /^(install|compress|uncompress)$/ ) {
58
    my $installer = LangInstaller->new( $lang, $pref, $verbose );
58
    my $installer = LangInstaller->new( $lang, $pref, $verbose );
59
    if ( $cmd ne 'create' and $lang and not grep( {$_ eq $lang} @{ $installer->{langs} } ) ) {
59
    if ( $lang and not grep( {$_ eq $lang} @{ $installer->{langs} } ) ) {
60
        print "Unsupported language: $lang\n";
60
        print "Unsupported language: $lang\n";
61
        exit;
61
        exit;
62
    }
62
    }
63
    if ( $all ) {
63
    if ( $all ) {
64
        usage() if $cmd eq 'create';
65
        for my $lang ( @{$installer->{langs}} ) {
64
        for my $lang ( @{$installer->{langs}} ) {
66
            $installer->set_lang( $lang );
65
            $installer->set_lang( $lang );
67
            $installer->$cmd(\@files);
66
            $installer->$cmd(\@files);
Lines 85-96 translate - Handle templates and preferences translation Link Here
85
84
86
=head1 SYNOPSYS
85
=head1 SYNOPSYS
87
86
88
  translate create fr-FR
89
  translate update fr-FR
90
  translate install fr-FR
87
  translate install fr-FR
91
  translate install fr-FR -f search -f memberentry
88
  translate install fr-FR -f search -f memberentry
92
  translate -p install fr-FR
89
  translate -p install fr-FR
93
  translate install
94
  translate compress [fr-FR]
90
  translate compress [fr-FR]
95
  translate uncompress [fr-FR]
91
  translate uncompress [fr-FR]
96
92
Lines 98-104 translate - Handle templates and preferences translation Link Here
98
94
99
In Koha, three categories of information are translated based on standard GNU
95
In Koha, three categories of information are translated based on standard GNU
100
.po files: opac templates pages, intranet templates and system preferences. The
96
.po files: opac templates pages, intranet templates and system preferences. The
101
script is a wrapper. It allows to quickly create/update/install .po files for a
97
script is a wrapper. It allows to quickly install .po files for a
102
given language or for all available languages.
98
given language or for all available languages.
103
99
104
=head1 USAGE
100
=head1 USAGE
Lines 107-144 Use the -v or --verbose parameter to make translator more verbose. Link Here
107
103
108
=over
104
=over
109
105
110
=item translate create F<lang>
111
112
Create 3 .po files in F</misc/translator/po> subdirectory: (1) from opac pages
113
templates, (2) intranet templates, and (3) from preferences. English 'en'
114
version of templates and preferences are used as references.
115
116
=over
117
118
=item F<lang>-opac-{theme}.po
119
120
Contains extracted text from english (en) OPAC templates found in
121
<KOHA_ROOT>/koha-tmpl/opac-tmpl/{theme}/en/ directory.
122
123
=item F<lang>-intranet.po
124
125
Contains extracted text from english (en) intranet templates found in
126
<KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/ directory.
127
128
=item F<lang>-pref.po
129
130
Contains extracted text from english (en) preferences. They are found in files
131
located in <KOHA_ROOT>/koha-tmpl/intranet-tmpl/prog/en/admin/preferences
132
directory.
133
134
=back
135
136
=item translate [-p] update F<lang>
137
138
Update .po files in F<po> directory, named F<lang>-*.po. Without F<lang>, all
139
available languages are updated. With -p option, only preferences .po file is
140
updated.
141
142
=item translate [-p|-f] install F<lang>
106
=item translate [-p|-f] install F<lang>
143
107
144
Use .po files to translate the english version of templates and preferences files
108
Use .po files to translate the english version of templates and preferences files
(-)a/misc/translator/xgettext-installer (+143 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
=head1 NAME
4
5
xgettext-installer - extract translatable strings from installer YAML files
6
7
=head1 SYNOPSIS
8
9
xgettext-installer [OPTION] [INPUTFILE]...
10
11
=head1 OPTIONS
12
13
=over
14
15
=item B<-f, --files-from=FILE>
16
17
get list of input files from FILE
18
19
=item B<-o, --output=FILE>
20
21
write output to the specified file
22
23
=item B<-h, --help>
24
25
display this help and exit
26
27
=back
28
29
=cut
30
31
use Modern::Perl;
32
33
use Getopt::Long;
34
use Locale::PO;
35
use Pod::Usage;
36
use YAML::Syck qw(LoadFile);
37
38
$YAML::Syck::ImplicitTyping = 1;
39
40
my $output = 'messages.pot';
41
my $files_from;
42
my $help;
43
44
GetOptions(
45
    'output=s' => \$output,
46
    'files-from=s' => \$files_from,
47
    'help' => \$help,
48
) or pod2usage(-verbose => 1, -exitval => 2);
49
50
if ($help) {
51
    pod2usage(-verbose => 1, -exitval => 0);
52
}
53
54
my @files = @ARGV;
55
if ($files_from) {
56
    open(my $fh, '<', $files_from) or die "Cannot open $files_from: $!";
57
    push @files, <$fh>;
58
    chomp @files;
59
    close $fh;
60
}
61
62
my $pot = {
63
    '' => Locale::PO->new(
64
        -msgid  => '',
65
        -msgstr =>
66
            "Project-Id-Version: Koha\n"
67
          . "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
68
          . "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\n"
69
          . "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\n"
70
          . "MIME-Version: 1.0\n"
71
          . "Content-Type: text/plain; charset=UTF-8\n"
72
          . "Content-Transfer-Encoding: 8bit\n"
73
    ),
74
};
75
76
for my $file (@files) {
77
    my $yaml = LoadFile($file);
78
    my @tables = @{ $yaml->{'tables'} };
79
80
    my $tablec = 0;
81
    for my $table (@tables) {
82
        $tablec++;
83
84
        my $table_name = ( keys %$table )[0];
85
        my @translatable = @{ $table->{$table_name}->{translatable} };
86
        my @rows = @{ $table->{$table_name}->{rows} };
87
        my @multiline = @{ $table->{$table_name}->{'multiline'} };
88
89
        my $rowc = 0;
90
        for my $row (@rows) {
91
            $rowc++;
92
93
            for my $field (@translatable) {
94
                if ( @multiline and grep { $_ eq $field } @multiline ) {
95
                    # multiline fields, only notices ATM
96
                    my $mulc;
97
                    foreach my $line ( @{ $row->{$field} } ) {
98
                        $mulc++;
99
100
                        # discard pure html, TT, empty
101
                        next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ );
102
103
                        # put placeholders
104
                        $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/g;
105
106
                        # discard non strings
107
                        next if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ or length($line) < 2 );
108
                        if ( not $pot->{$line} ) {
109
                            my $msg = new Locale::PO(
110
                                -msgid  => $line,
111
                                -msgstr => '',
112
                                -reference => "$file:$table_name:$tablec:row:$rowc:mul:$mulc"
113
                            );
114
                            $pot->{$line} = $msg;
115
                        }
116
                    }
117
                } elsif (defined $row->{$field} && length($row->{$field}) > 1 && !$pot->{ $row->{$field} }) {
118
                    my $msg = new Locale::PO(
119
                        -msgid     => $row->{$field},
120
                        -msgstr    => '',
121
                        -reference => "$file:$table_name:$tablec:row:$rowc"
122
                    );
123
                    $pot->{ $row->{$field} } = $msg;
124
                }
125
            }
126
        }
127
    }
128
129
    my $desccount = 0;
130
    for my $description ( @{ $yaml->{'description'} } ) {
131
        $desccount++;
132
        if ( length($description) > 1 and not $pot->{$description} ) {
133
            my $msg = new Locale::PO(
134
                -msgid     => $description,
135
                -msgstr    => '',
136
                -reference => "$file:description:$desccount"
137
            );
138
            $pot->{$description} = $msg;
139
        }
140
    }
141
}
142
143
Locale::PO->save_file_fromhash($output, $pot);
(-)a/misc/translator/xgettext-pref (+134 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
=head1 NAME
4
5
xgettext-pref - extract translatable strings from system preferences YAML files
6
7
=head1 SYNOPSIS
8
9
xgettext-pref [OPTION] [INPUTFILE]...
10
11
=head1 OPTIONS
12
13
=over
14
15
=item B<-f, --files-from=FILE>
16
17
get list of input files from FILE
18
19
=item B<-o, --output=FILE>
20
21
write output to the specified file
22
23
=item B<-h, --help>
24
25
display this help and exit
26
27
=back
28
29
=cut
30
31
use Modern::Perl;
32
33
use Getopt::Long;
34
use Locale::PO;
35
use Pod::Usage;
36
use YAML::Syck qw(LoadFile);
37
38
$YAML::Syck::ImplicitTyping = 1;
39
40
my $output = 'messages.pot';
41
my $files_from;
42
my $help;
43
44
GetOptions(
45
    'output=s' => \$output,
46
    'files-from=s' => \$files_from,
47
    'help' => \$help,
48
) or pod2usage(-verbose => 1, -exitval => 2);
49
50
if ($help) {
51
    pod2usage(-verbose => 1, -exitval => 0);
52
}
53
54
my @files = @ARGV;
55
if ($files_from) {
56
    open(my $fh, '<', $files_from) or die "Cannot open $files_from: $!";
57
    push @files, <$fh>;
58
    chomp @files;
59
    close $fh;
60
}
61
62
my $pot = {
63
    '' => Locale::PO->new(
64
        -msgid  => '',
65
        -msgstr => "Project-Id-Version: Koha\n"
66
          . "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
67
          . "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\n"
68
          . "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\n"
69
          . "MIME-Version: 1.0\n"
70
          . "Content-Type: text/plain; charset=UTF-8\n"
71
          . "Content-Transfer-Encoding: 8bit\n"
72
    ),
73
};
74
75
for my $file (@files) {
76
    my $pref = LoadFile($file);
77
    while ( my ($tab, $tab_content) = each %$pref ) {
78
        add_po($file, undef, $tab);
79
80
        if ( ref($tab_content) eq 'ARRAY' ) {
81
            add_prefs( $file, $tab, $tab_content );
82
        } else {
83
            while ( my ($section, $sysprefs) = each %$tab_content ) {
84
                my $context = "$tab > $section";
85
                add_po($file, $tab, $section);
86
                add_prefs( $file, $context, $sysprefs );
87
            }
88
        }
89
    }
90
}
91
92
Locale::PO->save_file_fromhash($output, $pot);
93
94
sub add_prefs {
95
    my ($file, $context, $prefs) = @_;
96
97
    for my $pref (@$prefs) {
98
        my $pref_name = '';
99
        for my $element (@$pref) {
100
            if ( ref($element) eq 'HASH' ) {
101
                $pref_name = $element->{pref};
102
                last;
103
            }
104
        }
105
        for my $element (@$pref) {
106
            if ( ref($element) eq 'HASH' ) {
107
                while ( my ( $key, $value ) = each(%$element) ) {
108
                    next unless $key eq 'choices' or $key eq 'multiple';
109
                    next unless ref($value) eq 'HASH';
110
                    for my $ckey ( keys %$value ) {
111
                        add_po( $file, "$context > $pref_name", $value->{$ckey} );
112
                    }
113
                }
114
            }
115
            elsif ($element) {
116
                add_po( $file, "$context > $pref_name", $element );
117
            }
118
        }
119
    }
120
}
121
122
sub add_po {
123
    my ( $reference, $msgctxt, $msgid ) = @_;
124
125
    return unless $msgid;
126
127
    my $key = ($msgctxt // '') . ";$msgid";
128
    $pot->{$key} = Locale::PO->new(
129
        -reference => $reference,
130
        -msgctxt   => $msgctxt,
131
        -msgid     => $msgid,
132
        -msgstr    => '',
133
    );
134
}
(-)a/misc/translator/xgettext-tt2 (+41 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
my $xgettext = Locale::XGettext::TT2::Koha->newFromArgv(\@ARGV);
6
$xgettext->setOption('plug_in', '');
7
$xgettext->run;
8
$xgettext->output;
9
10
package Locale::XGettext::TT2::Koha;
11
12
use parent 'Locale::XGettext::TT2';
13
14
sub defaultKeywords {
15
    return [
16
        't:1',
17
        'tx:1',
18
        'tn:1,2',
19
        'tnx:1,2',
20
        'txn:1,2',
21
        'tp:1c,2',
22
        'tpx:1c,2',
23
        'tnp:1c,2,3',
24
        'tnpx:1c,2,3',
25
    ];
26
}
27
28
sub defaultFlags {
29
    return [
30
        'tx:1:perl-brace-format',
31
        'tnx:1:perl-brace-format',
32
        'tnx:2:perl-brace-format',
33
        'txn:1:perl-brace-format',
34
        'txn:2:perl-brace-format',
35
        'tpx:2:perl-brace-format',
36
        'tnpx:2:perl-brace-format',
37
        'tnpx:3:perl-brace-format',
38
    ],
39
}
40
41
1;
(-)a/misc/translator/xgettext.pl (-1 / +1 lines)
Lines 173-179 EOF Link Here
173
    print $OUTPUT <<EOF;
173
    print $OUTPUT <<EOF;
174
msgid ""
174
msgid ""
175
msgstr ""
175
msgstr ""
176
"Project-Id-Version: PACKAGE VERSION\\n"
176
"Project-Id-Version: Koha\\n"
177
"POT-Creation-Date: $time_pot\\n"
177
"POT-Creation-Date: $time_pot\\n"
178
"PO-Revision-Date: $time_po\\n"
178
"PO-Revision-Date: $time_po\\n"
179
"Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n"
179
"Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n"
(-)a/package.json (-1 / +4 lines)
Lines 9-18 Link Here
9
  "dependencies": {
9
  "dependencies": {
10
    "gulp": "^3.9.1",
10
    "gulp": "^3.9.1",
11
    "gulp-autoprefixer": "^4.0.0",
11
    "gulp-autoprefixer": "^4.0.0",
12
    "gulp-concat-po": "^1.0.0",
12
    "gulp-cssnano": "^2.1.2",
13
    "gulp-cssnano": "^2.1.2",
14
    "gulp-exec": "^4.0.0",
13
    "gulp-sass": "^3.1.0",
15
    "gulp-sass": "^3.1.0",
14
    "gulp-sourcemaps": "^2.6.1",
16
    "gulp-sourcemaps": "^2.6.1",
15
    "gulp-util": "^3.0.8"
17
    "merge-stream": "^2.0.0",
18
    "minimist": "^1.2.5"
16
  },
19
  },
17
  "devDependencies": {},
20
  "devDependencies": {},
18
  "scripts": {
21
  "scripts": {
(-)a/t/LangInstaller.t (-109 lines)
Lines 1-109 Link Here
1
use Modern::Perl;
2
3
use FindBin '$Bin';
4
use lib "$Bin/../misc/translator";
5
6
use Test::More tests => 39;
7
use File::Temp qw(tempdir);
8
use File::Slurp;
9
use Locale::PO;
10
11
use t::lib::Mocks;
12
13
use_ok('LangInstaller');
14
15
my $installer = LangInstaller->new();
16
17
my $tempdir = tempdir(CLEANUP => 0);
18
t::lib::Mocks::mock_config('intrahtdocs', "$Bin/LangInstaller/templates");
19
my @files = ('simple.tt');
20
$installer->extract_messages_from_templates($tempdir, 'intranet', @files);
21
22
my $tempfile = "$tempdir/koha-tmpl/intranet-tmpl/simple.tt";
23
ok(-e $tempfile, 'it has created a temporary file simple.tt');
24
SKIP: {
25
    skip "simple.tt does not exist", 37 unless -e $tempfile;
26
27
    my $output = read_file($tempfile);
28
    my $expected_output = <<'EOF';
29
__('hello');
30
__x('hello {name}');
31
__n('item', 'items');
32
__nx('{count} item', '{count} items');
33
__p('context', 'hello');
34
__px('context', 'hello {name}');
35
__np('context', 'item', 'items');
36
__npx('context', '{count} item', '{count} items');
37
__npx('context', '{count} item', '{count} items');
38
__x('status is {status}');
39
__('active');
40
__('inactive');
41
__('Inside block');
42
EOF
43
44
    is($output, $expected_output, "Output of extract_messages_from_templates is as expected");
45
46
    my $xgettext_cmd = "xgettext -L Perl --from-code=UTF-8 "
47
        . "--package-name=Koha --package-version='' "
48
        . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 "
49
        . "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 "
50
        . "-o $tempdir/Koha.pot -D $tempdir koha-tmpl/intranet-tmpl/simple.tt";
51
52
    system($xgettext_cmd);
53
    my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
54
55
    my @expected = (
56
        {
57
            msgid => '"hello"',
58
        },
59
        {
60
            msgid => '"hello {name}"',
61
        },
62
        {
63
            msgid => '"item"',
64
            msgid_plural => '"items"',
65
        },
66
        {
67
            msgid => '"{count} item"',
68
            msgid_plural => '"{count} items"',
69
        },
70
        {
71
            msgid => '"hello"',
72
            msgctxt => '"context"',
73
        },
74
        {
75
            msgid => '"hello {name}"',
76
            msgctxt => '"context"',
77
        },
78
        {
79
            msgid => '"item"',
80
            msgid_plural => '"items"',
81
            msgctxt => '"context"',
82
        },
83
        {
84
            msgid => '"{count} item"',
85
            msgid_plural => '"{count} items"',
86
            msgctxt => '"context"',
87
        },
88
        {
89
            msgid => '"status is {status}"',
90
        },
91
        {
92
            msgid => '"active"',
93
        },
94
        {
95
            msgid => '"inactive"',
96
        },
97
        {
98
            msgid => '"Inside block"',
99
        },
100
    );
101
102
    for (my $i = 0; $i < @expected; $i++) {
103
        for my $key (qw(msgid msgid_plural msgctxt)) {
104
            my $expected = $expected[$i]->{$key};
105
            my $expected_str = defined $expected ? $expected : 'not defined';
106
            is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
107
        }
108
    }
109
}
(-)a/t/misc/translator/sample.pref (+14 lines)
Line 0 Link Here
1
Section:
2
    Subsection:
3
        -
4
            - pref: SamplePref
5
              choices:
6
                on: Do
7
                off: Do not do
8
            - that thing
9
        -
10
            - pref: MultiplePref
11
              multiple:
12
                foo: Foo ツ
13
                bar: Bar
14
                baz: Baz
(-)a/t/LangInstaller/templates/simple.tt (-1 / +1 lines)
Lines 1-6 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% PROCESS 'i18n.inc' %]
2
[% PROCESS 'i18n.inc' %]
3
[% t('hello') | $raw %]
3
[% t('hello ツ') | $raw %]
4
[% tx('hello {name}', { name = 'Bob' }) | $raw %]
4
[% tx('hello {name}', { name = 'Bob' }) | $raw %]
5
[% tn('item', 'items', count) | $raw %]
5
[% tn('item', 'items', count) | $raw %]
6
[% tnx('{count} item', '{count} items', count, { count = count }) | $raw %]
6
[% tnx('{count} item', '{count} items', count, { count = count }) | $raw %]
(-)a/t/misc/translator/sample.yml (+15 lines)
Line 0 Link Here
1
description:
2
  - "Sample installer file"
3
4
tables:
5
  - table1:
6
        translatable: [ column1, column2 ]
7
        multiline: [ column2 ]
8
        rows:
9
          - column1: foo ツ
10
            column2:
11
              - bar
12
              - baz
13
            column3: qux
14
            column4:
15
              - quux
(-)a/t/misc/translator/xgettext-installer.t (+32 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use File::Slurp;
6
use File::Temp qw(tempdir);
7
use FindBin qw($Bin);
8
use Locale::PO;
9
use Test::More tests => 4;
10
11
my $tempdir = tempdir(CLEANUP => 1);
12
13
write_file("$tempdir/files", "$Bin/sample.yml");
14
15
my $xgettext_cmd = "$Bin/../../../misc/translator/xgettext-installer "
16
    . "-o $tempdir/Koha.pot -f $tempdir/files";
17
18
system($xgettext_cmd);
19
my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
20
21
my @expected = (
22
    { msgid => '"Sample installer file"' },
23
    { msgid => '"bar"' },
24
    { msgid => '"baz"' },
25
    { msgid => '"foo ツ"' },
26
);
27
28
for (my $i = 0; $i < @expected; $i++) {
29
    my $expected = $expected[$i]->{msgid};
30
    my $expected_str = defined $expected ? $expected : 'not defined';
31
    is($pot->[$i + 1]->msgid, $expected, "$i: msgid is $expected_str");
32
}
(-)a/t/misc/translator/xgettext-pref.t (+61 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use File::Slurp;
6
use File::Temp qw(tempdir);
7
use FindBin qw($Bin);
8
use Locale::PO;
9
use Test::More tests => 16;
10
11
my $tempdir = tempdir(CLEANUP => 1);
12
13
write_file("$tempdir/files", "$Bin/sample.pref");
14
15
my $xgettext_cmd = "$Bin/../../../misc/translator/xgettext-pref "
16
    . "-o $tempdir/Koha.pot -f $tempdir/files";
17
18
system($xgettext_cmd);
19
my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
20
21
my @expected = (
22
    {
23
        msgid => '"Section"',
24
    },
25
    {
26
        msgctxt => '"Section > Subsection > MultiplePref"',
27
        msgid => '"Bar"',
28
    },
29
    {
30
        msgctxt => '"Section > Subsection > MultiplePref"',
31
        msgid => '"Baz"',
32
    },
33
    {
34
        msgctxt => '"Section > Subsection > MultiplePref"',
35
        msgid => '"Foo ツ"',
36
    },
37
    {
38
        msgctxt => '"Section > Subsection > SamplePref"',
39
        msgid => '"Do"',
40
    },
41
    {
42
        msgctxt => '"Section > Subsection > SamplePref"',
43
        msgid => '"Do not do"',
44
    },
45
    {
46
        msgctxt => '"Section > Subsection > SamplePref"',
47
        msgid => '"that thing"',
48
    },
49
    {
50
        msgctxt => '"Section"',
51
        msgid => '"Subsection"',
52
    },
53
);
54
55
for (my $i = 0; $i < @expected; $i++) {
56
    for my $key (qw(msgid msgctxt)) {
57
        my $expected = $expected[$i]->{$key};
58
        my $expected_str = defined $expected ? $expected : 'not defined';
59
        is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
60
    }
61
}
(-)a/t/misc/translator/xgettext-tt2.t (+74 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use File::Slurp;
6
use File::Temp qw(tempdir);
7
use FindBin qw($Bin);
8
use Locale::PO;
9
use Test::More tests => 36;
10
11
my $tempdir = tempdir(CLEANUP => 1);
12
13
write_file("$tempdir/files", "$Bin/sample.tt");
14
15
my $xgettext_cmd = "$Bin/../../../misc/translator/xgettext-tt2 --from-code=UTF-8 "
16
    . "-o $tempdir/Koha.pot -f $tempdir/files";
17
18
system($xgettext_cmd);
19
my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
20
21
my @expected = (
22
    {
23
        msgid => '"hello ツ"',
24
    },
25
    {
26
        msgid => '"hello {name}"',
27
    },
28
    {
29
        msgid => '"item"',
30
        msgid_plural => '"items"',
31
    },
32
    {
33
        msgid => '"{count} item"',
34
        msgid_plural => '"{count} items"',
35
    },
36
    {
37
        msgid => '"hello"',
38
        msgctxt => '"context"',
39
    },
40
    {
41
        msgid => '"hello {name}"',
42
        msgctxt => '"context"',
43
    },
44
    {
45
        msgid => '"item"',
46
        msgid_plural => '"items"',
47
        msgctxt => '"context"',
48
    },
49
    {
50
        msgid => '"{count} item"',
51
        msgid_plural => '"{count} items"',
52
        msgctxt => '"context"',
53
    },
54
    {
55
        msgid => '"status is {status}"',
56
    },
57
    {
58
        msgid => '"active"',
59
    },
60
    {
61
        msgid => '"inactive"',
62
    },
63
    {
64
        msgid => '"Inside block"',
65
    },
66
);
67
68
for (my $i = 0; $i < @expected; $i++) {
69
    for my $key (qw(msgid msgid_plural msgctxt)) {
70
        my $expected = $expected[$i]->{$key};
71
        my $expected_str = defined $expected ? $expected : 'not defined';
72
        is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
73
    }
74
}
(-)a/yarn.lock (-7 / +145 lines)
Lines 451-461 cliui@^3.2.0: Link Here
451
    strip-ansi "^3.0.1"
451
    strip-ansi "^3.0.1"
452
    wrap-ansi "^2.0.0"
452
    wrap-ansi "^2.0.0"
453
453
454
clone-buffer@^1.0.0:
455
  version "1.0.0"
456
  resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
457
  integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg=
458
454
clone-stats@^0.0.1:
459
clone-stats@^0.0.1:
455
  version "0.0.1"
460
  version "0.0.1"
456
  resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
461
  resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-0.0.1.tgz#b88f94a82cf38b8791d58046ea4029ad88ca99d1"
457
  integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=
462
  integrity sha1-uI+UqCzzi4eR1YBG6kAprYjKmdE=
458
463
464
clone-stats@^1.0.0:
465
  version "1.0.0"
466
  resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
467
  integrity sha1-s3gt/4u1R04Yuba/D9/ngvh3doA=
468
459
clone@^0.2.0:
469
clone@^0.2.0:
460
  version "0.2.0"
470
  version "0.2.0"
461
  resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f"
471
  resolved "https://registry.yarnpkg.com/clone/-/clone-0.2.0.tgz#c6126a90ad4f72dbf5acdb243cc37724fe93fc1f"
Lines 466-471 clone@^1.0.0, clone@^1.0.2: Link Here
466
  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
476
  resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e"
467
  integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
477
  integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4=
468
478
479
clone@^2.1.1:
480
  version "2.1.2"
481
  resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.2.tgz#1b7f4b9f591f1e8f83670401600345a02887435f"
482
  integrity sha1-G39Ln1kfHo+DZwQBYANFoCiHQ18=
483
484
cloneable-readable@^1.0.0:
485
  version "1.1.3"
486
  resolved "https://registry.yarnpkg.com/cloneable-readable/-/cloneable-readable-1.1.3.tgz#120a00cb053bfb63a222e709f9683ea2e11d8cec"
487
  integrity sha512-2EF8zTQOxYq70Y4XKtorQupqF0m49MBz2/yf5Bj+MHjvpG3Hy7sImifnqD6UA+TKYxeSV+u6qqQPawN5UvnpKQ==
488
  dependencies:
489
    inherits "^2.0.1"
490
    process-nextick-args "^2.0.0"
491
    readable-stream "^2.3.5"
492
469
coa@~1.0.1:
493
coa@~1.0.1:
470
  version "1.0.4"
494
  version "1.0.4"
471
  resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd"
495
  resolved "https://registry.yarnpkg.com/coa/-/coa-1.0.4.tgz#a9ef153660d6a86a8bdec0289a5c684d217432fd"
Lines 1244-1249 gulp-autoprefixer@^4.0.0: Link Here
1244
    through2 "^2.0.0"
1268
    through2 "^2.0.0"
1245
    vinyl-sourcemaps-apply "^0.2.0"
1269
    vinyl-sourcemaps-apply "^0.2.0"
1246
1270
1271
gulp-concat-po@^1.0.0:
1272
  version "1.0.0"
1273
  resolved "https://registry.yarnpkg.com/gulp-concat-po/-/gulp-concat-po-1.0.0.tgz#2fe7b2c12e45a566238e228f63396838013770ae"
1274
  integrity sha512-hFDZrUJcpw10TW3BfptL5W2FV/aMo3M+vxz9YQV4nlMBDAi8gs9/yZYZcYMYfl5XKhjpebSef8nyruoWdlX8Hw==
1275
  dependencies:
1276
    lodash.find "^4.6.0"
1277
    lodash.merge "^4.6.2"
1278
    lodash.uniq "^4.5.0"
1279
    plugin-error "^1.0.1"
1280
    pofile "^1.1.0"
1281
    through2 "^0.6.5"
1282
    vinyl "^2.2.0"
1283
1247
gulp-cssnano@^2.1.2:
1284
gulp-cssnano@^2.1.2:
1248
  version "2.1.3"
1285
  version "2.1.3"
1249
  resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
1286
  resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
Lines 1255-1260 gulp-cssnano@^2.1.2: Link Here
1255
    plugin-error "^1.0.1"
1292
    plugin-error "^1.0.1"
1256
    vinyl-sourcemaps-apply "^0.2.1"
1293
    vinyl-sourcemaps-apply "^0.2.1"
1257
1294
1295
gulp-exec@^4.0.0:
1296
  version "4.0.0"
1297
  resolved "https://registry.yarnpkg.com/gulp-exec/-/gulp-exec-4.0.0.tgz#4b6b67be0200d620143f3198a64257b68b146bb6"
1298
  integrity sha512-A9JvTyB3P4huusd/43bTr6SDg3MqBxL9AQbLnsKSO6/91wVkHfxgeJZlgDMkqK8sMel4so8wcko4SZOeB1UCgA==
1299
  dependencies:
1300
    lodash.template "^4.4.0"
1301
    plugin-error "^1.0.1"
1302
    through2 "^3.0.1"
1303
1258
gulp-sass@^3.1.0:
1304
gulp-sass@^3.1.0:
1259
  version "3.2.1"
1305
  version "3.2.1"
1260
  resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-3.2.1.tgz#2e3688a96fd8be1c0c01340750c191b2e79fab94"
1306
  resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-3.2.1.tgz#2e3688a96fd8be1c0c01340750c191b2e79fab94"
Lines 1283-1289 gulp-sourcemaps@^2.6.1: Link Here
1283
    strip-bom-string "1.X"
1329
    strip-bom-string "1.X"
1284
    through2 "2.X"
1330
    through2 "2.X"
1285
1331
1286
gulp-util@^3.0, gulp-util@^3.0.0, gulp-util@^3.0.8:
1332
gulp-util@^3.0, gulp-util@^3.0.0:
1287
  version "3.0.8"
1333
  version "3.0.8"
1288
  resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f"
1334
  resolved "https://registry.yarnpkg.com/gulp-util/-/gulp-util-3.0.8.tgz#0054e1e744502e27c04c187c3ecc505dd54bbb4f"
1289
  integrity sha1-AFTh50RQLifATBh8PsxQXdVLu08=
1335
  integrity sha1-AFTh50RQLifATBh8PsxQXdVLu08=
Lines 1469-1475 inherits@1: Link Here
1469
  resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b"
1515
  resolved "https://registry.yarnpkg.com/inherits/-/inherits-1.0.2.tgz#ca4309dadee6b54cc0b8d247e8d7c7a0975bdc9b"
1470
  integrity sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=
1516
  integrity sha1-ykMJ2t7mtUzAuNJH6NfHoJdb3Js=
1471
1517
1472
inherits@2, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
1518
inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3:
1473
  version "2.0.4"
1519
  version "2.0.4"
1474
  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
1520
  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
1475
  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
1521
  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
Lines 1854-1859 lodash.escape@^3.0.0: Link Here
1854
  dependencies:
1900
  dependencies:
1855
    lodash._root "^3.0.0"
1901
    lodash._root "^3.0.0"
1856
1902
1903
lodash.find@^4.6.0:
1904
  version "4.6.0"
1905
  resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
1906
  integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=
1907
1857
lodash.isarguments@^3.0.0:
1908
lodash.isarguments@^3.0.0:
1858
  version "3.1.0"
1909
  version "3.1.0"
1859
  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
1910
  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
Lines 1878-1883 lodash.memoize@^4.1.2: Link Here
1878
  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
1929
  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
1879
  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
1930
  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
1880
1931
1932
lodash.merge@^4.6.2:
1933
  version "4.6.2"
1934
  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
1935
  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
1936
1881
lodash.restparam@^3.0.0:
1937
lodash.restparam@^3.0.0:
1882
  version "3.6.1"
1938
  version "3.6.1"
1883
  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
1939
  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
Lines 1898-1903 lodash.template@^3.0.0: Link Here
1898
    lodash.restparam "^3.0.0"
1954
    lodash.restparam "^3.0.0"
1899
    lodash.templatesettings "^3.0.0"
1955
    lodash.templatesettings "^3.0.0"
1900
1956
1957
lodash.template@^4.4.0:
1958
  version "4.5.0"
1959
  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
1960
  integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
1961
  dependencies:
1962
    lodash._reinterpolate "^3.0.0"
1963
    lodash.templatesettings "^4.0.0"
1964
1901
lodash.templatesettings@^3.0.0:
1965
lodash.templatesettings@^3.0.0:
1902
  version "3.1.1"
1966
  version "3.1.1"
1903
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
1967
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
Lines 1906-1911 lodash.templatesettings@^3.0.0: Link Here
1906
    lodash._reinterpolate "^3.0.0"
1970
    lodash._reinterpolate "^3.0.0"
1907
    lodash.escape "^3.0.0"
1971
    lodash.escape "^3.0.0"
1908
1972
1973
lodash.templatesettings@^4.0.0:
1974
  version "4.2.0"
1975
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
1976
  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
1977
  dependencies:
1978
    lodash._reinterpolate "^3.0.0"
1979
1909
lodash.uniq@^4.5.0:
1980
lodash.uniq@^4.5.0:
1910
  version "4.5.0"
1981
  version "4.5.0"
1911
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
1982
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
Lines 1998-2003 meow@^3.7.0: Link Here
1998
    redent "^1.0.0"
2069
    redent "^1.0.0"
1999
    trim-newlines "^1.0.0"
2070
    trim-newlines "^1.0.0"
2000
2071
2072
merge-stream@^2.0.0:
2073
  version "2.0.0"
2074
  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
2075
  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
2076
2001
micromatch@^3.0.4:
2077
micromatch@^3.0.4:
2002
  version "3.1.10"
2078
  version "3.1.10"
2003
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
2079
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
Lines 2046-2051 minimist@^1.1.0, minimist@^1.1.3: Link Here
2046
  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
2122
  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
2047
  integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
2123
  integrity sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=
2048
2124
2125
minimist@^1.2.5:
2126
  version "1.2.5"
2127
  resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.5.tgz#67d66014b66a6a8aaa0c083c5fd58df4e4e97602"
2128
  integrity sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==
2129
2049
mixin-deep@^1.2.0:
2130
mixin-deep@^1.2.0:
2050
  version "1.3.2"
2131
  version "1.3.2"
2051
  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
2132
  resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
Lines 2431-2436 plugin-error@^1.0.1: Link Here
2431
    arr-union "^3.1.0"
2512
    arr-union "^3.1.0"
2432
    extend-shallow "^3.0.2"
2513
    extend-shallow "^3.0.2"
2433
2514
2515
pofile@^1.1.0:
2516
  version "1.1.0"
2517
  resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.1.0.tgz#9ce84bbef5043ceb4f19bdc3520d85778fad4f94"
2518
  integrity sha512-6XYcNkXWGiJ2CVXogTP7uJ6ZXQCldYLZc16wgRp8tqRaBTTyIfF+TUT3EQJPXTLAT7OTPpTAoaFdoXKfaTRU1w==
2519
2434
posix-character-classes@^0.1.0:
2520
posix-character-classes@^0.1.0:
2435
  version "0.1.1"
2521
  version "0.1.1"
2436
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
2522
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
Lines 2694-2700 pretty-hrtime@^1.0.0: Link Here
2694
  resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
2780
  resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
2695
  integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
2781
  integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=
2696
2782
2697
process-nextick-args@~2.0.0:
2783
process-nextick-args@^2.0.0, process-nextick-args@~2.0.0:
2698
  version "2.0.1"
2784
  version "2.0.1"
2699
  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
2785
  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
2700
  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
2786
  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
Lines 2754-2759 read-pkg@^1.0.0: Link Here
2754
    normalize-package-data "^2.3.2"
2840
    normalize-package-data "^2.3.2"
2755
    path-type "^1.0.0"
2841
    path-type "^1.0.0"
2756
2842
2843
"readable-stream@2 || 3":
2844
  version "3.6.0"
2845
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
2846
  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
2847
  dependencies:
2848
    inherits "^2.0.3"
2849
    string_decoder "^1.1.1"
2850
    util-deprecate "^1.0.1"
2851
2757
"readable-stream@>=1.0.33-1 <1.1.0-0":
2852
"readable-stream@>=1.0.33-1 <1.1.0-0":
2758
  version "1.0.34"
2853
  version "1.0.34"
2759
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
2854
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
Lines 2777-2782 readable-stream@^2.0.1, readable-stream@^2.0.6, readable-stream@~2.3.6: Link Here
2777
    string_decoder "~1.1.1"
2872
    string_decoder "~1.1.1"
2778
    util-deprecate "~1.0.1"
2873
    util-deprecate "~1.0.1"
2779
2874
2875
readable-stream@^2.3.5:
2876
  version "2.3.7"
2877
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
2878
  integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==
2879
  dependencies:
2880
    core-util-is "~1.0.0"
2881
    inherits "~2.0.3"
2882
    isarray "~1.0.0"
2883
    process-nextick-args "~2.0.0"
2884
    safe-buffer "~5.1.1"
2885
    string_decoder "~1.1.1"
2886
    util-deprecate "~1.0.1"
2887
2780
readable-stream@~1.1.9:
2888
readable-stream@~1.1.9:
2781
  version "1.1.14"
2889
  version "1.1.14"
2782
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
2890
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.1.14.tgz#7cf4c54ef648e3813084c636dd2079e166c081d9"
Lines 2853-2858 replace-ext@0.0.1: Link Here
2853
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
2961
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-0.0.1.tgz#29bbd92078a739f0bcce2b4ee41e837953522924"
2854
  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
2962
  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
2855
2963
2964
replace-ext@^1.0.0:
2965
  version "1.0.0"
2966
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
2967
  integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
2968
2856
request@^2.87.0, request@^2.88.0:
2969
request@^2.87.0, request@^2.88.0:
2857
  version "2.88.0"
2970
  version "2.88.0"
2858
  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
2971
  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
Lines 2921-2927 rimraf@2: Link Here
2921
  dependencies:
3034
  dependencies:
2922
    glob "^7.1.3"
3035
    glob "^7.1.3"
2923
3036
2924
safe-buffer@^5.0.1, safe-buffer@^5.1.2:
3037
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
2925
  version "5.2.0"
3038
  version "5.2.0"
2926
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
3039
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
2927
  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
3040
  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
Lines 3176-3181 string-width@^1.0.1, string-width@^1.0.2: Link Here
3176
    is-fullwidth-code-point "^2.0.0"
3289
    is-fullwidth-code-point "^2.0.0"
3177
    strip-ansi "^4.0.0"
3290
    strip-ansi "^4.0.0"
3178
3291
3292
string_decoder@^1.1.1:
3293
  version "1.3.0"
3294
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
3295
  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
3296
  dependencies:
3297
    safe-buffer "~5.2.0"
3298
3179
string_decoder@~0.10.x:
3299
string_decoder@~0.10.x:
3180
  version "0.10.31"
3300
  version "0.10.31"
3181
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
3301
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
Lines 3278-3284 through2@2.X, through2@^2.0.0, through2@^2.0.3: Link Here
3278
    readable-stream "~2.3.6"
3398
    readable-stream "~2.3.6"
3279
    xtend "~4.0.1"
3399
    xtend "~4.0.1"
3280
3400
3281
through2@^0.6.1:
3401
through2@^0.6.1, through2@^0.6.5:
3282
  version "0.6.5"
3402
  version "0.6.5"
3283
  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
3403
  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
3284
  integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
3404
  integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
Lines 3286-3291 through2@^0.6.1: Link Here
3286
    readable-stream ">=1.0.33-1 <1.1.0-0"
3406
    readable-stream ">=1.0.33-1 <1.1.0-0"
3287
    xtend ">=4.0.0 <4.1.0-0"
3407
    xtend ">=4.0.0 <4.1.0-0"
3288
3408
3409
through2@^3.0.1:
3410
  version "3.0.1"
3411
  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a"
3412
  integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==
3413
  dependencies:
3414
    readable-stream "2 || 3"
3415
3289
tildify@^1.0.0:
3416
tildify@^1.0.0:
3290
  version "1.2.0"
3417
  version "1.2.0"
3291
  resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a"
3418
  resolved "https://registry.yarnpkg.com/tildify/-/tildify-1.2.0.tgz#dcec03f55dca9b7aa3e5b04f21817eb56e63588a"
Lines 3433-3439 user-home@^1.1.1: Link Here
3433
  resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
3560
  resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
3434
  integrity sha1-K1viOjK2Onyd640PKNSFcko98ZA=
3561
  integrity sha1-K1viOjK2Onyd640PKNSFcko98ZA=
3435
3562
3436
util-deprecate@~1.0.1:
3563
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
3437
  version "1.0.2"
3564
  version "1.0.2"
3438
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
3565
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
3439
  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
3566
  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
Lines 3510-3515 vinyl@^0.5.0: Link Here
3510
    clone-stats "^0.0.1"
3637
    clone-stats "^0.0.1"
3511
    replace-ext "0.0.1"
3638
    replace-ext "0.0.1"
3512
3639
3640
vinyl@^2.2.0:
3641
  version "2.2.0"
3642
  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
3643
  integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
3644
  dependencies:
3645
    clone "^2.1.1"
3646
    clone-buffer "^1.0.0"
3647
    clone-stats "^1.0.0"
3648
    cloneable-readable "^1.0.0"
3649
    remove-trailing-separator "^1.0.1"
3650
    replace-ext "^1.0.0"
3651
3513
whet.extend@~0.9.9:
3652
whet.extend@~0.9.9:
3514
  version "0.9.9"
3653
  version "0.9.9"
3515
  resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
3654
  resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1"
3516
- 

Return to bug 25067