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 / +301 lines)
Lines 1-13 Link Here
1
/* eslint-env node */
1
/* eslint-env node */
2
/* eslint no-console:"off" */
2
/* eslint no-console:"off" */
3
3
4
const { dest, series, src, watch } = require('gulp');
4
const { dest, parallel, series, src, watch } = require('gulp');
5
6
const child_process = require('child_process');
7
const fs = require('fs');
8
const os = require('os');
9
const path = require('path');
10
const util = require('util');
5
11
6
const sass = require("gulp-sass");
12
const sass = require("gulp-sass");
7
const cssnano = require("gulp-cssnano");
13
const cssnano = require("gulp-cssnano");
8
const rtlcss = require('gulp-rtlcss');
14
const rtlcss = require('gulp-rtlcss');
9
const sourcemaps = require('gulp-sourcemaps');
15
const sourcemaps = require('gulp-sourcemaps');
10
const autoprefixer = require('gulp-autoprefixer');
16
const autoprefixer = require('gulp-autoprefixer');
17
const concatPo = require('gulp-concat-po');
18
const exec = require('gulp-exec');
19
const merge = require('merge-stream');
20
const through2 = require('through2');
21
const Vinyl = require('vinyl');
11
const args = require('minimist')(process.argv.slice(2));
22
const args = require('minimist')(process.argv.slice(2));
12
const rename = require('gulp-rename');
23
const rename = require('gulp-rename');
13
24
Lines 62-69 function build() { Link Here
62
        .pipe(dest(css_base));
73
        .pipe(dest(css_base));
63
}
74
}
64
75
76
const poTasks = {
77
    'marc-MARC21': {
78
        extract: po_extract_marc_marc21,
79
        create: po_create_marc_marc21,
80
        update: po_update_marc_marc21,
81
    },
82
    'marc-NORMARC': {
83
        extract: po_extract_marc_normarc,
84
        create: po_create_marc_normarc,
85
        update: po_update_marc_normarc,
86
    },
87
    'marc-UNIMARC': {
88
        extract: po_extract_marc_unimarc,
89
        create: po_create_marc_unimarc,
90
        update: po_update_marc_unimarc,
91
    },
92
    'staff-prog': {
93
        extract: po_extract_staff,
94
        create: po_create_staff,
95
        update: po_update_staff,
96
    },
97
    'opac-bootstrap': {
98
        extract: po_extract_opac,
99
        create: po_create_opac,
100
        update: po_update_opac,
101
    },
102
    'pref': {
103
        extract: po_extract_pref,
104
        create: po_create_pref,
105
        update: po_update_pref,
106
    },
107
    'messages': {
108
        extract: po_extract_messages,
109
        create: po_create_messages,
110
        update: po_update_messages,
111
    },
112
    'messages-js': {
113
        extract: po_extract_messages_js,
114
        create: po_create_messages_js,
115
        update: po_update_messages_js,
116
    },
117
    'installer': {
118
        extract: po_extract_installer,
119
        create: po_create_installer,
120
        update: po_update_installer,
121
    },
122
    'installer-MARC21': {
123
        extract: po_extract_installer_marc21,
124
        create: po_create_installer_marc21,
125
        update: po_update_installer_marc21,
126
    },
127
};
128
129
const poTypes = Object.keys(poTasks);
130
131
function po_extract_marc (type) {
132
    return src(`koha-tmpl/*-tmpl/*/en/**/*${type}*`, { read: false, nocase: true })
133
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', `Koha-marc-${type}.pot`))
134
        .pipe(dest('misc/translator'))
135
}
136
137
function po_extract_marc_marc21 ()  { return po_extract_marc('MARC21') }
138
function po_extract_marc_normarc () { return po_extract_marc('NORMARC') }
139
function po_extract_marc_unimarc () { return po_extract_marc('UNIMARC') }
140
141
function po_extract_staff () {
142
    const globs = [
143
        'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
144
        'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
145
        'koha-tmpl/intranet-tmpl/prog/en/xslt/*.xsl',
146
        'koha-tmpl/intranet-tmpl/prog/en/columns.def',
147
        '!koha-tmpl/intranet-tmpl/prog/en/**/*MARC21*',
148
        '!koha-tmpl/intranet-tmpl/prog/en/**/*NORMARC*',
149
        '!koha-tmpl/intranet-tmpl/prog/en/**/*UNIMARC*',
150
        '!koha-tmpl/intranet-tmpl/prog/en/**/*marc21*',
151
        '!koha-tmpl/intranet-tmpl/prog/en/**/*normarc*',
152
        '!koha-tmpl/intranet-tmpl/prog/en/**/*unimarc*',
153
    ];
154
155
    return src(globs, { read: false, nocase: true })
156
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', 'Koha-staff-prog.pot'))
157
        .pipe(dest('misc/translator'))
158
}
159
160
function po_extract_opac () {
161
    const globs = [
162
        'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
163
        'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
164
        'koha-tmpl/opac-tmpl/bootstrap/en/xslt/*.xsl',
165
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*MARC21*',
166
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*NORMARC*',
167
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*UNIMARC*',
168
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*marc21*',
169
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*normarc*',
170
        '!koha-tmpl/opac-tmpl/bootstrap/en/**/*unimarc*',
171
    ];
172
173
    return src(globs, { read: false, nocase: true })
174
        .pipe(xgettext('misc/translator/xgettext.pl --charset=UTF-8 -s', 'Koha-opac-bootstrap.pot'))
175
        .pipe(dest('misc/translator'))
176
}
177
178
const xgettext_options = '--from-code=UTF-8 --package-name Koha '
179
    + '--package-version= -k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 '
180
    + '-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ '
181
    + '-kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3 --force-po';
182
183
function po_extract_messages_js () {
184
    const globs = [
185
        'koha-tmpl/intranet-tmpl/prog/js/**/*.js',
186
        'koha-tmpl/opac-tmpl/bootstrap/js/**/*.js',
187
    ];
188
189
    return src(globs, { read: false, nocase: true })
190
        .pipe(xgettext(`xgettext -L JavaScript ${xgettext_options}`, 'Koha-messages-js.pot'))
191
        .pipe(dest('misc/translator'))
192
}
193
194
function po_extract_messages () {
195
    const perlStream = src(['**/*.pl', '**/*.pm'], { read: false, nocase: true })
196
        .pipe(xgettext(`xgettext -L Perl ${xgettext_options}`, 'Koha-perl.pot'))
197
198
    const ttStream = src([
199
            'koha-tmpl/intranet-tmpl/prog/en/**/*.tt',
200
            'koha-tmpl/intranet-tmpl/prog/en/**/*.inc',
201
            'koha-tmpl/opac-tmpl/bootstrap/en/**/*.tt',
202
            'koha-tmpl/opac-tmpl/bootstrap/en/**/*.inc',
203
        ], { read: false, nocase: true })
204
        .pipe(xgettext('misc/translator/xgettext-tt2 --from-code=UTF-8', 'Koha-tt.pot'))
205
206
    const headers = {
207
        'Project-Id-Version': 'Koha',
208
        'Content-Type': 'text/plain; charset=UTF-8',
209
    };
210
211
    return merge(perlStream, ttStream)
212
        .pipe(concatPo('Koha-messages.pot', { headers }))
213
        .pipe(dest('misc/translator'))
214
}
215
216
function po_extract_pref () {
217
    return src('koha-tmpl/intranet-tmpl/prog/en/modules/admin/preferences/*.pref', { read: false })
218
        .pipe(xgettext('misc/translator/xgettext-pref', 'Koha-pref.pot'))
219
        .pipe(dest('misc/translator'))
220
}
221
222
function po_extract_installer () {
223
    const globs = [
224
        'installer/data/mysql/en/mandatory/*.yml',
225
        'installer/data/mysql/en/optional/*.yml',
226
    ];
227
228
    return src(globs, { read: false, nocase: true })
229
        .pipe(xgettext('misc/translator/xgettext-installer', 'Koha-installer.pot'))
230
        .pipe(dest('misc/translator'))
231
}
232
233
function po_extract_installer_marc (type) {
234
    const globs = `installer/data/mysql/en/marcflavour/${type}/**/*.yml`;
235
236
    return src(globs, { read: false, nocase: true })
237
        .pipe(xgettext('misc/translator/xgettext-installer', `Koha-installer-${type}.pot`))
238
        .pipe(dest('misc/translator'))
239
}
240
241
function po_extract_installer_marc21 ()  { return po_extract_installer_marc('MARC21') }
242
243
function po_create_type (type) {
244
    const access = util.promisify(fs.access);
245
    const exec = util.promisify(child_process.exec);
246
247
    const languages = getLanguages();
248
    const promises = [];
249
    for (const language of languages) {
250
        const locale = language.split('-').filter(s => s.length !== 4).join('_');
251
        const po = `misc/translator/po/${language}-${type}.po`;
252
        const pot = `misc/translator/Koha-${type}.pot`;
253
254
        const promise = access(po)
255
            .catch(() => exec(`msginit -o ${po} -i ${pot} -l ${locale} --no-translator`))
256
        promises.push(promise);
257
    }
258
259
    return Promise.all(promises);
260
}
261
262
function po_create_marc_marc21 ()       { return po_create_type('marc-MARC21') }
263
function po_create_marc_normarc ()      { return po_create_type('marc-NORMARC') }
264
function po_create_marc_unimarc ()      { return po_create_type('marc-UNIMARC') }
265
function po_create_staff ()             { return po_create_type('staff-prog') }
266
function po_create_opac ()              { return po_create_type('opac-bootstrap') }
267
function po_create_pref ()              { return po_create_type('pref') }
268
function po_create_messages ()          { return po_create_type('messages') }
269
function po_create_messages_js ()       { return po_create_type('messages-js') }
270
function po_create_installer ()         { return po_create_type('installer') }
271
function po_create_installer_marc21 ()  { return po_create_type('installer-MARC21') }
272
273
function po_update_type (type) {
274
    const msgmerge_opts = '--backup=off --quiet --sort-output --update';
275
    const cmd = `msgmerge ${msgmerge_opts} <%= file.path %> misc/translator/Koha-${type}.pot`;
276
    const languages = getLanguages();
277
    const globs = languages.map(language => `misc/translator/po/${language}-${type}.po`);
278
279
    return src(globs)
280
        .pipe(exec(cmd, { continueOnError: true }))
281
        .pipe(exec.reporter({ err: false, stdout: false }))
282
}
283
284
function po_update_marc_marc21 ()       { return po_update_type('marc-MARC21') }
285
function po_update_marc_normarc ()      { return po_update_type('marc-NORMARC') }
286
function po_update_marc_unimarc ()      { return po_update_type('marc-UNIMARC') }
287
function po_update_staff ()             { return po_update_type('staff-prog') }
288
function po_update_opac ()              { return po_update_type('opac-bootstrap') }
289
function po_update_pref ()              { return po_update_type('pref') }
290
function po_update_messages ()          { return po_update_type('messages') }
291
function po_update_messages_js ()       { return po_update_type('messages-js') }
292
function po_update_installer ()         { return po_update_type('installer') }
293
function po_update_installer_marc21 ()  { return po_update_type('installer-MARC21') }
294
295
/**
296
 * Gulp plugin that executes xgettext-like command `cmd` on all files given as
297
 * input, and then outputs the result as a POT file named `filename`.
298
 * `cmd` should accept -o and -f options
299
 */
300
function xgettext (cmd, filename) {
301
    const filenames = [];
302
303
    function transform (file, encoding, callback) {
304
        filenames.push(path.relative(file.cwd, file.path));
305
        callback();
306
    }
307
308
    function flush (callback) {
309
        fs.mkdtemp(path.join(os.tmpdir(), 'koha-'), (err, folder) => {
310
            const outputFilename = path.join(folder, filename);
311
            const filesFilename = path.join(folder, 'files');
312
            fs.writeFile(filesFilename, filenames.join(os.EOL), err => {
313
                if (err) return callback(err);
314
315
                const command = `${cmd} -o ${outputFilename} -f ${filesFilename}`;
316
                child_process.exec(command, err => {
317
                    if (err) return callback(err);
318
319
                    fs.readFile(outputFilename, (err, data) => {
320
                        if (err) return callback(err);
321
322
                        const file = new Vinyl();
323
                        file.path = path.join(file.base, filename);
324
                        file.contents = data;
325
                        callback(null, file);
326
                    });
327
                });
328
            });
329
        })
330
    }
331
332
    return through2.obj(transform, flush);
333
}
334
335
/**
336
 * Return languages selected for PO-related tasks
337
 *
338
 * This can be either languages given on command-line with --lang option, or
339
 * all the languages found in misc/translator/po otherwise
340
 */
341
function getLanguages () {
342
    if (Array.isArray(args.lang)) {
343
        return args.lang;
344
    }
345
346
    if (args.lang) {
347
        return [args.lang];
348
    }
349
350
    const filenames = fs.readdirSync('misc/translator/po')
351
        .filter(filename => filename.endsWith('.po'))
352
        .filter(filename => !filename.startsWith('.'))
353
354
    const re = new RegExp('-(' + poTypes.join('|') + ')\.po$');
355
    languages = filenames.map(filename => filename.replace(re, ''))
356
357
    return Array.from(new Set(languages));
358
}
359
65
exports.build = build;
360
exports.build = build;
66
exports.css = css;
361
exports.css = css;
362
363
exports['po:create'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].create)));
364
exports['po:update'] = parallel(...poTypes.map(type => series(poTasks[type].extract, poTasks[type].update)));
365
exports['po:extract'] = parallel(...poTypes.map(type => poTasks[type].extract));
366
67
exports.default = function () {
367
exports.default = function () {
68
    watch(css_base + "/src/**/*.scss", series('css'));
368
    watch(css_base + "/src/**/*.scss", series('css'));
69
}
369
}
(-)a/misc/translator/LangInstaller.pm (-693 / +86 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, $msgid, $default) = @_;
189
152
190
sub po_append {
153
    my $po = $self->{po}->{Locale::PO->quote($msgid)};
191
    my ($self, $id, $comment) = @_;
154
    if ($po) {
192
    my $po = $self->{po};
155
        my $msgstr = Locale::PO->dequote($po->msgstr);
193
    my $p = $po->{$id};
156
        if ($msgstr and length($msgstr) > 0) {
194
    if ( $p ) {
157
            return $msgstr;
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
207
sub add_prefs {
208
    my ($self, $comment, $prefs) = @_;
209
210
    for my $pref ( @$prefs ) {
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
        }
158
        }
233
    }
159
    }
234
}
235
236
237
sub get_trans_text {
238
    my ($self, $id) = @_;
239
160
240
    my $po = $self->{po}->{$id};
161
    return $default;
241
    return unless $po;
242
    return Locale::PO->dequote($po->msgstr);
243
}
162
}
244
163
164
sub get_translated_tab_content {
165
    my ($self, $file, $tab_content) = @_;
245
166
246
sub update_tab_prefs {
167
    if ( ref($tab_content) eq 'ARRAY' ) {
247
    my ($self, $pref, $prefs) = @_;
168
        return $self->get_translated_prefs($file, $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
    }
169
    }
278
}
279
170
171
    my $translated_tab_content = {
172
        map {
173
            my $section = $_;
174
            my $sysprefs = $tab_content->{$section};
175
            my $msgid = sprintf('%s %s', $file, $section);
280
176
281
sub get_po_from_prefs {
177
            $self->get_trans_text($msgid, $section) => $self->get_translated_prefs($file, $sysprefs);
282
    my $self = shift;
178
        } keys %$tab_content
179
    };
283
180
284
    for my $file ( @{$self->{pref_files}} ) {
181
    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
}
182
}
302
183
184
sub get_translated_prefs {
185
    my ($self, $file, $sysprefs) = @_;
303
186
304
sub save_po {
187
    my $translated_prefs = [
305
    my $self = shift;
188
        map {
189
            my ($pref_elt) = grep { ref($_) eq 'HASH' && exists $_->{pref} } @$_;
190
            my $pref_name = $pref_elt ? $pref_elt->{pref} : '';
191
192
            my $translated_syspref = [
193
                map {
194
                    $self->get_translated_pref($file, $pref_name, $_);
195
                } @$_
196
            ];
306
197
307
    # Create file header if it doesn't already exist
198
            $translated_syspref;
308
    my $po = $self->{po};
199
        } @$sysprefs
309
    $po->{''} ||= $default_pref_po_header;
200
    ];
310
201
311
    # Write .po entries into a file put in Koha standard po directory
202
    return $translated_prefs;
312
    Locale::PO->save_file_fromhash( $self->po_filename("-pref.po"), $po );
313
    say "Saved in file: ", $self->po_filename("-pref.po") if $self->{verbose};
314
}
203
}
315
204
205
sub get_translated_pref {
206
    my ($self, $file, $pref_name, $syspref) = @_;
316
207
317
sub get_po_merged_with_en {
208
    unless (ref($syspref)) {
318
    my $self = shift;
209
        $syspref //= '';
319
210
        my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $syspref);
320
    # Get po from current 'en' .pref files
211
        return $self->get_trans_text($msgid, $syspref);
321
    $self->get_po_from_prefs();
212
    }
322
    my $po_current = $self->{po};
323
213
324
    # Get po from previous generation
214
    my $translated_pref = {
325
    my $po_previous = Locale::PO->load_file_ashash( $self->po_filename("-pref.po") );
215
        map {
216
            my $key = $_;
217
            my $value = $syspref->{$key};
326
218
327
    for my $id ( keys %$po_current ) {
219
            my $translated_value = $value;
328
        my $po =  $po_previous->{Locale::PO->quote($id)};
220
            if (($key eq 'choices' || $key eq 'multiple') && ref($value) eq 'HASH') {
329
        next unless $po;
221
                $translated_value = {
330
        my $text = Locale::PO->dequote( $po->msgstr );
222
                    map {
331
        $po_current->{$id}->msgstr( $text );
223
                        my $msgid = sprintf('%s#%s# %s', $file, $pref_name, $value->{$_});
332
    }
224
                        $_ => $self->get_trans_text($msgid, $value->{$_})
333
}
225
                    } keys %$value
226
                }
227
            }
334
228
229
            $key => $translated_value
230
        } keys %$syspref
231
    };
335
232
336
sub update_prefs {
233
    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
}
234
}
343
235
344
345
sub install_prefs {
236
sub install_prefs {
346
    my $self = shift;
237
    my $self = shift;
347
238
Lines 350-394 sub install_prefs { Link Here
350
        exit;
241
        exit;
351
    }
242
    }
352
243
353
    # Get the language .po file merged with last modified 'en' preferences
244
    $self->{po} = Locale::PO->load_file_ashash($self->po_filename("-pref.po"), 'utf8');
354
    $self->get_po_merged_with_en();
355
245
356
    for my $file ( @{$self->{pref_files}} ) {
246
    for my $file ( @{$self->{pref_files}} ) {
357
        my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
247
        my $pref = LoadFile( $self->{path_pref_en} . "/$file" );
358
        $self->{file} = $file;
248
359
        # First, keys are replaced (tab titles)
249
        my $translated_pref = {
360
        $pref = do {
250
            map {
361
            my %pref = map { 
251
                my $tab = $_;
362
                $self->get_trans_text( $self->{file} ) || $_ => $pref->{$_}
252
                my $tab_content = $pref->{$tab};
363
            } keys %$pref;
253
364
            \%pref;
254
                $self->get_trans_text($file, $tab) => $self->get_translated_tab_content($file, $tab_content);
255
            } keys %$pref
365
        };
256
        };
366
        while ( my ($tab, $tab_content) = each %$pref ) {
257
367
            if ( ref($tab_content) eq 'ARRAY' ) {
258
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";
259
        my $file_trans = $self->{po_path_lang} . "/$file";
389
        print "Write $file\n" if $self->{verbose};
260
        print "Write $file\n" if $self->{verbose};
390
        open my $fh, ">", $file_trans;
261
        DumpFile($file_trans, $translated_pref);
391
        print $fh Dump($pref);
392
    }
262
    }
393
}
263
}
394
264
Lines 429-608 sub install_tmpl { Link Here
429
    }
299
    }
430
}
300
}
431
301
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 {
302
sub translate_yaml {
607
    my $self   = shift;
303
    my $self   = shift;
608
    my $target = shift;
304
    my $target = shift;
Lines 716-750 sub install_installer { Link Here
716
    }
412
    }
717
}
413
}
718
414
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 {
415
sub locale_name {
749
    my $self = shift;
416
    my $self = shift;
750
417
Lines 758-1007 sub locale_name { Link Here
758
    return $locale;
425
    return $locale;
759
}
426
}
760
427
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 {
428
sub install_messages {
1006
    my ($self) = @_;
429
    my ($self) = @_;
1007
430
Lines 1012-1019 sub install_messages { Link Here
1012
    my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
435
    my $js_pofile = "$self->{path_po}/$self->{lang}-messages-js.po";
1013
436
1014
    unless ( -f $pofile && -f $js_pofile ) {
437
    unless ( -f $pofile && -f $js_pofile ) {
1015
        $self->create_messages();
438
        die "PO files for language '$self->{lang}' do not exist";
1016
    }
439
    }
440
1017
    say "Install messages ($locale)" if $self->{verbose};
441
    say "Install messages ($locale)" if $self->{verbose};
1018
    make_path($modir);
442
    make_path($modir);
1019
    system "$self->{msgfmt} -o $mofile $pofile";
443
    system "$self->{msgfmt} -o $mofile $pofile";
Lines 1035-1047 sub install_messages { Link Here
1035
    }
459
    }
1036
}
460
}
1037
461
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 {
462
sub compress {
1046
    my ($self, $files) = @_;
463
    my ($self, $files) = @_;
1047
    my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
464
    my @langs = $self->{lang} ? ($self->{lang}) : $self->get_all_langs();
Lines 1074-1084 sub install { Link Here
1074
    my ($self, $files) = @_;
491
    my ($self, $files) = @_;
1075
    return unless $self->{lang};
492
    return unless $self->{lang};
1076
    $self->uncompress();
493
    $self->uncompress();
1077
    $self->install_tmpl($files) unless $self->{pref_only};
494
1078
    $self->install_prefs();
495
    if ($self->{pref_only}) {
1079
    $self->install_messages();
496
        $self->install_prefs();
1080
    $self->remove_pot();
497
    } else {
1081
    $self->install_installer();
498
        $self->install_tmpl($files);
499
        $self->install_prefs();
500
        $self->install_messages();
501
        $self->install_installer();
502
    }
1082
}
503
}
1083
504
1084
505
Lines 1090-1123 sub get_all_langs { Link Here
1090
    @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
511
    @files = map { $_ =~ s/-pref.(po|po.gz)$//r } @files;
1091
}
512
}
1092
513
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;
514
1;
1122
515
1123
516
(-)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 (-42 / +16 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 71-79 if ( $cmd =~ /^(create|install|update|compress|uncompress)$/ ) { Link Here
71
        $installer->$cmd(\@files);
70
        $installer->$cmd(\@files);
72
    }
71
    }
73
72
74
    Koha::Caches->get_instance()->flush_all if $cmd ne 'update';
73
    Koha::Caches->get_instance()->flush_all;
75
}
74
} elsif ($cmd eq 'create' or $cmd eq 'update') {
76
else {
75
    my $command = "gulp po:$cmd";
76
    $command .= " --silent" if (!$verbose);
77
    $command .= " --lang $lang" if ($lang);
78
79
    if ($verbose) {
80
        print STDERR "Deprecation notice: PO creation and update are now gulp tasks. See docs/development/internationalization.md\n";
81
        print STDERR "Running `$command`\n";
82
    }
83
84
    system($command);
85
} else {
77
    usage();
86
    usage();
78
}
87
}
79
88
Lines 85-96 translate - Handle templates and preferences translation Link Here
85
94
86
=head1 SYNOPSYS
95
=head1 SYNOPSYS
87
96
88
  translate create fr-FR
89
  translate update fr-FR
90
  translate install fr-FR
97
  translate install fr-FR
91
  translate install fr-FR -f search -f memberentry
98
  translate install fr-FR -f search -f memberentry
92
  translate -p install fr-FR
99
  translate -p install fr-FR
93
  translate install
94
  translate compress [fr-FR]
100
  translate compress [fr-FR]
95
  translate uncompress [fr-FR]
101
  translate uncompress [fr-FR]
96
102
Lines 98-104 translate - Handle templates and preferences translation Link Here
98
104
99
In Koha, three categories of information are translated based on standard GNU
105
In Koha, three categories of information are translated based on standard GNU
100
.po files: opac templates pages, intranet templates and system preferences. The
106
.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
107
script is a wrapper. It allows to quickly install .po files for a
102
given language or for all available languages.
108
given language or for all available languages.
103
109
104
=head1 USAGE
110
=head1 USAGE
Lines 107-144 Use the -v or --verbose parameter to make translator more verbose. Link Here
107
113
108
=over
114
=over
109
115
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>
116
=item translate [-p|-f] install F<lang>
143
117
144
Use .po files to translate the english version of templates and preferences files
118
Use .po files to translate the english version of templates and preferences files
(-)a/misc/translator/xgettext-installer (+158 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
=head1 NAME
19
20
xgettext-installer - extract translatable strings from installer YAML files
21
22
=head1 SYNOPSIS
23
24
xgettext-installer [OPTION] [INPUTFILE]...
25
26
=head1 OPTIONS
27
28
=over
29
30
=item B<-f, --files-from=FILE>
31
32
get list of input files from FILE
33
34
=item B<-o, --output=FILE>
35
36
write output to the specified file
37
38
=item B<-h, --help>
39
40
display this help and exit
41
42
=back
43
44
=cut
45
46
use Modern::Perl;
47
48
use Getopt::Long;
49
use Locale::PO;
50
use Pod::Usage;
51
use YAML::Syck qw(LoadFile);
52
53
$YAML::Syck::ImplicitTyping = 1;
54
55
my $output = 'messages.pot';
56
my $files_from;
57
my $help;
58
59
GetOptions(
60
    'output=s' => \$output,
61
    'files-from=s' => \$files_from,
62
    'help' => \$help,
63
) or pod2usage(-verbose => 1, -exitval => 2);
64
65
if ($help) {
66
    pod2usage(-verbose => 1, -exitval => 0);
67
}
68
69
my @files = @ARGV;
70
if ($files_from) {
71
    open(my $fh, '<', $files_from) or die "Cannot open $files_from: $!";
72
    push @files, <$fh>;
73
    chomp @files;
74
    close $fh;
75
}
76
77
my $pot = {
78
    '' => Locale::PO->new(
79
        -msgid  => '',
80
        -msgstr =>
81
            "Project-Id-Version: Koha\n"
82
          . "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
83
          . "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\n"
84
          . "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\n"
85
          . "MIME-Version: 1.0\n"
86
          . "Content-Type: text/plain; charset=UTF-8\n"
87
          . "Content-Transfer-Encoding: 8bit\n"
88
    ),
89
};
90
91
for my $file (@files) {
92
    my $yaml = LoadFile($file);
93
    my @tables = @{ $yaml->{'tables'} };
94
95
    my $tablec = 0;
96
    for my $table (@tables) {
97
        $tablec++;
98
99
        my $table_name = ( keys %$table )[0];
100
        my @translatable = @{ $table->{$table_name}->{translatable} };
101
        my @rows = @{ $table->{$table_name}->{rows} };
102
        my @multiline = @{ $table->{$table_name}->{'multiline'} };
103
104
        my $rowc = 0;
105
        for my $row (@rows) {
106
            $rowc++;
107
108
            for my $field (@translatable) {
109
                if ( @multiline and grep { $_ eq $field } @multiline ) {
110
                    # multiline fields, only notices ATM
111
                    my $mulc;
112
                    foreach my $line ( @{ $row->{$field} } ) {
113
                        $mulc++;
114
115
                        # discard pure html, TT, empty
116
                        next if ( $line =~ /^(\s*<.*?>\s*$|^\s*\[.*?\]\s*|\s*)$/ );
117
118
                        # put placeholders
119
                        $line =~ s/(<<.*?>>|\[\%.*?\%\]|<.*?>)/\%s/g;
120
121
                        # discard non strings
122
                        next if ( $line =~ /^(\s|%s|-|[[:punct:]]|\(|\))*$/ or length($line) < 2 );
123
                        if ( not $pot->{$line} ) {
124
                            my $msg = new Locale::PO(
125
                                -msgid  => $line,
126
                                -msgstr => '',
127
                                -reference => "$file:$table_name:$tablec:row:$rowc:mul:$mulc"
128
                            );
129
                            $pot->{$line} = $msg;
130
                        }
131
                    }
132
                } elsif (defined $row->{$field} && length($row->{$field}) > 1 && !$pot->{ $row->{$field} }) {
133
                    my $msg = new Locale::PO(
134
                        -msgid     => $row->{$field},
135
                        -msgstr    => '',
136
                        -reference => "$file:$table_name:$tablec:row:$rowc"
137
                    );
138
                    $pot->{ $row->{$field} } = $msg;
139
                }
140
            }
141
        }
142
    }
143
144
    my $desccount = 0;
145
    for my $description ( @{ $yaml->{'description'} } ) {
146
        $desccount++;
147
        if ( length($description) > 1 and not $pot->{$description} ) {
148
            my $msg = new Locale::PO(
149
                -msgid     => $description,
150
                -msgstr    => '',
151
                -reference => "$file:description:$desccount"
152
            );
153
            $pot->{$description} = $msg;
154
        }
155
    }
156
}
157
158
Locale::PO->save_file_fromhash($output, $pot);
(-)a/misc/translator/xgettext-pref (+151 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
=head1 NAME
19
20
xgettext-pref - extract translatable strings from system preferences YAML files
21
22
=head1 SYNOPSIS
23
24
xgettext-pref [OPTION] [INPUTFILE]...
25
26
=head1 OPTIONS
27
28
=over
29
30
=item B<-f, --files-from=FILE>
31
32
get list of input files from FILE
33
34
=item B<-o, --output=FILE>
35
36
write output to the specified file
37
38
=item B<-h, --help>
39
40
display this help and exit
41
42
=back
43
44
=cut
45
46
use Modern::Perl;
47
48
use File::Basename;
49
use Getopt::Long;
50
use Locale::PO;
51
use Pod::Usage;
52
use YAML::Syck qw(LoadFile);
53
54
$YAML::Syck::ImplicitTyping = 1;
55
56
my $output = 'messages.pot';
57
my $files_from;
58
my $help;
59
60
GetOptions(
61
    'output=s' => \$output,
62
    'files-from=s' => \$files_from,
63
    'help' => \$help,
64
) or pod2usage(-verbose => 1, -exitval => 2);
65
66
if ($help) {
67
    pod2usage(-verbose => 1, -exitval => 0);
68
}
69
70
my @files = @ARGV;
71
if ($files_from) {
72
    open(my $fh, '<', $files_from) or die "Cannot open $files_from: $!";
73
    push @files, <$fh>;
74
    chomp @files;
75
    close $fh;
76
}
77
78
my $pot = {
79
    '' => Locale::PO->new(
80
        -msgid  => '',
81
        -msgstr => "Project-Id-Version: Koha\n"
82
          . "PO-Revision-Date: YEAR-MO-DA HO:MI +ZONE\n"
83
          . "Last-Translator: FULL NAME <EMAIL\@ADDRESS>\n"
84
          . "Language-Team: Koha Translate List <koha-translate\@lists.koha-community.org>\n"
85
          . "MIME-Version: 1.0\n"
86
          . "Content-Type: text/plain; charset=UTF-8\n"
87
          . "Content-Transfer-Encoding: 8bit\n"
88
    ),
89
};
90
91
for my $file (@files) {
92
    my $pref = LoadFile($file);
93
    while ( my ($tab, $tab_content) = each %$pref ) {
94
        add_po(undef, basename($file));
95
96
        if ( ref($tab_content) eq 'ARRAY' ) {
97
            add_prefs( $file, $tab, $tab_content );
98
        } else {
99
            while ( my ($section, $sysprefs) = each %$tab_content ) {
100
                my $context = "$tab > $section";
101
                my $msgid = sprintf('%s %s', basename($file), $section);
102
                add_po($tab, $msgid);
103
                add_prefs( $file, $context, $sysprefs );
104
            }
105
        }
106
    }
107
}
108
109
Locale::PO->save_file_fromhash($output, $pot);
110
111
sub add_prefs {
112
    my ($file, $context, $prefs) = @_;
113
114
    for my $pref (@$prefs) {
115
        my $pref_name = '';
116
        for my $element (@$pref) {
117
            if ( ref($element) eq 'HASH' ) {
118
                $pref_name = $element->{pref};
119
                last;
120
            }
121
        }
122
        for my $element (@$pref) {
123
            if ( ref($element) eq 'HASH' ) {
124
                while ( my ( $key, $value ) = each(%$element) ) {
125
                    next unless $key eq 'choices' or $key eq 'multiple';
126
                    next unless ref($value) eq 'HASH';
127
                    for my $ckey ( keys %$value ) {
128
                        my $msgid = sprintf('%s#%s# %s', basename($file), $pref_name, $value->{$ckey});
129
                        add_po( "$context > $pref_name", $msgid );
130
                    }
131
                }
132
            }
133
            elsif ($element) {
134
                my $msgid = sprintf('%s#%s# %s', basename($file), $pref_name, $element);
135
                add_po( "$context > $pref_name", $msgid );
136
            }
137
        }
138
    }
139
}
140
141
sub add_po {
142
    my ($comment, $msgid ) = @_;
143
144
    return unless $msgid;
145
146
    $pot->{$msgid} = Locale::PO->new(
147
        -comment   => $comment,
148
        -msgid     => $msgid,
149
        -msgstr    => '',
150
    );
151
}
(-)a/misc/translator/xgettext-tt2 (+56 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
18
use Modern::Perl;
19
20
my $xgettext = Locale::XGettext::TT2::Koha->newFromArgv(\@ARGV);
21
$xgettext->setOption('plug_in', '');
22
$xgettext->run;
23
$xgettext->output;
24
25
package Locale::XGettext::TT2::Koha;
26
27
use parent 'Locale::XGettext::TT2';
28
29
sub defaultKeywords {
30
    return [
31
        't:1',
32
        'tx:1',
33
        'tn:1,2',
34
        'tnx:1,2',
35
        'txn:1,2',
36
        'tp:1c,2',
37
        'tpx:1c,2',
38
        'tnp:1c,2,3',
39
        'tnpx:1c,2,3',
40
    ];
41
}
42
43
sub defaultFlags {
44
    return [
45
        'tx:1:perl-brace-format',
46
        'tnx:1:perl-brace-format',
47
        'tnx:2:perl-brace-format',
48
        'txn:1:perl-brace-format',
49
        'txn:2:perl-brace-format',
50
        'tpx:2:perl-brace-format',
51
        'tnpx:2:perl-brace-format',
52
        'tnpx:3:perl-brace-format',
53
    ],
54
}
55
56
1;
(-)a/misc/translator/xgettext.pl (-1 / +16 lines)
Lines 1-5 Link Here
1
#!/usr/bin/perl
1
#!/usr/bin/perl
2
2
3
# This file is part of Koha.
4
#
5
# Koha is free software; you can redistribute it and/or modify it
6
# under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 3 of the License, or
8
# (at your option) any later version.
9
#
10
# Koha is distributed in the hope that it will be useful, but
11
# WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with Koha; if not, see <http://www.gnu.org/licenses>.
17
3
=head1 NAME
18
=head1 NAME
4
19
5
xgettext.pl - xgettext(1)-like interface for .tt strings extraction
20
xgettext.pl - xgettext(1)-like interface for .tt strings extraction
Lines 173-179 EOF Link Here
173
    print $OUTPUT <<EOF;
188
    print $OUTPUT <<EOF;
174
msgid ""
189
msgid ""
175
msgstr ""
190
msgstr ""
176
"Project-Id-Version: PACKAGE VERSION\\n"
191
"Project-Id-Version: Koha\\n"
177
"POT-Creation-Date: $time_pot\\n"
192
"POT-Creation-Date: $time_pot\\n"
178
"PO-Revision-Date: $time_po\\n"
193
"PO-Revision-Date: $time_po\\n"
179
"Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n"
194
"Last-Translator: FULL NAME <EMAIL\@ADDRESS>\\n"
(-)a/package.json (+3 lines)
Lines 10-20 Link Here
10
    "bootstrap": "^4.5.2",
10
    "bootstrap": "^4.5.2",
11
    "gulp": "^4.0.2",
11
    "gulp": "^4.0.2",
12
    "gulp-autoprefixer": "^4.0.0",
12
    "gulp-autoprefixer": "^4.0.0",
13
    "gulp-concat-po": "^1.0.0",
13
    "gulp-cssnano": "^2.1.2",
14
    "gulp-cssnano": "^2.1.2",
15
    "gulp-exec": "^4.0.0",
14
    "gulp-rename": "^2.0.0",
16
    "gulp-rename": "^2.0.0",
15
    "gulp-rtlcss": "^1.4.1",
17
    "gulp-rtlcss": "^1.4.1",
16
    "gulp-sass": "^3.1.0",
18
    "gulp-sass": "^3.1.0",
17
    "gulp-sourcemaps": "^2.6.1",
19
    "gulp-sourcemaps": "^2.6.1",
20
    "merge-stream": "^2.0.0",
18
    "minimist": "^1.2.5"
21
    "minimist": "^1.2.5"
19
  },
22
  },
20
  "scripts": {
23
  "scripts": {
(-)a/t/LangInstaller.t (-109 lines)
Lines 1-109 Link Here
1
use Modern::Perl;
2
3
use FindBin '$Bin';
4
use lib "$Bin/../misc/translator";
5
6
use Test::More tests => 39;
7
use File::Temp qw(tempdir);
8
use File::Slurp;
9
use Locale::PO;
10
11
use t::lib::Mocks;
12
13
use_ok('LangInstaller');
14
15
my $installer = LangInstaller->new();
16
17
my $tempdir = tempdir(CLEANUP => 0);
18
t::lib::Mocks::mock_config('intrahtdocs', "$Bin/LangInstaller/templates");
19
my @files = ('simple.tt');
20
$installer->extract_messages_from_templates($tempdir, 'intranet', @files);
21
22
my $tempfile = "$tempdir/koha-tmpl/intranet-tmpl/simple.tt";
23
ok(-e $tempfile, 'it has created a temporary file simple.tt');
24
SKIP: {
25
    skip "simple.tt does not exist", 37 unless -e $tempfile;
26
27
    my $output = read_file($tempfile);
28
    my $expected_output = <<'EOF';
29
__('hello');
30
__x('hello {name}');
31
__n('item', 'items');
32
__nx('{count} item', '{count} items');
33
__p('context', 'hello');
34
__px('context', 'hello {name}');
35
__np('context', 'item', 'items');
36
__npx('context', '{count} item', '{count} items');
37
__npx('context', '{count} item', '{count} items');
38
__x('status is {status}');
39
__('active');
40
__('inactive');
41
__('Inside block');
42
EOF
43
44
    is($output, $expected_output, "Output of extract_messages_from_templates is as expected");
45
46
    my $xgettext_cmd = "xgettext -L Perl --from-code=UTF-8 "
47
        . "--package-name=Koha --package-version='' "
48
        . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 "
49
        . "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 "
50
        . "-o $tempdir/Koha.pot -D $tempdir koha-tmpl/intranet-tmpl/simple.tt";
51
52
    system($xgettext_cmd);
53
    my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
54
55
    my @expected = (
56
        {
57
            msgid => '"hello"',
58
        },
59
        {
60
            msgid => '"hello {name}"',
61
        },
62
        {
63
            msgid => '"item"',
64
            msgid_plural => '"items"',
65
        },
66
        {
67
            msgid => '"{count} item"',
68
            msgid_plural => '"{count} items"',
69
        },
70
        {
71
            msgid => '"hello"',
72
            msgctxt => '"context"',
73
        },
74
        {
75
            msgid => '"hello {name}"',
76
            msgctxt => '"context"',
77
        },
78
        {
79
            msgid => '"item"',
80
            msgid_plural => '"items"',
81
            msgctxt => '"context"',
82
        },
83
        {
84
            msgid => '"{count} item"',
85
            msgid_plural => '"{count} items"',
86
            msgctxt => '"context"',
87
        },
88
        {
89
            msgid => '"status is {status}"',
90
        },
91
        {
92
            msgid => '"active"',
93
        },
94
        {
95
            msgid => '"inactive"',
96
        },
97
        {
98
            msgid => '"Inside block"',
99
        },
100
    );
101
102
    for (my $i = 0; $i < @expected; $i++) {
103
        for my $key (qw(msgid msgid_plural msgctxt)) {
104
            my $expected = $expected[$i]->{$key};
105
            my $expected_str = defined $expected ? $expected : 'not defined';
106
            is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
107
        }
108
    }
109
}
(-)a/t/misc/translator/sample.pref (+14 lines)
Line 0 Link Here
1
Section:
2
    Subsection:
3
        -
4
            - pref: SamplePref
5
              choices:
6
                on: Do
7
                off: Do not do
8
            - that thing
9
        -
10
            - pref: MultiplePref
11
              multiple:
12
                foo: Foo ツ
13
                bar: Bar
14
                baz: Baz
(-)a/t/LangInstaller/templates/simple.tt (-1 / +1 lines)
Lines 1-6 Link Here
1
[% USE raw %]
1
[% USE raw %]
2
[% PROCESS 'i18n.inc' %]
2
[% PROCESS 'i18n.inc' %]
3
[% t('hello') | $raw %]
3
[% t('hello ツ') | $raw %]
4
[% tx('hello {name}', { name = 'Bob' }) | $raw %]
4
[% tx('hello {name}', { name = 'Bob' }) | $raw %]
5
[% tn('item', 'items', count) | $raw %]
5
[% tn('item', 'items', count) | $raw %]
6
[% tnx('{count} item', '{count} items', count, { count = count }) | $raw %]
6
[% tnx('{count} item', '{count} items', count, { count = count }) | $raw %]
(-)a/t/misc/translator/sample.yml (+15 lines)
Line 0 Link Here
1
description:
2
  - "Sample installer file"
3
4
tables:
5
  - table1:
6
        translatable: [ column1, column2 ]
7
        multiline: [ column2 ]
8
        rows:
9
          - column1: foo ツ
10
            column2:
11
              - bar
12
              - baz
13
            column3: qux
14
            column4:
15
              - quux
(-)a/t/misc/translator/xgettext-installer.t (+32 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use File::Slurp;
6
use File::Temp qw(tempdir);
7
use FindBin qw($Bin);
8
use Locale::PO;
9
use Test::More tests => 4;
10
11
my $tempdir = tempdir(CLEANUP => 1);
12
13
write_file("$tempdir/files", "$Bin/sample.yml");
14
15
my $xgettext_cmd = "$Bin/../../../misc/translator/xgettext-installer "
16
    . "-o $tempdir/Koha.pot -f $tempdir/files";
17
18
system($xgettext_cmd);
19
my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
20
21
my @expected = (
22
    { msgid => '"Sample installer file"' },
23
    { msgid => '"bar"' },
24
    { msgid => '"baz"' },
25
    { msgid => '"foo ツ"' },
26
);
27
28
for (my $i = 0; $i < @expected; $i++) {
29
    my $expected = $expected[$i]->{msgid};
30
    my $expected_str = defined $expected ? $expected : 'not defined';
31
    is($pot->[$i + 1]->msgid, $expected, "$i: msgid is $expected_str");
32
}
(-)a/t/misc/translator/xgettext-pref.t (+54 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 => '"sample.pref"',
24
    },
25
    {
26
        msgid => '"sample.pref Subsection"',
27
    },
28
    {
29
        msgid => '"sample.pref#MultiplePref# Bar"',
30
    },
31
    {
32
        msgid => '"sample.pref#MultiplePref# Baz"',
33
    },
34
    {
35
        msgid => '"sample.pref#MultiplePref# Foo ツ"',
36
    },
37
    {
38
        msgid => '"sample.pref#SamplePref# Do"',
39
    },
40
    {
41
        msgid => '"sample.pref#SamplePref# Do not do"',
42
    },
43
    {
44
        msgid => '"sample.pref#SamplePref# that thing"',
45
    },
46
);
47
48
for (my $i = 0; $i < @expected; $i++) {
49
    for my $key (qw(msgid msgctxt)) {
50
        my $expected = $expected[$i]->{$key};
51
        my $expected_str = defined $expected ? $expected : 'not defined';
52
        is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
53
    }
54
}
(-)a/t/misc/translator/xgettext-tt2.t (+74 lines)
Line 0 Link Here
1
#!/usr/bin/perl
2
3
use Modern::Perl;
4
5
use File::Slurp;
6
use File::Temp qw(tempdir);
7
use FindBin qw($Bin);
8
use Locale::PO;
9
use Test::More tests => 36;
10
11
my $tempdir = tempdir(CLEANUP => 1);
12
13
write_file("$tempdir/files", "$Bin/sample.tt");
14
15
my $xgettext_cmd = "$Bin/../../../misc/translator/xgettext-tt2 --from-code=UTF-8 "
16
    . "-o $tempdir/Koha.pot -f $tempdir/files";
17
18
system($xgettext_cmd);
19
my $pot = Locale::PO->load_file_asarray("$tempdir/Koha.pot");
20
21
my @expected = (
22
    {
23
        msgid => '"hello ツ"',
24
    },
25
    {
26
        msgid => '"hello {name}"',
27
    },
28
    {
29
        msgid => '"item"',
30
        msgid_plural => '"items"',
31
    },
32
    {
33
        msgid => '"{count} item"',
34
        msgid_plural => '"{count} items"',
35
    },
36
    {
37
        msgid => '"hello"',
38
        msgctxt => '"context"',
39
    },
40
    {
41
        msgid => '"hello {name}"',
42
        msgctxt => '"context"',
43
    },
44
    {
45
        msgid => '"item"',
46
        msgid_plural => '"items"',
47
        msgctxt => '"context"',
48
    },
49
    {
50
        msgid => '"{count} item"',
51
        msgid_plural => '"{count} items"',
52
        msgctxt => '"context"',
53
    },
54
    {
55
        msgid => '"status is {status}"',
56
    },
57
    {
58
        msgid => '"active"',
59
    },
60
    {
61
        msgid => '"inactive"',
62
    },
63
    {
64
        msgid => '"Inside block"',
65
    },
66
);
67
68
for (my $i = 0; $i < @expected; $i++) {
69
    for my $key (qw(msgid msgid_plural msgctxt)) {
70
        my $expected = $expected[$i]->{$key};
71
        my $expected_str = defined $expected ? $expected : 'not defined';
72
        is($pot->[$i + 1]->$key, $expected, "$i: $key is $expected_str");
73
    }
74
}
(-)a/yarn.lock (-7 / +109 lines)
Lines 1522-1527 gulp-cli@^2.2.0: Link Here
1522
    v8flags "^3.2.0"
1522
    v8flags "^3.2.0"
1523
    yargs "^7.1.0"
1523
    yargs "^7.1.0"
1524
1524
1525
gulp-concat-po@^1.0.0:
1526
  version "1.0.0"
1527
  resolved "https://registry.yarnpkg.com/gulp-concat-po/-/gulp-concat-po-1.0.0.tgz#2fe7b2c12e45a566238e228f63396838013770ae"
1528
  integrity sha512-hFDZrUJcpw10TW3BfptL5W2FV/aMo3M+vxz9YQV4nlMBDAi8gs9/yZYZcYMYfl5XKhjpebSef8nyruoWdlX8Hw==
1529
  dependencies:
1530
    lodash.find "^4.6.0"
1531
    lodash.merge "^4.6.2"
1532
    lodash.uniq "^4.5.0"
1533
    plugin-error "^1.0.1"
1534
    pofile "^1.1.0"
1535
    through2 "^0.6.5"
1536
    vinyl "^2.2.0"
1537
1525
gulp-cssnano@^2.1.2:
1538
gulp-cssnano@^2.1.2:
1526
  version "2.1.3"
1539
  version "2.1.3"
1527
  resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
1540
  resolved "https://registry.yarnpkg.com/gulp-cssnano/-/gulp-cssnano-2.1.3.tgz#02007e2817af09b3688482b430ad7db807aebf72"
Lines 1533-1538 gulp-cssnano@^2.1.2: Link Here
1533
    plugin-error "^1.0.1"
1546
    plugin-error "^1.0.1"
1534
    vinyl-sourcemaps-apply "^0.2.1"
1547
    vinyl-sourcemaps-apply "^0.2.1"
1535
1548
1549
gulp-exec@^4.0.0:
1550
  version "4.0.0"
1551
  resolved "https://registry.yarnpkg.com/gulp-exec/-/gulp-exec-4.0.0.tgz#4b6b67be0200d620143f3198a64257b68b146bb6"
1552
  integrity sha512-A9JvTyB3P4huusd/43bTr6SDg3MqBxL9AQbLnsKSO6/91wVkHfxgeJZlgDMkqK8sMel4so8wcko4SZOeB1UCgA==
1553
  dependencies:
1554
    lodash.template "^4.4.0"
1555
    plugin-error "^1.0.1"
1556
    through2 "^3.0.1"
1557
1536
gulp-rename@^2.0.0:
1558
gulp-rename@^2.0.0:
1537
  version "2.0.0"
1559
  version "2.0.0"
1538
  resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c"
1560
  resolved "https://registry.yarnpkg.com/gulp-rename/-/gulp-rename-2.0.0.tgz#9bbc3962b0c0f52fc67cd5eaff6c223ec5b9cf6c"
Lines 2205-2210 lodash.escape@^3.0.0: Link Here
2205
  dependencies:
2227
  dependencies:
2206
    lodash._root "^3.0.0"
2228
    lodash._root "^3.0.0"
2207
2229
2230
lodash.find@^4.6.0:
2231
  version "4.6.0"
2232
  resolved "https://registry.yarnpkg.com/lodash.find/-/lodash.find-4.6.0.tgz#cb0704d47ab71789ffa0de8b97dd926fb88b13b1"
2233
  integrity sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=
2234
2208
lodash.isarguments@^3.0.0:
2235
lodash.isarguments@^3.0.0:
2209
  version "3.1.0"
2236
  version "3.1.0"
2210
  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
2237
  resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a"
Lines 2229-2234 lodash.memoize@^4.1.2: Link Here
2229
  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
2256
  resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
2230
  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
2257
  integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
2231
2258
2259
lodash.merge@^4.6.2:
2260
  version "4.6.2"
2261
  resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
2262
  integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
2263
2232
lodash.restparam@^3.0.0:
2264
lodash.restparam@^3.0.0:
2233
  version "3.6.1"
2265
  version "3.6.1"
2234
  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
2266
  resolved "https://registry.yarnpkg.com/lodash.restparam/-/lodash.restparam-3.6.1.tgz#936a4e309ef330a7645ed4145986c85ae5b20805"
Lines 2249-2254 lodash.template@^3.0.0: Link Here
2249
    lodash.restparam "^3.0.0"
2281
    lodash.restparam "^3.0.0"
2250
    lodash.templatesettings "^3.0.0"
2282
    lodash.templatesettings "^3.0.0"
2251
2283
2284
lodash.template@^4.4.0:
2285
  version "4.5.0"
2286
  resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab"
2287
  integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==
2288
  dependencies:
2289
    lodash._reinterpolate "^3.0.0"
2290
    lodash.templatesettings "^4.0.0"
2291
2252
lodash.templatesettings@^3.0.0:
2292
lodash.templatesettings@^3.0.0:
2253
  version "3.1.1"
2293
  version "3.1.1"
2254
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
2294
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-3.1.1.tgz#fb307844753b66b9f1afa54e262c745307dba8e5"
Lines 2257-2262 lodash.templatesettings@^3.0.0: Link Here
2257
    lodash._reinterpolate "^3.0.0"
2297
    lodash._reinterpolate "^3.0.0"
2258
    lodash.escape "^3.0.0"
2298
    lodash.escape "^3.0.0"
2259
2299
2300
lodash.templatesettings@^4.0.0:
2301
  version "4.2.0"
2302
  resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33"
2303
  integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==
2304
  dependencies:
2305
    lodash._reinterpolate "^3.0.0"
2306
2260
lodash.uniq@^4.5.0:
2307
lodash.uniq@^4.5.0:
2261
  version "4.5.0"
2308
  version "4.5.0"
2262
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
2309
  resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
Lines 2359-2364 meow@^3.7.0: Link Here
2359
    redent "^1.0.0"
2406
    redent "^1.0.0"
2360
    trim-newlines "^1.0.0"
2407
    trim-newlines "^1.0.0"
2361
2408
2409
merge-stream@^2.0.0:
2410
  version "2.0.0"
2411
  resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
2412
  integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==
2413
2362
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
2414
micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4:
2363
  version "3.1.10"
2415
  version "3.1.10"
2364
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
2416
  resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
Lines 2842-2847 plugin-error@^1.0.1: Link Here
2842
    arr-union "^3.1.0"
2894
    arr-union "^3.1.0"
2843
    extend-shallow "^3.0.2"
2895
    extend-shallow "^3.0.2"
2844
2896
2897
pofile@^1.1.0:
2898
  version "1.1.0"
2899
  resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.1.0.tgz#9ce84bbef5043ceb4f19bdc3520d85778fad4f94"
2900
  integrity sha512-6XYcNkXWGiJ2CVXogTP7uJ6ZXQCldYLZc16wgRp8tqRaBTTyIfF+TUT3EQJPXTLAT7OTPpTAoaFdoXKfaTRU1w==
2901
2845
posix-character-classes@^0.1.0:
2902
posix-character-classes@^0.1.0:
2846
  version "0.1.1"
2903
  version "0.1.1"
2847
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
2904
  resolved "https://registry.yarnpkg.com/posix-character-classes/-/posix-character-classes-0.1.1.tgz#01eac0fe3b5af71a2a6c02feabb8c1fef7e00eab"
Lines 3177-3182 read-pkg@^1.0.0: Link Here
3177
    normalize-package-data "^2.3.2"
3234
    normalize-package-data "^2.3.2"
3178
    path-type "^1.0.0"
3235
    path-type "^1.0.0"
3179
3236
3237
"readable-stream@2 || 3":
3238
  version "3.6.0"
3239
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
3240
  integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
3241
  dependencies:
3242
    inherits "^2.0.3"
3243
    string_decoder "^1.1.1"
3244
    util-deprecate "^1.0.1"
3245
3246
"readable-stream@>=1.0.33-1 <1.1.0-0":
3247
  version "1.0.34"
3248
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
3249
  integrity sha1-Elgg40vIQtLyqq+v5MKRbuMsFXw=
3250
  dependencies:
3251
    core-util-is "~1.0.0"
3252
    inherits "~2.0.1"
3253
    isarray "0.0.1"
3254
    string_decoder "~0.10.x"
3255
3180
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
3256
readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.5, readable-stream@^2.2.2, readable-stream@^2.3.3, readable-stream@^2.3.5, readable-stream@^2.3.6, readable-stream@~2.3.6:
3181
  version "2.3.7"
3257
  version "2.3.7"
3182
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
3258
  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57"
Lines 3293-3301 replace-ext@0.0.1: Link Here
3293
  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
3369
  integrity sha1-KbvZIHinOfC8zitO5B6DeVNSKSQ=
3294
3370
3295
replace-ext@^1.0.0:
3371
replace-ext@^1.0.0:
3296
  version "1.0.1"
3372
  version "1.0.0"
3297
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.1.tgz#2d6d996d04a15855d967443631dd5f77825b016a"
3373
  resolved "https://registry.yarnpkg.com/replace-ext/-/replace-ext-1.0.0.tgz#de63128373fcbf7c3ccfa4de5a480c45a67958eb"
3298
  integrity sha512-yD5BHCe7quCgBph4rMQ+0KkIRKwWCrHDOX1p1Gp6HwjPM5kVoCdKGNhN7ydqqsX6lJEnQDKZ/tFMiEdQ1dvPEw==
3374
  integrity sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=
3299
3375
3300
replace-homedir@^1.0.0:
3376
replace-homedir@^1.0.0:
3301
  version "1.0.0"
3377
  version "1.0.0"
Lines 3407-3412 safe-buffer@~5.1.0, safe-buffer@~5.1.1: Link Here
3407
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
3483
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
3408
  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
3484
  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
3409
3485
3486
safe-buffer@~5.2.0:
3487
  version "5.2.0"
3488
  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519"
3489
  integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg==
3490
3410
safe-regex@^1.1.0:
3491
safe-regex@^1.1.0:
3411
  version "1.1.0"
3492
  version "1.1.0"
3412
  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
3493
  resolved "https://registry.yarnpkg.com/safe-regex/-/safe-regex-1.1.0.tgz#40a3669f3b077d1e943d44629e157dd48023bf2e"
Lines 3668-3673 string-width@^3.0.0, string-width@^3.1.0: Link Here
3668
    is-fullwidth-code-point "^2.0.0"
3749
    is-fullwidth-code-point "^2.0.0"
3669
    strip-ansi "^5.1.0"
3750
    strip-ansi "^5.1.0"
3670
3751
3752
string_decoder@^1.1.1:
3753
  version "1.3.0"
3754
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
3755
  integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
3756
  dependencies:
3757
    safe-buffer "~5.2.0"
3758
3671
string_decoder@~0.10.x:
3759
string_decoder@~0.10.x:
3672
  version "0.10.31"
3760
  version "0.10.31"
3673
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
3761
  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
Lines 3790-3795 through2@2.X, through2@^2.0.0, through2@^2.0.3, through2@^2.0.5, through2@~2.0.0 Link Here
3790
    readable-stream "~2.3.6"
3878
    readable-stream "~2.3.6"
3791
    xtend "~4.0.1"
3879
    xtend "~4.0.1"
3792
3880
3881
through2@^0.6.5:
3882
  version "0.6.5"
3883
  resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
3884
  integrity sha1-QaucZ7KdVyCQcUEOHXp6lozTrUg=
3885
  dependencies:
3886
    readable-stream ">=1.0.33-1 <1.1.0-0"
3887
    xtend ">=4.0.0 <4.1.0-0"
3888
3889
through2@^3.0.1:
3890
  version "3.0.1"
3891
  resolved "https://registry.yarnpkg.com/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a"
3892
  integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww==
3893
  dependencies:
3894
    readable-stream "2 || 3"
3895
3793
time-stamp@^1.0.0:
3896
time-stamp@^1.0.0:
3794
  version "1.1.0"
3897
  version "1.1.0"
3795
  resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
3898
  resolved "https://registry.yarnpkg.com/time-stamp/-/time-stamp-1.1.0.tgz#764a5a11af50561921b133f3b44e618687e0f5c3"
Lines 3974-3980 use@^3.1.0: Link Here
3974
  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
4077
  resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
3975
  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
4078
  integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
3976
4079
3977
util-deprecate@~1.0.1:
4080
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
3978
  version "1.0.2"
4081
  version "1.0.2"
3979
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
4082
  resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
3980
  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
4083
  integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
Lines 4070-4076 vinyl@^0.5.0: Link Here
4070
    clone-stats "^0.0.1"
4173
    clone-stats "^0.0.1"
4071
    replace-ext "0.0.1"
4174
    replace-ext "0.0.1"
4072
4175
4073
vinyl@^2.0.0:
4176
vinyl@^2.0.0, vinyl@^2.2.0:
4074
  version "2.2.0"
4177
  version "2.2.0"
4075
  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
4178
  resolved "https://registry.yarnpkg.com/vinyl/-/vinyl-2.2.0.tgz#d85b07da96e458d25b2ffe19fece9f2caa13ed86"
4076
  integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
4179
  integrity sha512-MBH+yP0kC/GQ5GwBqrTPTzEfiiLjta7hTtvQtbxBgTeSXsmKQRQecjibMbxIXzVT3Y9KJK+drOz1/k+vsu8Nkg==
Lines 4133-4139 wrappy@1: Link Here
4133
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
4236
  resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f"
4134
  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
4237
  integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=
4135
4238
4136
xtend@~4.0.0, xtend@~4.0.1:
4239
"xtend@>=4.0.0 <4.1.0-0", xtend@~4.0.0, xtend@~4.0.1:
4137
  version "4.0.2"
4240
  version "4.0.2"
4138
  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
4241
  resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
4139
  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
4242
  integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
4140
- 

Return to bug 25067