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-12 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 sourcemaps = require('gulp-sourcemaps');
14
const sourcemaps = require('gulp-sourcemaps');
9
const autoprefixer = require('gulp-autoprefixer');
15
const autoprefixer = require('gulp-autoprefixer');
16
const concatPo = require('gulp-concat-po');
17
const exec = require('gulp-exec');
18
const merge = require('merge-stream');
19
const through2 = require('through2');
20
const Vinyl = require('vinyl');
10
const args = require('minimist')(process.argv.slice(2));
21
const args = require('minimist')(process.argv.slice(2));
11
22
12
const STAFF_JS_BASE = "koha-tmpl/intranet-tmpl/prog/js";
23
const STAFF_JS_BASE = "koha-tmpl/intranet-tmpl/prog/js";
Lines 46-53 function build() { Link Here
46
        .pipe(dest(css_base));
57
        .pipe(dest(css_base));
47
}
58
}
48
59
60
const poTasks = {
61
    'marc-MARC21': {
62
        extract: po_extract_marc_marc21,
63
        create: po_create_marc_marc21,
64
        update: po_update_marc_marc21,
65
    },
66
    'marc-NORMARC': {
67
        extract: po_extract_marc_normarc,
68
        create: po_create_marc_normarc,
69
        update: po_update_marc_normarc,
70
    },
71
    'marc-UNIMARC': {
72
        extract: po_extract_marc_unimarc,
73
        create: po_create_marc_unimarc,
74
        update: po_update_marc_unimarc,
75
    },
76
    'staff-prog': {
77
        extract: po_extract_staff,
78
        create: po_create_staff,
79
        update: po_update_staff,
80
    },
81
    'opac-bootstrap': {
82
        extract: po_extract_opac,
83
        create: po_create_opac,
84
        update: po_update_opac,
85
    },
86
    'pref': {
87
        extract: po_extract_pref,
88
        create: po_create_pref,
89
        update: po_update_pref,
90
    },
91
    'messages': {
92
        extract: po_extract_messages,
93
        create: po_create_messages,
94
        update: po_update_messages,
95
    },
96
    'messages-js': {
97
        extract: po_extract_messages_js,
98
        create: po_create_messages_js,
99
        update: po_update_messages_js,
100
    },
101
    'installer': {
102
        extract: po_extract_installer,
103
        create: po_create_installer,
104
        update: po_update_installer,
105
    },
106
    'installer-MARC21': {
107
        extract: po_extract_installer_marc21,
108
        create: po_create_installer_marc21,
109
        update: po_update_installer_marc21,
110
    },
111
};
112
113
const poTypes = Object.keys(poTasks);
114
115
function po_extract_marc (type) {
116
    return src(`koha-tmpl/*-tmpl/*/en/**/*${type}*`, { read: false, nocase: true })
117
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', `Koha-marc-${type}.pot`))
118
        .pipe(dest('misc/translator'))
119
}
120
121
function po_extract_marc_marc21 ()  { return po_extract_marc('MARC21') }
122
function po_extract_marc_normarc () { return po_extract_marc('NORMARC') }
123
function po_extract_marc_unimarc () { return po_extract_marc('UNIMARC') }
124
125
function po_extract_staff () {
126
    const globs = [
127
        'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
128
        'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
129
        '!koha-tmpl/intranet-tmpl/prog/en/**/*MARC21*',
130
        '!koha-tmpl/intranet-tmpl/prog/en/**/*NORMARC*',
131
        '!koha-tmpl/intranet-tmpl/prog/en/**/*UNIMARC*',
132
    ];
133
134
    return src(globs, { read: false, nocase: true })
135
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', 'Koha-staff-prog.pot'))
136
        .pipe(dest('misc/translator'))
137
}
138
139
function po_extract_opac () {
140
    const globs = [
141
        'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
142
        'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
143
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*MARC21*',
144
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*NORMARC*',
145
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*UNIMARC*',
146
    ];
147
148
    return src(globs, { read: false, nocase: true })
149
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', 'Koha-opac-bootstrap.pot'))
150
        .pipe(dest('misc/translator'))
151
}
152
153
const xgettext_options = '--from-code=UTF-8 --package-name Koha '
154
    + '--package-version= -k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 '
155
    + '-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ '
156
    + '-kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3 --force-po';
157
158
function po_extract_messages_js () {
159
    const globs = [
160
        'koha-tmpl/intranet-tmpl/prog/js/**/*.js',
161
        'koha-tmpl/opac-tmpl/bootstrap/js/**/*.js',
162
    ];
163
164
    return src(globs, { read: false, nocase: true })
165
        .pipe(xgettext(`xgettext -L JavaScript ${xgettext_options}`, 'Koha-messages-js.pot'))
166
        .pipe(dest('misc/translator'))
167
}
168
169
function po_extract_messages () {
170
    const perlStream = src(['**/*.pl', '**/*.pm'], { read: false, nocase: true })
171
        .pipe(xgettext(`xgettext -L Perl ${xgettext_options}`, 'Koha-perl.pot'))
172
173
    const ttStream = src([
174
            'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
175
            'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
176
            'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
177
            'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
178
        ], { read: false, nocase: true })
179
        .pipe(xgettext('misc/translator/xgettext-tt2 --from-code=UTF-8', 'Koha-tt.pot'))
180
181
    const headers = {
182
        'Project-Id-Version': 'Koha',
183
        'Content-Type': 'text/plain; charset=UTF-8',
184
    };
185
186
    return merge(perlStream, ttStream)
187
        .pipe(concatPo('Koha-messages.pot', { headers }))
188
        .pipe(dest('misc/translator'))
189
}
190
191
function po_extract_pref () {
192
    return src('koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/*.pref', { read: false })
193
        .pipe(xgettext('misc/translator/xgettext-pref', 'Koha-pref.pot'))
194
        .pipe(dest('misc/translator'))
195
}
196
197
function po_extract_installer () {
198
    const globs = [
199
        'installer/data/mysql/en/mandatory/*.yml',
200
        'installer/data/mysql/en/optional/*.yml',
201
    ];
202
203
    return src(globs, { read: false, nocase: true })
204
        .pipe(xgettext('misc/translator/xgettext-installer', 'Koha-installer.pot'))
205
        .pipe(dest('misc/translator'))
206
}
207
208
function po_extract_installer_marc (type) {
209
    const globs = `installer/data/mysql/en/marcflavour/${type}/**/*.yml`;
210
211
    return src(globs, { read: false, nocase: true })
212
        .pipe(xgettext('misc/translator/xgettext-installer', `Koha-installer-${type}.pot`))
213
        .pipe(dest('misc/translator'))
214
}
215
216
function po_extract_installer_marc21 ()  { return po_extract_installer_marc('MARC21') }
217
218
function po_create_type (type) {
219
    const access = util.promisify(fs.access);
220
    const exec = util.promisify(child_process.exec);
221
222
    const languages = getLanguages();
223
    const promises = [];
224
    for (const language of languages) {
225
        const locale = language.split('-').filter(s => s.length !== 4).join('_');
226
        const po = `misc/translator/po/${language}-${type}.po`;
227
        const pot = `misc/translator/Koha-${type}.pot`;
228
229
        const promise = access(po)
230
            .catch(() => exec(`msginit -o ${po} -i ${pot} -l ${locale} --no-translator`))
231
        promises.push(promise);
232
    }
233
234
    return Promise.all(promises);
235
}
236
237
function po_create_marc_marc21 ()       { return po_create_type('marc-MARC21') }
238
function po_create_marc_normarc ()      { return po_create_type('marc-NORMARC') }
239
function po_create_marc_unimarc ()      { return po_create_type('marc-UNIMARC') }
240
function po_create_staff ()             { return po_create_type('staff-prog') }
241
function po_create_opac ()              { return po_create_type('opac-bootstrap') }
242
function po_create_pref ()              { return po_create_type('pref') }
243
function po_create_messages ()          { return po_create_type('messages') }
244
function po_create_messages_js ()       { return po_create_type('messages-js') }
245
function po_create_installer ()         { return po_create_type('installer') }
246
function po_create_installer_marc21 ()  { return po_create_type('installer-MARC21') }
247
248
function po_update_type (type) {
249
    const msgmerge_opts = '--backup=off --quiet --sort-output --update';
250
    const cmd = `msgmerge ${msgmerge_opts} <%= file.path %> misc/translator/Koha-${type}.pot`;
251
    const languages = getLanguages();
252
    const globs = languages.map(language => `misc/translator/po/${language}-${type}.po`);
253
254
    return src(globs)
255
        .pipe(exec(cmd, { continueOnError: true }))
256
        .pipe(exec.reporter({ err: false, stdout: false }))
257
}
258
259
function po_update_marc_marc21 ()       { return po_update_type('marc-MARC21') }
260
function po_update_marc_normarc ()      { return po_update_type('marc-NORMARC') }
261
function po_update_marc_unimarc ()      { return po_update_type('marc-UNIMARC') }
262
function po_update_staff ()             { return po_update_type('staff-prog') }
263
function po_update_opac ()              { return po_update_type('opac-bootstrap') }
264
function po_update_pref ()              { return po_update_type('pref') }
265
function po_update_messages ()          { return po_update_type('messages') }
266
function po_update_messages_js ()       { return po_update_type('messages-js') }
267
function po_update_installer ()         { return po_update_type('installer') }
268
function po_update_installer_marc21 ()  { return po_update_type('installer-MARC21') }
269
270
/**
271
 * Gulp plugin that executes xgettext-like command `cmd` on all files given as
272
 * input, and then outputs the result as a POT file named `filename`.
273
 * `cmd` should accept -o and -f options
274
 */
275
function xgettext (cmd, filename) {
276
    const filenames = [];
277
278
    function transform (file, encoding, callback) {
279
        filenames.push(path.relative(file.cwd, file.path));
280
        callback();
281
    }
282
283
    function flush (callback) {
284
        fs.mkdtemp(path.join(os.tmpdir(), 'koha-'), (err, folder) => {
285
            const outputFilename = path.join(folder, filename);
286
            const filesFilename = path.join(folder, 'files');
287
            fs.writeFile(filesFilename, filenames.join(os.EOL), err => {
288
                if (err) return callback(err);
289
290
                const command = `${cmd} -o ${outputFilename} -f ${filesFilename}`;
291
                child_process.exec(command, err => {
292
                    if (err) return callback(err);
293
294
                    fs.readFile(outputFilename, (err, data) => {
295
                        if (err) return callback(err);
296
297
                        const file = new Vinyl();
298
                        file.path = path.join(file.base, filename);
299
                        file.contents = data;
300
                        callback(null, file);
301
                    });
302
                });
303
            });
304
        })
305
    }
306
307
    return through2.obj(transform, flush);
308
}
309
310
/**
311
 * Return languages selected for PO-related tasks
312
 *
313
 * This can be either languages given on command-line with --lang option, or
314
 * all the languages found in misc/translator/po otherwise
315
 */
316
function getLanguages () {
317
    if (Array.isArray(args.lang)) {
318
        return args.lang;
319
    }
320
321
    if (args.lang) {
322
        return [args.lang];
323
    }
324
325
    const filenames = fs.readdirSync('misc/translator/po')
326
        .filter(filename => filename.endsWith('.po'))
327
        .filter(filename => !filename.startsWith('.'))
328
329
    const re = new RegExp('-(' + poTypes.join('|') + ')\.po$');
330
    languages = filenames.map(filename => filename.replace(re, ''))
331
332
    return Array.from(new Set(languages));
333
}
334
49
exports.build = build;
335
exports.build = build;
50
exports.css = css;
336
exports.css = css;
337
338
exports['po:create'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].create)));
339
exports['po:update'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].update)));
340
exports['po:extract'] = parallel(...poTypes.map(type => poTasks[type].extract));
341
51
exports.default = function () {
342
exports.default = function () {
52
    watch(css_base + "/src/**/*.scss", series('css'));
343
    watch(css_base + "/src/**/*.scss", series('css'));
53
}
344
}
(-)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 9-17 Link Here
9
  "dependencies": {
9
  "dependencies": {
10
    "gulp": "^4.0.2",
10
    "gulp": "^4.0.2",
11
    "gulp-autoprefixer": "^4.0.0",
11
    "gulp-autoprefixer": "^4.0.0",
12
    "gulp-concat-po": "^1.0.0",
12
    "gulp-cssnano": "^2.1.2",
13
    "gulp-cssnano": "^2.1.2",
14
    "gulp-exec": "^4.0.0",
13
    "gulp-sass": "^3.1.0",
15
    "gulp-sass": "^3.1.0",
14
    "gulp-sourcemaps": "^2.6.1",
16
    "gulp-sourcemaps": "^2.6.1",
17
    "merge-stream": "^2.0.0",
15
    "minimist": "^1.2.5"
18
    "minimist": "^1.2.5"
16
  },
19
  },
17
  "devDependencies": {},
20
  "devDependencies": {},
(-)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 (-8 / +105 lines)
Lines 1468-1473 gulp-cli@^2.2.0: Link Here
1468
    v8flags "^3.2.0"
1468
    v8flags "^3.2.0"
1469
    yargs "^7.1.0"
1469
    yargs "^7.1.0"
1470
1470
1471
gulp-concat-po@^1.0.0:
1472
  version "1.0.0"
1473
  resolved "https://registry.yarnpkg.com/gulp-concat-po/-/gulp-concat-po-1.0.0.tgz#2fe7b2c12e45a566238e228f63396838013770ae"
1474
  integrity sha512-hFDZrUJcpw10TW3BfptL5W2FV/aMo3M+vxz9YQV4nlMBDAi8gs9/yZYZcYMYfl5XKhjpebSef8nyruoWdlX8Hw==
1475
  dependencies:
1476
    lodash.find "^4.6.0"
1477
    lodash.merge "^4.6.2"
1478
    lodash.uniq "^4.5.0"
1479
    plugin-error "^1.0.1"
1480
    pofile "^1.1.0"
1481
    through2 "^0.6.5"
1482
    vinyl "^2.2.0"
1483
1471
gulp-cssnano@^2.1.2:
1484
gulp-cssnano@^2.1.2:
1472
  version "2.1.3"
1485
  version "2.1.3"
1473
  resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
1486
  resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
Lines 1479-1484 gulp-cssnano@^2.1.2: Link Here
1479
    plugin-error "^1.0.1"
1492
    plugin-error "^1.0.1"
1480
    vinyl-sourcemaps-apply "^0.2.1"
1493
    vinyl-sourcemaps-apply "^0.2.1"
1481
1494
1495
gulp-exec@^4.0.0:
1496
  version "4.0.0"
1497
  resolved "https://registry.yarnpkg.com/gulp-exec/-/gulp-exec-4.0.0.tgz#4b6b67be0200d620143f3198a64257b68b146bb6"
1498
  integrity sha512-A9JvTyB3P4huusd/43bTr6SDg3MqBxL9AQbLnsKSO6/91wVkHfxgeJZlgDMkqK8sMel4so8wcko4SZOeB1UCgA==
1499
  dependencies:
1500
    lodash.template "^4.4.0"
1501
    plugin-error "^1.0.1"
1502
    through2 "^3.0.1"
1503
1482
gulp-sass@^3.1.0:
1504
gulp-sass@^3.1.0:
1483
  version "3.2.1"
1505
  version "3.2.1"
1484
  resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-3.2.1.tgz#2e3688a96fd8be1c0c01340750c191b2e79fab94"
1506
  resolved "https://registry.yarnpkg.com/gulp-sass/-/gulp-sass-3.2.1.tgz#2e3688a96fd8be1c0c01340750c191b2e79fab94"
Lines 2130-2135 lodash.escape@^3.0.0: Link Here
2130
  dependencies:
2152
  dependencies:
2131
    lodash._root "^3.0.0"
2153
    lodash._root "^3.0.0"
2132
2154
2155
lodash.find@^4.6.0:
2156
  version "4.6.0"
2157
  resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
2158
  integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=
2159
2133
lodash.isarguments@^3.0.0:
2160
lodash.isarguments@^3.0.0:
2134
  version "3.1.0"
2161
  version "3.1.0"
2135
  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
2162
  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
Lines 2154-2159 lodash.memoize@^4.1.2: Link Here
2154
  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
2181
  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
2155
  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
2182
  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
2156
2183
2184
lodash.merge@^4.6.2:
2185
  version "4.6.2"
2186
  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
2187
  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
2188
2157
lodash.restparam@^3.0.0:
2189
lodash.restparam@^3.0.0:
2158
  version "3.6.1"
2190
  version "3.6.1"
2159
  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
2191
  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
Lines 2174-2179 lodash.template@^3.0.0: Link Here
2174
    lodash.restparam "^3.0.0"
2206
    lodash.restparam "^3.0.0"
2175
    lodash.templatesettings "^3.0.0"
2207
    lodash.templatesettings "^3.0.0"
2176
2208
2209
lodash.template@^4.4.0:
2210
  version "4.5.0"
2211
  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
2212
  integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
2213
  dependencies:
2214
    lodash._reinterpolate "^3.0.0"
2215
    lodash.templatesettings "^4.0.0"
2216
2177
lodash.templatesettings@^3.0.0:
2217
lodash.templatesettings@^3.0.0:
2178
  version "3.1.1"
2218
  version "3.1.1"
2179
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
2219
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
Lines 2182-2187 lodash.templatesettings@^3.0.0: Link Here
2182
    lodash._reinterpolate "^3.0.0"
2222
    lodash._reinterpolate "^3.0.0"
2183
    lodash.escape "^3.0.0"
2223
    lodash.escape "^3.0.0"
2184
2224
2225
lodash.templatesettings@^4.0.0:
2226
  version "4.2.0"
2227
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
2228
  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
2229
  dependencies:
2230
    lodash._reinterpolate "^3.0.0"
2231
2185
lodash.uniq@^4.5.0:
2232
lodash.uniq@^4.5.0:
2186
  version "4.5.0"
2233
  version "4.5.0"
2187
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
2234
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
Lines 2284-2289 meow@^3.7.0: Link Here
2284
    redent "^1.0.0"
2331
    redent "^1.0.0"
2285
    trim-newlines "^1.0.0"
2332
    trim-newlines "^1.0.0"
2286
2333
2334
merge-stream@^2.0.0:
2335
  version "2.0.0"
2336
  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
2337
  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
2338
2287
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
2339
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
2288
  version "3.1.10"
2340
  version "3.1.10"
2289
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
2341
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
Lines 2753-2758 plugin-error@^1.0.1: Link Here
2753
    arr-union "^3.1.0"
2805
    arr-union "^3.1.0"
2754
    extend-shallow "^3.0.2"
2806
    extend-shallow "^3.0.2"
2755
2807
2808
pofile@^1.1.0:
2809
  version "1.1.0"
2810
  resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.1.0.tgz#9ce84bbef5043ceb4f19bdc3520d85778fad4f94"
2811
  integrity sha512-6XYcNkXWGiJ2CVXogTP7uJ6ZXQCldYLZc16wgRp8tqRaBTTyIfF+TUT3EQJPXTLAT7OTPpTAoaFdoXKfaTRU1w==
2812
2756
posix-character-classes@^0.1.0:
2813
posix-character-classes@^0.1.0:
2757
  version "0.1.1"
2814
  version "0.1.1"
2758
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
2815
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
Lines 3093-3098 read-pkg@^1.0.0: Link Here
3093
    normalize-package-data "^2.3.2"
3150
    normalize-package-data "^2.3.2"
3094
    path-type "^1.0.0"
3151
    path-type "^1.0.0"
3095
3152
3153
"readable-stream@2 || 3":
3154
  version "3.6.0"
3155
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
3156
  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
3157
  dependencies:
3158
    inherits "^2.0.3"
3159
    string_decoder "^1.1.1"
3160
    util-deprecate "^1.0.1"
3161
3162
"readable-stream@>=1.0.33-1 <1.1.0-0":
3163
  version "1.0.34"
3164
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
3165
  integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
3166
  dependencies:
3167
    core-util-is "~1.0.0"
3168
    inherits "~2.0.1"
3169
    isarray "0.0.1"
3170
    string_decoder "~0.10.x"
3171
3096
readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, 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:
3172
readable-stream@^2.0.0, readable-stream@^2.0.2, readable-stream@^2.0.5, 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:
3097
  version "2.3.7"
3173
  version "2.3.7"
3098
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
3174
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
Lines 3222-3230 replace-ext@0.0.1: Link Here
3222
  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
3298
  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
3223
3299
3224
replace-ext@^1.0.0:
3300
replace-ext@^1.0.0:
3225
  version "1.0.1"
3301
  version "1.0.0"
3226
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a"
3302
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
3227
  integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==
3303
  integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
3228
3304
3229
replace-homedir@^1.0.0:
3305
replace-homedir@^1.0.0:
3230
  version "1.0.0"
3306
  version "1.0.0"
Lines 3317-3323 rimraf@2: Link Here
3317
  dependencies:
3393
  dependencies:
3318
    glob "^7.1.3"
3394
    glob "^7.1.3"
3319
3395
3320
safe-buffer@^5.0.1, safe-buffer@^5.1.2:
3396
safe-buffer@^5.0.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0:
3321
  version "5.2.0"
3397
  version "5.2.0"
3322
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
3398
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
3323
  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
3399
  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
Lines 3595-3600 string-width@^1.0.1, string-width@^1.0.2: Link Here
3595
    is-fullwidth-code-point "^2.0.0"
3671
    is-fullwidth-code-point "^2.0.0"
3596
    strip-ansi "^4.0.0"
3672
    strip-ansi "^4.0.0"
3597
3673
3674
string_decoder@^1.1.1:
3675
  version "1.3.0"
3676
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
3677
  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
3678
  dependencies:
3679
    safe-buffer "~5.2.0"
3680
3598
string_decoder@~0.10.x:
3681
string_decoder@~0.10.x:
3599
  version "0.10.31"
3682
  version "0.10.31"
3600
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
3683
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
Lines 3705-3710 through2@2.X, through2@^2.0.0, through2@^2.0.3, through2@~2.0.0: Link Here
3705
    readable-stream "~2.3.6"
3788
    readable-stream "~2.3.6"
3706
    xtend "~4.0.1"
3789
    xtend "~4.0.1"
3707
3790
3791
through2@^0.6.5:
3792
  version "0.6.5"
3793
  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
3794
  integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
3795
  dependencies:
3796
    readable-stream ">=1.0.33-1 <1.1.0-0"
3797
    xtend ">=4.0.0 <4.1.0-0"
3798
3799
through2@^3.0.1:
3800
  version "3.0.1"
3801
  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a"
3802
  integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==
3803
  dependencies:
3804
    readable-stream "2 || 3"
3805
3708
time-stamp@^1.0.0:
3806
time-stamp@^1.0.0:
3709
  version "1.1.0"
3807
  version "1.1.0"
3710
  resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
3808
  resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
Lines 3889-3895 use@^3.1.0: Link Here
3889
  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
3987
  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
3890
  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
3988
  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
3891
3989
3892
util-deprecate@~1.0.1:
3990
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
3893
  version "1.0.2"
3991
  version "1.0.2"
3894
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
3992
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
3895
  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
3993
  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
Lines 3985-3991 vinyl@^0.5.0: Link Here
3985
    clone-stats "^0.0.1"
4083
    clone-stats "^0.0.1"
3986
    replace-ext "0.0.1"
4084
    replace-ext "0.0.1"
3987
4085
3988
vinyl@^2.0.0:
4086
vinyl@^2.0.0, vinyl@^2.2.0:
3989
  version "2.2.0"
4087
  version "2.2.0"
3990
  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
4088
  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
3991
  integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
4089
  integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
Lines 4034-4040 wrappy@1: Link Here
4034
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
4132
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
4035
  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
4133
  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
4036
4134
4037
xtend@~4.0.0, xtend@~4.0.1:
4135
"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1:
4038
  version "4.0.2"
4136
  version "4.0.2"
4039
  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
4137
  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
4040
  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
4138
  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
4041
- 

Return to bug 25067