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

(-)a/cpanfile (-1 / +1 lines)
Lines 143-148 recommends 'Gravatar::URL', '1.03'; Link Here
143
recommends 'HTTPD::Bench::ApacheBench', '0.73';
143
recommends 'HTTPD::Bench::ApacheBench', '0.73';
144
recommends 'LWP::Protocol::https', '5.836';
144
recommends 'LWP::Protocol::https', '5.836';
145
recommends 'Lingua::Ispell', '0.07';
145
recommends 'Lingua::Ispell', '0.07';
146
recommends 'Locale::XGettext::TT2', '0.7';
146
recommends 'Module::Bundled::Files', '0.03';
147
recommends 'Module::Bundled::Files', '0.03';
147
recommends 'Module::Load::Conditional', '0.38';
148
recommends 'Module::Load::Conditional', '0.38';
148
recommends 'Module::Pluggable', '3.9';
149
recommends 'Module::Pluggable', '3.9';
Lines 154-160 recommends 'Net::SFTP::Foreign', '1.73'; Link Here
154
recommends 'Net::Server', '0.97';
155
recommends 'Net::Server', '0.97';
155
recommends 'Net::Z3950::SimpleServer', '1.15';
156
recommends 'Net::Z3950::SimpleServer', '1.15';
156
recommends 'PDF::FromHTML', '0.31';
157
recommends 'PDF::FromHTML', '0.31';
157
recommends 'PPI', '1.215';
158
recommends 'Parallel::ForkManager', '0.75';
158
recommends 'Parallel::ForkManager', '0.75';
159
recommends 'Readonly', '0.01';
159
recommends 'Readonly', '0.01';
160
recommends 'Readonly::XS', '0.01';
160
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 (-1 / +292 lines)
Lines 1-13 Link Here
1
/* eslint-env node */
1
/* eslint-env node */
2
/* eslint no-console:"off" */
2
/* eslint no-console:"off" */
3
3
4
const { dest, series, src, watch } = require('gulp');
4
const { dest, parallel, series, src, watch } = require('gulp');
5
6
const child_process = require('child_process');
7
const fs = require('fs');
8
const os = require('os');
9
const path = require('path');
10
const util = require('util');
5
11
6
const sass = require("gulp-sass");
12
const sass = require("gulp-sass");
7
const cssnano = require("gulp-cssnano");
13
const cssnano = require("gulp-cssnano");
8
const rtlcss = require('gulp-rtlcss');
14
const rtlcss = require('gulp-rtlcss');
9
const sourcemaps = require('gulp-sourcemaps');
15
const sourcemaps = require('gulp-sourcemaps');
10
const autoprefixer = require('gulp-autoprefixer');
16
const autoprefixer = require('gulp-autoprefixer');
17
const concatPo = require('gulp-concat-po');
18
const exec = require('gulp-exec');
19
const merge = require('merge-stream');
20
const through2 = require('through2');
21
const Vinyl = require('vinyl');
11
const args = require('minimist')(process.argv.slice(2));
22
const args = require('minimist')(process.argv.slice(2));
12
const rename = require('gulp-rename');
23
const rename = require('gulp-rename');
13
24
Lines 62-69 function build() { Link Here
62
        .pipe(dest(css_base));
73
        .pipe(dest(css_base));
63
}
74
}
64
75
76
const poTasks = {
77
    'marc-MARC21': {
78
        extract: po_extract_marc_marc21,
79
        create: po_create_marc_marc21,
80
        update: po_update_marc_marc21,
81
    },
82
    'marc-NORMARC': {
83
        extract: po_extract_marc_normarc,
84
        create: po_create_marc_normarc,
85
        update: po_update_marc_normarc,
86
    },
87
    'marc-UNIMARC': {
88
        extract: po_extract_marc_unimarc,
89
        create: po_create_marc_unimarc,
90
        update: po_update_marc_unimarc,
91
    },
92
    'staff-prog': {
93
        extract: po_extract_staff,
94
        create: po_create_staff,
95
        update: po_update_staff,
96
    },
97
    'opac-bootstrap': {
98
        extract: po_extract_opac,
99
        create: po_create_opac,
100
        update: po_update_opac,
101
    },
102
    'pref': {
103
        extract: po_extract_pref,
104
        create: po_create_pref,
105
        update: po_update_pref,
106
    },
107
    'messages': {
108
        extract: po_extract_messages,
109
        create: po_create_messages,
110
        update: po_update_messages,
111
    },
112
    'messages-js': {
113
        extract: po_extract_messages_js,
114
        create: po_create_messages_js,
115
        update: po_update_messages_js,
116
    },
117
    'installer': {
118
        extract: po_extract_installer,
119
        create: po_create_installer,
120
        update: po_update_installer,
121
    },
122
    'installer-MARC21': {
123
        extract: po_extract_installer_marc21,
124
        create: po_create_installer_marc21,
125
        update: po_update_installer_marc21,
126
    },
127
};
128
129
const poTypes = Object.keys(poTasks);
130
131
function po_extract_marc (type) {
132
    return src(`koha-tmpl/*-tmpl/*/en/**/*${type}*`, { read: false, nocase: true })
133
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', `Koha-marc-${type}.pot`))
134
        .pipe(dest('misc/translator'))
135
}
136
137
function po_extract_marc_marc21 ()  { return po_extract_marc('MARC21') }
138
function po_extract_marc_normarc () { return po_extract_marc('NORMARC') }
139
function po_extract_marc_unimarc () { return po_extract_marc('UNIMARC') }
140
141
function po_extract_staff () {
142
    const globs = [
143
        'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
144
        'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
145
        '!koha-tmpl/intranet-tmpl/prog/en/**/*MARC21*',
146
        '!koha-tmpl/intranet-tmpl/prog/en/**/*NORMARC*',
147
        '!koha-tmpl/intranet-tmpl/prog/en/**/*UNIMARC*',
148
    ];
149
150
    return src(globs, { read: false, nocase: true })
151
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', 'Koha-staff-prog.pot'))
152
        .pipe(dest('misc/translator'))
153
}
154
155
function po_extract_opac () {
156
    const globs = [
157
        'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
158
        'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
159
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*MARC21*',
160
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*NORMARC*',
161
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*UNIMARC*',
162
    ];
163
164
    return src(globs, { read: false, nocase: true })
165
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', 'Koha-opac-bootstrap.pot'))
166
        .pipe(dest('misc/translator'))
167
}
168
169
const xgettext_options = '--from-code=UTF-8 --package-name Koha '
170
    + '--package-version= -k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 '
171
    + '-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ '
172
    + '-kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3 --force-po';
173
174
function po_extract_messages_js () {
175
    const globs = [
176
        'koha-tmpl/intranet-tmpl/prog/js/**/*.js',
177
        'koha-tmpl/opac-tmpl/bootstrap/js/**/*.js',
178
    ];
179
180
    return src(globs, { read: false, nocase: true })
181
        .pipe(xgettext(`xgettext -L JavaScript ${xgettext_options}`, 'Koha-messages-js.pot'))
182
        .pipe(dest('misc/translator'))
183
}
184
185
function po_extract_messages () {
186
    const perlStream = src(['**/*.pl', '**/*.pm'], { read: false, nocase: true })
187
        .pipe(xgettext(`xgettext -L Perl ${xgettext_options}`, 'Koha-perl.pot'))
188
189
    const ttStream = src([
190
            'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
191
            'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
192
            'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
193
            'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
194
        ], { read: false, nocase: true })
195
        .pipe(xgettext('misc/translator/xgettext-tt2 --from-code=UTF-8', 'Koha-tt.pot'))
196
197
    const headers = {
198
        'Project-Id-Version': 'Koha',
199
        'Content-Type': 'text/plain; charset=UTF-8',
200
    };
201
202
    return merge(perlStream, ttStream)
203
        .pipe(concatPo('Koha-messages.pot', { headers }))
204
        .pipe(dest('misc/translator'))
205
}
206
207
function po_extract_pref () {
208
    return src('koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/*.pref', { read: false })
209
        .pipe(xgettext('misc/translator/xgettext-pref', 'Koha-pref.pot'))
210
        .pipe(dest('misc/translator'))
211
}
212
213
function po_extract_installer () {
214
    const globs = [
215
        'installer/data/mysql/en/mandatory/*.yml',
216
        'installer/data/mysql/en/optional/*.yml',
217
    ];
218
219
    return src(globs, { read: false, nocase: true })
220
        .pipe(xgettext('misc/translator/xgettext-installer', 'Koha-installer.pot'))
221
        .pipe(dest('misc/translator'))
222
}
223
224
function po_extract_installer_marc (type) {
225
    const globs = `installer/data/mysql/en/marcflavour/${type}/**/*.yml`;
226
227
    return src(globs, { read: false, nocase: true })
228
        .pipe(xgettext('misc/translator/xgettext-installer', `Koha-installer-${type}.pot`))
229
        .pipe(dest('misc/translator'))
230
}
231
232
function po_extract_installer_marc21 ()  { return po_extract_installer_marc('MARC21') }
233
234
function po_create_type (type) {
235
    const access = util.promisify(fs.access);
236
    const exec = util.promisify(child_process.exec);
237
238
    const languages = getLanguages();
239
    const promises = [];
240
    for (const language of languages) {
241
        const locale = language.split('-').filter(s => s.length !== 4).join('_');
242
        const po = `misc/translator/po/${language}-${type}.po`;
243
        const pot = `misc/translator/Koha-${type}.pot`;
244
245
        const promise = access(po)
246
            .catch(() => exec(`msginit -o ${po} -i ${pot} -l ${locale} --no-translator`))
247
        promises.push(promise);
248
    }
249
250
    return Promise.all(promises);
251
}
252
253
function po_create_marc_marc21 ()       { return po_create_type('marc-MARC21') }
254
function po_create_marc_normarc ()      { return po_create_type('marc-NORMARC') }
255
function po_create_marc_unimarc ()      { return po_create_type('marc-UNIMARC') }
256
function po_create_staff ()             { return po_create_type('staff-prog') }
257
function po_create_opac ()              { return po_create_type('opac-bootstrap') }
258
function po_create_pref ()              { return po_create_type('pref') }
259
function po_create_messages ()          { return po_create_type('messages') }
260
function po_create_messages_js ()       { return po_create_type('messages-js') }
261
function po_create_installer ()         { return po_create_type('installer') }
262
function po_create_installer_marc21 ()  { return po_create_type('installer-MARC21') }
263
264
function po_update_type (type) {
265
    const msgmerge_opts = '--backup=off --quiet --sort-output --update';
266
    const cmd = `msgmerge ${msgmerge_opts} <%= file.path %> misc/translator/Koha-${type}.pot`;
267
    const languages = getLanguages();
268
    const globs = languages.map(language => `misc/translator/po/${language}-${type}.po`);
269
270
    return src(globs)
271
        .pipe(exec(cmd, { continueOnError: true }))
272
        .pipe(exec.reporter({ err: false, stdout: false }))
273
}
274
275
function po_update_marc_marc21 ()       { return po_update_type('marc-MARC21') }
276
function po_update_marc_normarc ()      { return po_update_type('marc-NORMARC') }
277
function po_update_marc_unimarc ()      { return po_update_type('marc-UNIMARC') }
278
function po_update_staff ()             { return po_update_type('staff-prog') }
279
function po_update_opac ()              { return po_update_type('opac-bootstrap') }
280
function po_update_pref ()              { return po_update_type('pref') }
281
function po_update_messages ()          { return po_update_type('messages') }
282
function po_update_messages_js ()       { return po_update_type('messages-js') }
283
function po_update_installer ()         { return po_update_type('installer') }
284
function po_update_installer_marc21 ()  { return po_update_type('installer-MARC21') }
285
286
/**
287
 * Gulp plugin that executes xgettext-like command `cmd` on all files given as
288
 * input, and then outputs the result as a POT file named `filename`.
289
 * `cmd` should accept -o and -f options
290
 */
291
function xgettext (cmd, filename) {
292
    const filenames = [];
293
294
    function transform (file, encoding, callback) {
295
        filenames.push(path.relative(file.cwd, file.path));
296
        callback();
297
    }
298
299
    function flush (callback) {
300
        fs.mkdtemp(path.join(os.tmpdir(), 'koha-'), (err, folder) => {
301
            const outputFilename = path.join(folder, filename);
302
            const filesFilename = path.join(folder, 'files');
303
            fs.writeFile(filesFilename, filenames.join(os.EOL), err => {
304
                if (err) return callback(err);
305
306
                const command = `${cmd} -o ${outputFilename} -f ${filesFilename}`;
307
                child_process.exec(command, err => {
308
                    if (err) return callback(err);
309
310
                    fs.readFile(outputFilename, (err, data) => {
311
                        if (err) return callback(err);
312
313
                        const file = new Vinyl();
314
                        file.path = path.join(file.base, filename);
315
                        file.contents = data;
316
                        callback(null, file);
317
                    });
318
                });
319
            });
320
        })
321
    }
322
323
    return through2.obj(transform, flush);
324
}
325
326
/**
327
 * Return languages selected for PO-related tasks
328
 *
329
 * This can be either languages given on command-line with --lang option, or
330
 * all the languages found in misc/translator/po otherwise
331
 */
332
function getLanguages () {
333
    if (Array.isArray(args.lang)) {
334
        return args.lang;
335
    }
336
337
    if (args.lang) {
338
        return [args.lang];
339
    }
340
341
    const filenames = fs.readdirSync('misc/translator/po')
342
        .filter(filename => filename.endsWith('.po'))
343
        .filter(filename => !filename.startsWith('.'))
344
345
    const re = new RegExp('-(' + poTypes.join('|') + ')\.po$');
346
    languages = filenames.map(filename => filename.replace(re, ''))
347
348
    return Array.from(new Set(languages));
349
}
350
65
exports.build = build;
351
exports.build = build;
66
exports.css = css;
352
exports.css = css;
353
354
exports['po:create'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].create)));
355
exports['po:update'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].update)));
356
exports['po:extract'] = parallel(...poTypes.map(type => poTasks[type].extract));
357
67
exports.default = function () {
358
exports.default = function () {
68
    watch(css_base + "/src/**/*.scss", series('css'));
359
    watch(css_base + "/src/**/*.scss", series('css'));
69
}
360
}
(-)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/po/te-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 (+3 lines)
Lines 10-20 Link Here
10
    "bootstrap": "^4.5.2",
10
    "bootstrap": "^4.5.2",
11
    "gulp": "^4.0.2",
11
    "gulp": "^4.0.2",
12
    "gulp-autoprefixer": "^4.0.0",
12
    "gulp-autoprefixer": "^4.0.0",
13
    "gulp-concat-po": "^1.0.0",
13
    "gulp-cssnano": "^2.1.2",
14
    "gulp-cssnano": "^2.1.2",
15
    "gulp-exec": "^4.0.0",
14
    "gulp-rename": "^2.0.0",
16
    "gulp-rename": "^2.0.0",
15
    "gulp-rtlcss": "^1.4.1",
17
    "gulp-rtlcss": "^1.4.1",
16
    "gulp-sass": "^3.1.0",
18
    "gulp-sass": "^3.1.0",
17
    "gulp-sourcemaps": "^2.6.1",
19
    "gulp-sourcemaps": "^2.6.1",
20
    "merge-stream": "^2.0.0",
18
    "minimist": "^1.2.5"
21
    "minimist": "^1.2.5"
19
  },
22
  },
20
  "scripts": {
23
  "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 / +109 lines)
Lines 1522-1527 gulp-cli@^2.2.0: Link Here
1522
    v8flags "^3.2.0"
1522
    v8flags "^3.2.0"
1523
    yargs "^7.1.0"
1523
    yargs "^7.1.0"
1524
1524
1525
gulp-concat-po@^1.0.0:
1526
  version "1.0.0"
1527
  resolved "https://registry.yarnpkg.com/gulp-concat-po/-/gulp-concat-po-1.0.0.tgz#2fe7b2c12e45a566238e228f63396838013770ae"
1528
  integrity sha512-hFDZrUJcpw10TW3BfptL5W2FV/aMo3M+vxz9YQV4nlMBDAi8gs9/yZYZcYMYfl5XKhjpebSef8nyruoWdlX8Hw==
1529
  dependencies:
1530
    lodash.find "^4.6.0"
1531
    lodash.merge "^4.6.2"
1532
    lodash.uniq "^4.5.0"
1533
    plugin-error "^1.0.1"
1534
    pofile "^1.1.0"
1535
    through2 "^0.6.5"
1536
    vinyl "^2.2.0"
1537
1525
gulp-cssnano@^2.1.2:
1538
gulp-cssnano@^2.1.2:
1526
  version "2.1.3"
1539
  version "2.1.3"
1527
  resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
1540
  resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
Lines 1533-1538 gulp-cssnano@^2.1.2: Link Here
1533
    plugin-error "^1.0.1"
1546
    plugin-error "^1.0.1"
1534
    vinyl-sourcemaps-apply "^0.2.1"
1547
    vinyl-sourcemaps-apply "^0.2.1"
1535
1548
1549
gulp-exec@^4.0.0:
1550
  version "4.0.0"
1551
  resolved "https://registry.yarnpkg.com/gulp-exec/-/gulp-exec-4.0.0.tgz#4b6b67be0200d620143f3198a64257b68b146bb6"
1552
  integrity sha512-A9JvTyB3P4huusd/43bTr6SDg3MqBxL9AQbLnsKSO6/91wVkHfxgeJZlgDMkqK8sMel4so8wcko4SZOeB1UCgA==
1553
  dependencies:
1554
    lodash.template "^4.4.0"
1555
    plugin-error "^1.0.1"
1556
    through2 "^3.0.1"
1557
1536
gulp-rename@^2.0.0:
1558
gulp-rename@^2.0.0:
1537
  version "2.0.0"
1559
  version "2.0.0"
1538
  resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c"
1560
  resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c"
Lines 2205-2210 lodash.escape@^3.0.0: Link Here
2205
  dependencies:
2227
  dependencies:
2206
    lodash._root "^3.0.0"
2228
    lodash._root "^3.0.0"
2207
2229
2230
lodash.find@^4.6.0:
2231
  version "4.6.0"
2232
  resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
2233
  integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=
2234
2208
lodash.isarguments@^3.0.0:
2235
lodash.isarguments@^3.0.0:
2209
  version "3.1.0"
2236
  version "3.1.0"
2210
  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
2237
  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
Lines 2229-2234 lodash.memoize@^4.1.2: Link Here
2229
  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
2256
  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
2230
  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
2257
  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
2231
2258
2259
lodash.merge@^4.6.2:
2260
  version "4.6.2"
2261
  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
2262
  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
2263
2232
lodash.restparam@^3.0.0:
2264
lodash.restparam@^3.0.0:
2233
  version "3.6.1"
2265
  version "3.6.1"
2234
  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
2266
  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
Lines 2249-2254 lodash.template@^3.0.0: Link Here
2249
    lodash.restparam "^3.0.0"
2281
    lodash.restparam "^3.0.0"
2250
    lodash.templatesettings "^3.0.0"
2282
    lodash.templatesettings "^3.0.0"
2251
2283
2284
lodash.template@^4.4.0:
2285
  version "4.5.0"
2286
  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
2287
  integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
2288
  dependencies:
2289
    lodash._reinterpolate "^3.0.0"
2290
    lodash.templatesettings "^4.0.0"
2291
2252
lodash.templatesettings@^3.0.0:
2292
lodash.templatesettings@^3.0.0:
2253
  version "3.1.1"
2293
  version "3.1.1"
2254
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
2294
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
Lines 2257-2262 lodash.templatesettings@^3.0.0: Link Here
2257
    lodash._reinterpolate "^3.0.0"
2297
    lodash._reinterpolate "^3.0.0"
2258
    lodash.escape "^3.0.0"
2298
    lodash.escape "^3.0.0"
2259
2299
2300
lodash.templatesettings@^4.0.0:
2301
  version "4.2.0"
2302
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
2303
  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
2304
  dependencies:
2305
    lodash._reinterpolate "^3.0.0"
2306
2260
lodash.uniq@^4.5.0:
2307
lodash.uniq@^4.5.0:
2261
  version "4.5.0"
2308
  version "4.5.0"
2262
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
2309
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
Lines 2359-2364 meow@^3.7.0: Link Here
2359
    redent "^1.0.0"
2406
    redent "^1.0.0"
2360
    trim-newlines "^1.0.0"
2407
    trim-newlines "^1.0.0"
2361
2408
2409
merge-stream@^2.0.0:
2410
  version "2.0.0"
2411
  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
2412
  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
2413
2362
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
2414
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
2363
  version "3.1.10"
2415
  version "3.1.10"
2364
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
2416
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
Lines 2842-2847 plugin-error@^1.0.1: Link Here
2842
    arr-union "^3.1.0"
2894
    arr-union "^3.1.0"
2843
    extend-shallow "^3.0.2"
2895
    extend-shallow "^3.0.2"
2844
2896
2897
pofile@^1.1.0:
2898
  version "1.1.0"
2899
  resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.1.0.tgz#9ce84bbef5043ceb4f19bdc3520d85778fad4f94"
2900
  integrity sha512-6XYcNkXWGiJ2CVXogTP7uJ6ZXQCldYLZc16wgRp8tqRaBTTyIfF+TUT3EQJPXTLAT7OTPpTAoaFdoXKfaTRU1w==
2901
2845
posix-character-classes@^0.1.0:
2902
posix-character-classes@^0.1.0:
2846
  version "0.1.1"
2903
  version "0.1.1"
2847
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
2904
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
Lines 3177-3182 read-pkg@^1.0.0: Link Here
3177
    normalize-package-data "^2.3.2"
3234
    normalize-package-data "^2.3.2"
3178
    path-type "^1.0.0"
3235
    path-type "^1.0.0"
3179
3236
3237
"readable-stream@2 || 3":
3238
  version "3.6.0"
3239
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
3240
  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
3241
  dependencies:
3242
    inherits "^2.0.3"
3243
    string_decoder "^1.1.1"
3244
    util-deprecate "^1.0.1"
3245
3246
"readable-stream@>=1.0.33-1 <1.1.0-0":
3247
  version "1.0.34"
3248
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
3249
  integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
3250
  dependencies:
3251
    core-util-is "~1.0.0"
3252
    inherits "~2.0.1"
3253
    isarray "0.0.1"
3254
    string_decoder "~0.10.x"
3255
3180
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
3256
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
3181
  version "2.3.7"
3257
  version "2.3.7"
3182
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
3258
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
Lines 3293-3301 replace-ext@0.0.1: Link Here
3293
  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
3369
  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
3294
3370
3295
replace-ext@^1.0.0:
3371
replace-ext@^1.0.0:
3296
  version "1.0.1"
3372
  version "1.0.0"
3297
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a"
3373
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
3298
  integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==
3374
  integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
3299
3375
3300
replace-homedir@^1.0.0:
3376
replace-homedir@^1.0.0:
3301
  version "1.0.0"
3377
  version "1.0.0"
Lines 3407-3412 safe-buffer@~5.1.0, safe-buffer@~5.1.1: Link Here
3407
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
3483
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
3408
  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
3484
  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
3409
3485
3486
safe-buffer@~5.2.0:
3487
  version "5.2.0"
3488
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
3489
  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
3490
3410
safe-regex@^1.1.0:
3491
safe-regex@^1.1.0:
3411
  version "1.1.0"
3492
  version "1.1.0"
3412
  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
3493
  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
Lines 3668-3673 string-width@^3.0.0, string-width@^3.1.0: Link Here
3668
    is-fullwidth-code-point "^2.0.0"
3749
    is-fullwidth-code-point "^2.0.0"
3669
    strip-ansi "^5.1.0"
3750
    strip-ansi "^5.1.0"
3670
3751
3752
string_decoder@^1.1.1:
3753
  version "1.3.0"
3754
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
3755
  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
3756
  dependencies:
3757
    safe-buffer "~5.2.0"
3758
3671
string_decoder@~0.10.x:
3759
string_decoder@~0.10.x:
3672
  version "0.10.31"
3760
  version "0.10.31"
3673
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
3761
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
Lines 3790-3795 through2@2.X, through2@^2.0.0, through2@^2.0.3, through2@^2.0.5, through2@~2.0.0 Link Here
3790
    readable-stream "~2.3.6"
3878
    readable-stream "~2.3.6"
3791
    xtend "~4.0.1"
3879
    xtend "~4.0.1"
3792
3880
3881
through2@^0.6.5:
3882
  version "0.6.5"
3883
  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
3884
  integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
3885
  dependencies:
3886
    readable-stream ">=1.0.33-1 <1.1.0-0"
3887
    xtend ">=4.0.0 <4.1.0-0"
3888
3889
through2@^3.0.1:
3890
  version "3.0.1"
3891
  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a"
3892
  integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==
3893
  dependencies:
3894
    readable-stream "2 || 3"
3895
3793
time-stamp@^1.0.0:
3896
time-stamp@^1.0.0:
3794
  version "1.1.0"
3897
  version "1.1.0"
3795
  resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
3898
  resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
Lines 3974-3980 use@^3.1.0: Link Here
3974
  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
4077
  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
3975
  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
4078
  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
3976
4079
3977
util-deprecate@~1.0.1:
4080
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
3978
  version "1.0.2"
4081
  version "1.0.2"
3979
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
4082
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
3980
  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
4083
  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
Lines 4070-4076 vinyl@^0.5.0: Link Here
4070
    clone-stats "^0.0.1"
4173
    clone-stats "^0.0.1"
4071
    replace-ext "0.0.1"
4174
    replace-ext "0.0.1"
4072
4175
4073
vinyl@^2.0.0:
4176
vinyl@^2.0.0, vinyl@^2.2.0:
4074
  version "2.2.0"
4177
  version "2.2.0"
4075
  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
4178
  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
4076
  integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
4179
  integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
Lines 4133-4139 wrappy@1: Link Here
4133
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
4236
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
4134
  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
4237
  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
4135
4238
4136
xtend@~4.0.0, xtend@~4.0.1:
4239
"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1:
4137
  version "4.0.2"
4240
  version "4.0.2"
4138
  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
4241
  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
4139
  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
4242
  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
4140
- 

Return to bug 25067