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

(-)a/koha-tmpl/intranet-tmpl/js/Gettext.js (+1264 lines)
Line 0 Link Here
1
/*
2
Pure Javascript implementation of Uniforum message translation.
3
Copyright (C) 2008 Joshua I. Miller <unrtst@cpan.org>, all rights reserved
4
5
This program is free software; you can redistribute it and/or modify it
6
under the terms of the GNU Library General Public License as published
7
by the Free Software Foundation; either version 2, or (at your option)
8
any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13
Library General Public License for more details.
14
15
You should have received a copy of the GNU Library General Public
16
License along with this program; if not, write to the Free Software
17
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
18
USA.
19
20
=head1 NAME
21
22
Javascript Gettext - Javascript implemenation of GNU Gettext API.
23
24
=head1 SYNOPSIS
25
26
 // //////////////////////////////////////////////////////////
27
 // Optimum caching way
28
 <script language="javascript" src="/path/LC_MESSAGES/myDomain.json"></script>
29
 <script language="javascript" src="/path/Gettext.js'></script>
30
31
 // assuming myDomain.json defines variable json_locale_data
32
 var params = {  "domain" : "myDomain",
33
                 "locale_data" : json_locale_data
34
              };
35
 var gt = new Gettext(params);
36
 // create a shortcut if you'd like
37
 function _ (msgid) { return gt.gettext(msgid); }
38
 alert(_("some string"));
39
 // or use fully named method
40
 alert(gt.gettext("some string"));
41
 // change to use a different "domain"
42
 gt.textdomain("anotherDomain");
43
 alert(gt.gettext("some string"));
44
45
46
 // //////////////////////////////////////////////////////////
47
 // The other way to load the language lookup is a "link" tag
48
 // Downside is that not all browsers cache XMLHttpRequests the
49
 // same way, so caching of the language data isn't guarenteed
50
 // across page loads.
51
 // Upside is that it's easy to specify multiple files
52
 <link rel="gettext" href="/path/LC_MESSAGES/myDomain.json" />
53
 <script language="javascript" src="/path/Gettext.js'></script>
54
55
 var gt = new Gettext({ "domain" : "myDomain" });
56
 // rest is the same
57
58
59
 // //////////////////////////////////////////////////////////
60
 // The reson the shortcuts aren't exported by default is because they'd be
61
 // glued to the single domain you created. So, if you're adding i18n support
62
 // to some js library, you should use it as so:
63
64
 if (typeof(MyNamespace) == 'undefined') MyNamespace = {};
65
 MyNamespace.MyClass = function () {
66
     var gtParms = { "domain" : 'MyNamespace_MyClass' };
67
     this.gt = new Gettext(gtParams);
68
     return this;
69
 };
70
 MyNamespace.MyClass.prototype._ = function (msgid) {
71
     return this.gt.gettext(msgid);
72
 };
73
 MyNamespace.MyClass.prototype.something = function () {
74
     var myString = this._("this will get translated");
75
 };
76
77
 // //////////////////////////////////////////////////////////
78
 // Adding the shortcuts to a global scope is easier. If that's
79
 // ok in your app, this is certainly easier.
80
 var myGettext = new Gettext({ 'domain' : 'myDomain' });
81
 function _ (msgid) {
82
     return myGettext.gettext(msgid);
83
 }
84
 alert( _("text") );
85
86
 // //////////////////////////////////////////////////////////
87
 // Data structure of the json data
88
 // NOTE: if you're loading via the <script> tag, you can only
89
 // load one file, but it can contain multiple domains.
90
 var json_locale_data = {
91
     "MyDomain" : {
92
         "" : {
93
             "header_key" : "header value",
94
             "header_key" : "header value",
95
         "msgid" : [ "msgid_plural", "msgstr", "msgstr_plural", "msgstr_pluralN" ],
96
         "msgctxt\004msgid" : [ null, "msgstr" ],
97
         },
98
     "AnotherDomain" : {
99
         },
100
     }
101
102
=head1 DESCRIPTION
103
104
This is a javascript implementation of GNU Gettext, providing internationalization support for javascript. It differs from existing javascript implementations in that it will support all current Gettext features (ex. plural and context support), and will also support loading language catalogs from .mo, .po, or preprocessed json files (converter included).
105
106
The locale initialization differs from that of GNU Gettext / POSIX. Rather than setting the category, domain, and paths, and letting the libs find the right file, you must explicitly load the file at some point. The "domain" will still be honored. Future versions may be expanded to include support for set_locale like features.
107
108
109
=head1 INSTALL
110
111
To install this module, simply copy the file lib/Gettext.js to a web accessable location, and reference it from your application.
112
113
114
=head1 CONFIGURATION
115
116
Configure in one of two ways:
117
118
=over
119
120
=item 1. Optimal. Load language definition from statically defined json data.
121
122
    <script language="javascript" src="/path/locale/domain.json"></script>
123
124
    // in domain.json
125
    json_locale_data = {
126
        "mydomain" : {
127
            // po header fields
128
            "" : {
129
                "plural-forms" : "...",
130
                "lang" : "en",
131
                },
132
            // all the msgid strings and translations
133
            "msgid" : [ "msgid_plural", "translation", "plural_translation" ],
134
        },
135
    };
136
    // please see the included bin/po2json script for the details on this format
137
138
This method also allows you to use unsupported file formats, so long as you can parse them into the above format.
139
140
=item 2. Use AJAX to load language file.
141
142
Use XMLHttpRequest (actually, SJAX - syncronous) to load an external resource.
143
144
Supported external formats are:
145
146
=over
147
148
=item * Javascript Object Notation (.json)
149
150
(see bin/po2json)
151
152
    type=application/json
153
154
=item * Uniforum Portable Object (.po)
155
156
(see GNU Gettext's xgettext)
157
158
    type=application/x-po
159
160
=item * Machine Object (compiled .po) (.mo)
161
162
NOTE: .mo format isn't actually supported just yet, but support is planned.
163
164
(see GNU Gettext's msgfmt)
165
166
    type=application/x-mo
167
168
=back
169
170
=back
171
172
=head1 METHODS
173
174
The following methods are implemented:
175
176
  new Gettext(args)
177
  textdomain  (domain)
178
  gettext     (msgid)
179
  dgettext    (domainname, msgid)
180
  dcgettext   (domainname, msgid, LC_MESSAGES)
181
  ngettext    (msgid, msgid_plural, count)
182
  dngettext   (domainname, msgid, msgid_plural, count)
183
  dcngettext  (domainname, msgid, msgid_plural, count, LC_MESSAGES)
184
  pgettext    (msgctxt, msgid)
185
  dpgettext   (domainname, msgctxt, msgid)
186
  dcpgettext  (domainname, msgctxt, msgid, LC_MESSAGES)
187
  npgettext   (msgctxt, msgid, msgid_plural, count)
188
  dnpgettext  (domainname, msgctxt, msgid, msgid_plural, count)
189
  dcnpgettext (domainname, msgctxt, msgid, msgid_plural, count, LC_MESSAGES)
190
  strargs     (string, args_array)
191
192
193
=head2 new Gettext (args)
194
195
Several methods of loading locale data are included. You may specify a plugin or alternative method of loading data by passing the data in as the "locale_data" option. For example:
196
197
    var get_locale_data = function () {
198
        // plugin does whatever to populate locale_data
199
        return locale_data;
200
    };
201
    var gt = new Gettext( 'domain' : 'messages',
202
                          'locale_data' : get_locale_data() );
203
204
The above can also be used if locale data is specified in a statically included <SCRIPT> tag. Just specify the variable name in the call to new. Ex:
205
206
    var gt = new Gettext( 'domain' : 'messages',
207
                          'locale_data' : json_locale_data_variable );
208
209
Finally, you may load the locale data by referencing it in a <LINK> tag. Simply exclude the 'locale_data' option, and all <LINK rel="gettext" ...> items will be tried. The <LINK> should be specified as:
210
211
    <link rel="gettext" type="application/json" href="/path/to/file.json">
212
    <link rel="gettext" type="text/javascript"  href="/path/to/file.json">
213
    <link rel="gettext" type="application/x-po" href="/path/to/file.po">
214
    <link rel="gettext" type="application/x-mo" href="/path/to/file.mo">
215
216
args:
217
218
=over
219
220
=item domain
221
222
The Gettext domain, not www.whatev.com. It's usually your applications basename. If the .po file was "myapp.po", this would be "myapp".
223
224
=item locale_data
225
226
Raw locale data (in json structure). If specified, from_link data will be ignored.
227
228
=back
229
230
=cut
231
232
*/
233
234
Gettext = function (args) {
235
    this.domain         = 'messages';
236
    // locale_data will be populated from <link...> if not specified in args
237
    this.locale_data    = undefined;
238
239
    // set options
240
    var options = [ "domain", "locale_data" ];
241
    if (this.isValidObject(args)) {
242
        for (var i in args) {
243
            for (var j=0; j<options.length; j++) {
244
                if (i == options[j]) {
245
                    // don't set it if it's null or undefined
246
                    if (this.isValidObject(args[i]))
247
                        this[i] = args[i];
248
                }
249
            }
250
        }
251
    }
252
253
254
    // try to load the lang file from somewhere
255
    this.try_load_lang();
256
257
    return this;
258
}
259
260
Gettext.context_glue = "\004";
261
Gettext._locale_data = {};
262
263
Gettext.prototype.try_load_lang = function() {
264
    // check to see if language is statically included
265
    if (typeof(this.locale_data) != 'undefined') {
266
        // we're going to reformat it, and overwrite the variable
267
        var locale_copy = this.locale_data;
268
        this.locale_data = undefined;
269
        this.parse_locale_data(locale_copy);
270
271
        if (typeof(Gettext._locale_data[this.domain]) == 'undefined') {
272
            throw new Error("Error: Gettext 'locale_data' does not contain the domain '"+this.domain+"'");
273
        }
274
    }
275
276
277
    // try loading from JSON
278
    // get lang links
279
    var lang_link = this.get_lang_refs();
280
281
    if (typeof(lang_link) == 'object' && lang_link.length > 0) {
282
        // NOTE: there will be a delay here, as this is async.
283
        // So, any i18n calls made right after page load may not
284
        // get translated.
285
        // XXX: we may want to see if we can "fix" this behavior
286
        for (var i=0; i<lang_link.length; i++) {
287
            var link = lang_link[i];
288
            if (link.type == 'application/json') {
289
                if (! this.try_load_lang_json(link.href) ) {
290
                    throw new Error("Error: Gettext 'try_load_lang_json' failed. Unable to exec xmlhttprequest for link ["+link.href+"]");
291
                }
292
            } else if (link.type == 'application/x-po') {
293
                if (! this.try_load_lang_po(link.href) ) {
294
                    throw new Error("Error: Gettext 'try_load_lang_po' failed. Unable to exec xmlhttprequest for link ["+link.href+"]");
295
                }
296
            } else {
297
                // TODO: implement the other types (.mo)
298
                throw new Error("TODO: link type ["+link.type+"] found, and support is planned, but not implemented at this time.");
299
            }
300
        }
301
    }
302
};
303
304
// This takes the bin/po2json'd data, and moves it into an internal form
305
// for use in our lib, and puts it in our object as:
306
//  Gettext._locale_data = {
307
//      domain : {
308
//          head : { headfield : headvalue },
309
//          msgs : {
310
//              msgid : [ msgid_plural, msgstr, msgstr_plural ],
311
//          },
312
Gettext.prototype.parse_locale_data = function(locale_data) {
313
    if (typeof(Gettext._locale_data) == 'undefined') {
314
        Gettext._locale_data = { };
315
    }
316
317
    // suck in every domain defined in the supplied data
318
    for (var domain in locale_data) {
319
        // skip empty specs (flexibly)
320
        if ((! locale_data.hasOwnProperty(domain)) || (! this.isValidObject(locale_data[domain])))
321
            continue;
322
        // skip if it has no msgid's
323
        var has_msgids = false;
324
        for (var msgid in locale_data[domain]) {
325
            has_msgids = true;
326
            break;
327
        }
328
        if (! has_msgids) continue;
329
330
        // grab shortcut to data
331
        var data = locale_data[domain];
332
333
        // if they specifcy a blank domain, default to "messages"
334
        if (domain == "") domain = "messages";
335
        // init the data structure
336
        if (! this.isValidObject(Gettext._locale_data[domain]) )
337
            Gettext._locale_data[domain] = { };
338
        if (! this.isValidObject(Gettext._locale_data[domain].head) )
339
            Gettext._locale_data[domain].head = { };
340
        if (! this.isValidObject(Gettext._locale_data[domain].msgs) )
341
            Gettext._locale_data[domain].msgs = { };
342
343
        for (var key in data) {
344
            if (key == "") {
345
                var header = data[key];
346
                for (var head in header) {
347
                    var h = head.toLowerCase();
348
                    Gettext._locale_data[domain].head[h] = header[head];
349
                }
350
            } else {
351
                Gettext._locale_data[domain].msgs[key] = data[key];
352
            }
353
        }
354
    }
355
356
    // build the plural forms function
357
    for (var domain in Gettext._locale_data) {
358
        if (this.isValidObject(Gettext._locale_data[domain].head['plural-forms']) &&
359
            typeof(Gettext._locale_data[domain].head.plural_func) == 'undefined') {
360
            // untaint data
361
            var plural_forms = Gettext._locale_data[domain].head['plural-forms'];
362
            var pf_re = new RegExp('^(\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;a-zA-Z0-9_\(\)])+)', 'm');
363
            if (pf_re.test(plural_forms)) {
364
                //ex english: "Plural-Forms: nplurals=2; plural=(n != 1);\n"
365
                //pf = "nplurals=2; plural=(n != 1);";
366
                //ex russian: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10< =4 && (n%100<10 or n%100>=20) ? 1 : 2)
367
                //pf = "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)";
368
369
                var pf = Gettext._locale_data[domain].head['plural-forms'];
370
                if (! /;\s*$/.test(pf)) pf = pf.concat(';');
371
                /* We used to use eval, but it seems IE has issues with it.
372
                 * We now use "new Function", though it carries a slightly
373
                 * bigger performance hit.
374
                var code = 'function (n) { var plural; var nplurals; '+pf+' return { "nplural" : nplurals, "plural" : (plural === true ? 1 : plural ? plural : 0) }; };';
375
                Gettext._locale_data[domain].head.plural_func = eval("("+code+")");
376
                */
377
                var code = 'var plural; var nplurals; '+pf+' return { "nplural" : nplurals, "plural" : (plural === true ? 1 : plural ? plural : 0) };';
378
                Gettext._locale_data[domain].head.plural_func = new Function("n", code);
379
            } else {
380
                throw new Error("Syntax error in language file. Plural-Forms header is invalid ["+plural_forms+"]");
381
            }
382
383
        // default to english plural form
384
        } else if (typeof(Gettext._locale_data[domain].head.plural_func) == 'undefined') {
385
            Gettext._locale_data[domain].head.plural_func = function (n) {
386
                var p = (n != 1) ? 1 : 0;
387
                return { 'nplural' : 2, 'plural' : p };
388
                };
389
        } // else, plural_func already created
390
    }
391
392
    return;
393
};
394
395
396
// try_load_lang_po : do an ajaxy call to load in the .po lang defs
397
Gettext.prototype.try_load_lang_po = function(uri) {
398
    var data = this.sjax(uri);
399
    if (! data) return;
400
401
    var domain = this.uri_basename(uri);
402
    var parsed = this.parse_po(data);
403
404
    var rv = {};
405
    // munge domain into/outof header
406
    if (parsed) {
407
        if (! parsed[""]) parsed[""] = {};
408
        if (! parsed[""]["domain"]) parsed[""]["domain"] = domain;
409
        domain = parsed[""]["domain"];
410
        rv[domain] = parsed;
411
412
        this.parse_locale_data(rv);
413
    }
414
415
    return 1;
416
};
417
418
Gettext.prototype.uri_basename = function(uri) {
419
    var rv;
420
    if (rv = uri.match(/^(.*\/)?(.*)/)) {
421
        var ext_strip;
422
        if (ext_strip = rv[2].match(/^(.*)\..+$/))
423
            return ext_strip[1];
424
        else
425
            return rv[2];
426
    } else {
427
        return "";
428
    }
429
};
430
431
Gettext.prototype.parse_po = function(data) {
432
    var rv = {};
433
    var buffer = {};
434
    var lastbuffer = "";
435
    var errors = [];
436
    var lines = data.split("\n");
437
    for (var i=0; i<lines.length; i++) {
438
        // chomp
439
        lines[i] = lines[i].replace(/(\n|\r)+$/, '');
440
441
        var match;
442
443
        // Empty line / End of an entry.
444
        if (/^$/.test(lines[i])) {
445
            if (typeof(buffer['msgid']) != 'undefined') {
446
                var msg_ctxt_id = (typeof(buffer['msgctxt']) != 'undefined' &&
447
                                   buffer['msgctxt'].length) ?
448
                                  buffer['msgctxt']+Gettext.context_glue+buffer['msgid'] :
449
                                  buffer['msgid'];
450
                var msgid_plural = (typeof(buffer['msgid_plural']) != 'undefined' &&
451
                                    buffer['msgid_plural'].length) ?
452
                                   buffer['msgid_plural'] :
453
                                   null;
454
455
                // find msgstr_* translations and push them on
456
                var trans = [];
457
                for (var str in buffer) {
458
                    var match;
459
                    if (match = str.match(/^msgstr_(\d+)/))
460
                        trans[parseInt(match[1])] = buffer[str];
461
                }
462
                trans.unshift(msgid_plural);
463
464
                // only add it if we've got a translation
465
                // NOTE: this doesn't conform to msgfmt specs
466
                if (trans.length > 1) rv[msg_ctxt_id] = trans;
467
468
                buffer = {};
469
                lastbuffer = "";
470
            }
471
472
        // comments
473
        } else if (/^#/.test(lines[i])) {
474
            continue;
475
476
        // msgctxt
477
        } else if (match = lines[i].match(/^msgctxt\s+(.*)/)) {
478
            lastbuffer = 'msgctxt';
479
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
480
481
        // msgid
482
        } else if (match = lines[i].match(/^msgid\s+(.*)/)) {
483
            lastbuffer = 'msgid';
484
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
485
486
        // msgid_plural
487
        } else if (match = lines[i].match(/^msgid_plural\s+(.*)/)) {
488
            lastbuffer = 'msgid_plural';
489
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
490
491
        // msgstr
492
        } else if (match = lines[i].match(/^msgstr\s+(.*)/)) {
493
            lastbuffer = 'msgstr_0';
494
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
495
496
        // msgstr[0] (treak like msgstr)
497
        } else if (match = lines[i].match(/^msgstr\[0\]\s+(.*)/)) {
498
            lastbuffer = 'msgstr_0';
499
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
500
501
        // msgstr[n]
502
        } else if (match = lines[i].match(/^msgstr\[(\d+)\]\s+(.*)/)) {
503
            lastbuffer = 'msgstr_'+match[1];
504
            buffer[lastbuffer] = this.parse_po_dequote(match[2]);
505
506
        // continued string
507
        } else if (/^"/.test(lines[i])) {
508
            buffer[lastbuffer] += this.parse_po_dequote(lines[i]);
509
510
        // something strange
511
        } else {
512
            errors.push("Strange line ["+i+"] : "+lines[i]);
513
        }
514
    }
515
516
517
    // handle the final entry
518
    if (typeof(buffer['msgid']) != 'undefined') {
519
        var msg_ctxt_id = (typeof(buffer['msgctxt']) != 'undefined' &&
520
                           buffer['msgctxt'].length) ?
521
                          buffer['msgctxt']+Gettext.context_glue+buffer['msgid'] :
522
                          buffer['msgid'];
523
        var msgid_plural = (typeof(buffer['msgid_plural']) != 'undefined' &&
524
                            buffer['msgid_plural'].length) ?
525
                           buffer['msgid_plural'] :
526
                           null;
527
528
        // find msgstr_* translations and push them on
529
        var trans = [];
530
        for (var str in buffer) {
531
            var match;
532
            if (match = str.match(/^msgstr_(\d+)/))
533
                trans[parseInt(match[1])] = buffer[str];
534
        }
535
        trans.unshift(msgid_plural);
536
537
        // only add it if we've got a translation
538
        // NOTE: this doesn't conform to msgfmt specs
539
        if (trans.length > 1) rv[msg_ctxt_id] = trans;
540
541
        buffer = {};
542
        lastbuffer = "";
543
    }
544
545
546
    // parse out the header
547
    if (rv[""] && rv[""][1]) {
548
        var cur = {};
549
        var hlines = rv[""][1].split(/\\n/);
550
        for (var i=0; i<hlines.length; i++) {
551
            if (! hlines.length) continue;
552
553
            var pos = hlines[i].indexOf(':', 0);
554
            if (pos != -1) {
555
                var key = hlines[i].substring(0, pos);
556
                var val = hlines[i].substring(pos +1);
557
                var keylow = key.toLowerCase();
558
559
                if (cur[keylow] && cur[keylow].length) {
560
                    errors.push("SKIPPING DUPLICATE HEADER LINE: "+hlines[i]);
561
                } else if (/#-#-#-#-#/.test(keylow)) {
562
                    errors.push("SKIPPING ERROR MARKER IN HEADER: "+hlines[i]);
563
                } else {
564
                    // remove begining spaces if any
565
                    val = val.replace(/^\s+/, '');
566
                    cur[keylow] = val;
567
                }
568
569
            } else {
570
                errors.push("PROBLEM LINE IN HEADER: "+hlines[i]);
571
                cur[hlines[i]] = '';
572
            }
573
        }
574
575
        // replace header string with assoc array
576
        rv[""] = cur;
577
    } else {
578
        rv[""] = {};
579
    }
580
581
    // TODO: XXX: if there are errors parsing, what do we want to do?
582
    // GNU Gettext silently ignores errors. So will we.
583
    // alert( "Errors parsing po file:\n" + errors.join("\n") );
584
585
    return rv;
586
};
587
588
589
Gettext.prototype.parse_po_dequote = function(str) {
590
    var match;
591
    if (match = str.match(/^"(.*)"/)) {
592
        str = match[1];
593
    }
594
    // unescale all embedded quotes (fixes bug #17504)
595
    str = str.replace(/\\"/g, "\"");
596
    return str;
597
};
598
599
600
// try_load_lang_json : do an ajaxy call to load in the lang defs
601
Gettext.prototype.try_load_lang_json = function(uri) {
602
    var data = this.sjax(uri);
603
    if (! data) return;
604
605
    var rv = this.JSON(data);
606
    this.parse_locale_data(rv);
607
608
    return 1;
609
};
610
611
// this finds all <link> tags, filters out ones that match our
612
// specs, and returns a list of hashes of those
613
Gettext.prototype.get_lang_refs = function() {
614
    var langs = new Array();
615
    var links = document.getElementsByTagName("link");
616
    // find all <link> tags in dom; filter ours
617
    for (var i=0; i<links.length; i++) {
618
        if (links[i].rel == 'gettext' && links[i].href) {
619
            if (typeof(links[i].type) == 'undefined' ||
620
                links[i].type == '') {
621
                if (/\.json$/i.test(links[i].href)) {
622
                    links[i].type = 'application/json';
623
                } else if (/\.js$/i.test(links[i].href)) {
624
                    links[i].type = 'application/json';
625
                } else if (/\.po$/i.test(links[i].href)) {
626
                    links[i].type = 'application/x-po';
627
                } else if (/\.mo$/i.test(links[i].href)) {
628
                    links[i].type = 'application/x-mo';
629
                } else {
630
                    throw new Error("LINK tag with rel=gettext found, but the type and extension are unrecognized.");
631
                }
632
            }
633
634
            links[i].type = links[i].type.toLowerCase();
635
            if (links[i].type == 'application/json') {
636
                links[i].type = 'application/json';
637
            } else if (links[i].type == 'text/javascript') {
638
                links[i].type = 'application/json';
639
            } else if (links[i].type == 'application/x-po') {
640
                links[i].type = 'application/x-po';
641
            } else if (links[i].type == 'application/x-mo') {
642
                links[i].type = 'application/x-mo';
643
            } else {
644
                throw new Error("LINK tag with rel=gettext found, but the type attribute ["+links[i].type+"] is unrecognized.");
645
            }
646
647
            langs.push(links[i]);
648
        }
649
    }
650
    return langs;
651
};
652
653
654
/*
655
656
=head2 textdomain( domain )
657
658
Set domain for future gettext() calls
659
660
A  message  domain  is  a  set of translatable msgid messages. Usually,
661
every software package has its own message domain. The domain  name  is
662
used to determine the message catalog where a translation is looked up;
663
it must be a non-empty string.
664
665
The current message domain is used by the gettext, ngettext, pgettext,
666
npgettext functions, and by the dgettext, dcgettext, dngettext, dcngettext,
667
dpgettext, dcpgettext, dnpgettext and dcnpgettext functions when called
668
with a NULL domainname argument.
669
670
If domainname is not NULL, the current message domain is set to
671
domainname.
672
673
If domainname is undefined, null, or empty string, the function returns
674
the current message domain.
675
676
If  successful,  the  textdomain  function  returns the current message
677
domain, after possibly changing it. (ie. if you set a new domain, the
678
value returned will NOT be the previous domain).
679
680
=cut
681
682
*/
683
Gettext.prototype.textdomain = function (domain) {
684
    if (domain && domain.length) this.domain = domain;
685
    return this.domain;
686
}
687
688
/*
689
690
=head2 gettext( MSGID )
691
692
Returns the translation for B<MSGID>.  Example:
693
694
    alert( gt.gettext("Hello World!\n") );
695
696
If no translation can be found, the unmodified B<MSGID> is returned,
697
i. e. the function can I<never> fail, and will I<never> mess up your
698
original message.
699
700
One common mistake is to interpolate a variable into the string like this:
701
702
  var translated = gt.gettext("Hello " + full_name);
703
704
The interpolation will happen before it's passed to gettext, and it's
705
unlikely you'll have a translation for every "Hello Tom" and "Hello Dick"
706
and "Hellow Harry" that may arise.
707
708
Use C<strargs()> (see below) to solve this problem:
709
710
  var translated = Gettext.strargs( gt.gettext("Hello %1"), [full_name] );
711
712
This is espeically useful when multiple replacements are needed, as they
713
may not appear in the same order within the translation. As an English to
714
French example:
715
716
  Expected result: "This is the red ball"
717
  English: "This is the %1 %2"
718
  French:  "C'est le %2 %1"
719
  Code: Gettext.strargs( gt.gettext("This is the %1 %2"), ["red", "ball"] );
720
721
(The example is stupid because neither color nor thing will get
722
translated here ...).
723
724
=head2 dgettext( TEXTDOMAIN, MSGID )
725
726
Like gettext(), but retrieves the message for the specified
727
B<TEXTDOMAIN> instead of the default domain.  In case you wonder what
728
a textdomain is, see above section on the textdomain() call.
729
730
=head2 dcgettext( TEXTDOMAIN, MSGID, CATEGORY )
731
732
Like dgettext() but retrieves the message from the specified B<CATEGORY>
733
instead of the default category C<LC_MESSAGES>.
734
735
NOTE: the categories are really useless in javascript context. This is
736
here for GNU Gettext API compatability. In practice, you'll never need
737
to use this. This applies to all the calls including the B<CATEGORY>.
738
739
740
=head2 ngettext( MSGID, MSGID_PLURAL, COUNT )
741
742
Retrieves the correct translation for B<COUNT> items.  In legacy software
743
you will often find something like:
744
745
    alert( count + " file(s) deleted.\n" );
746
747
or
748
749
    printf(count + " file%s deleted.\n", $count == 1 ? '' : 's');
750
751
I<NOTE: javascript lacks a builtin printf, so the above isn't a working example>
752
753
The first example looks awkward, the second will only work in English
754
and languages with similar plural rules.  Before ngettext() was introduced,
755
the best practice for internationalized programs was:
756
757
    if (count == 1) {
758
        alert( gettext("One file deleted.\n") );
759
    } else {
760
        printf( gettext("%d files deleted.\n"), count );
761
    }
762
763
This is a nuisance for the programmer and often still not sufficient
764
for an adequate translation.  Many languages have completely different
765
ideas on numerals.  Some (French, Italian, ...) treat 0 and 1 alike,
766
others make no distinction at all (Japanese, Korean, Chinese, ...),
767
others have two or more plural forms (Russian, Latvian, Czech,
768
Polish, ...).  The solution is:
769
770
    printf( ngettext("One file deleted.\n",
771
                     "%d files deleted.\n",
772
                     count), // argument to ngettext!
773
            count);          // argument to printf!
774
775
In English, or if no translation can be found, the first argument
776
(B<MSGID>) is picked if C<count> is one, the second one otherwise.
777
For other languages, the correct plural form (of 1, 2, 3, 4, ...)
778
is automatically picked, too.  You don't have to know anything about
779
the plural rules in the target language, ngettext() will take care
780
of that.
781
782
This is most of the time sufficient but you will have to prove your
783
creativity in cases like
784
785
    "%d file(s) deleted, and %d file(s) created.\n"
786
787
That said, javascript lacks C<printf()> support. Supplied with Gettext.js
788
is the C<strargs()> method, which can be used for these cases:
789
790
    Gettext.strargs( gt.ngettext( "One file deleted.\n",
791
                                  "%d files deleted.\n",
792
                                  count), // argument to ngettext!
793
                     count); // argument to strargs!
794
795
NOTE: the variable replacement isn't done for you, so you must
796
do it yourself as in the above.
797
798
=head2 dngettext( TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT )
799
800
Like ngettext() but retrieves the translation from the specified
801
textdomain instead of the default domain.
802
803
=head2 dcngettext( TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT, CATEGORY )
804
805
Like dngettext() but retrieves the translation from the specified
806
category, instead of the default category C<LC_MESSAGES>.
807
808
809
=head2 pgettext( MSGCTXT, MSGID )
810
811
Returns the translation of MSGID, given the context of MSGCTXT.
812
813
Both items are used as a unique key into the message catalog.
814
815
This allows the translator to have two entries for words that may
816
translate to different foreign words based on their context. For
817
example, the word "View" may be a noun or a verb, which may be
818
used in a menu as File->View or View->Source.
819
820
    alert( pgettext( "Verb: To View", "View" ) );
821
    alert( pgettext( "Noun: A View", "View"  ) );
822
823
The above will both lookup different entries in the message catalog.
824
825
In English, or if no translation can be found, the second argument
826
(B<MSGID>) is returned.
827
828
=head2 dpgettext( TEXTDOMAIN, MSGCTXT, MSGID )
829
830
Like pgettext(), but retrieves the message for the specified
831
B<TEXTDOMAIN> instead of the default domain.
832
833
=head2 dcpgettext( TEXTDOMAIN, MSGCTXT, MSGID, CATEGORY )
834
835
Like dpgettext() but retrieves the message from the specified B<CATEGORY>
836
instead of the default category C<LC_MESSAGES>.
837
838
839
=head2 npgettext( MSGCTXT, MSGID, MSGID_PLURAL, COUNT )
840
841
Like ngettext() with the addition of context as in pgettext().
842
843
In English, or if no translation can be found, the second argument
844
(MSGID) is picked if B<COUNT> is one, the third one otherwise.
845
846
=head2 dnpgettext( TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT )
847
848
Like npgettext() but retrieves the translation from the specified
849
textdomain instead of the default domain.
850
851
=head2 dcnpgettext( TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT, CATEGORY )
852
853
Like dnpgettext() but retrieves the translation from the specified
854
category, instead of the default category C<LC_MESSAGES>.
855
856
=cut
857
858
*/
859
860
// gettext
861
Gettext.prototype.gettext = function (msgid) {
862
    var msgctxt;
863
    var msgid_plural;
864
    var n;
865
    var category;
866
    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
867
};
868
869
Gettext.prototype.dgettext = function (domain, msgid) {
870
    var msgctxt;
871
    var msgid_plural;
872
    var n;
873
    var category;
874
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
875
};
876
877
Gettext.prototype.dcgettext = function (domain, msgid, category) {
878
    var msgctxt;
879
    var msgid_plural;
880
    var n;
881
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
882
};
883
884
// ngettext
885
Gettext.prototype.ngettext = function (msgid, msgid_plural, n) {
886
    var msgctxt;
887
    var category;
888
    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
889
};
890
891
Gettext.prototype.dngettext = function (domain, msgid, msgid_plural, n) {
892
    var msgctxt;
893
    var category;
894
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
895
};
896
897
Gettext.prototype.dcngettext = function (domain, msgid, msgid_plural, n, category) {
898
    var msgctxt;
899
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category, category);
900
};
901
902
// pgettext
903
Gettext.prototype.pgettext = function (msgctxt, msgid) {
904
    var msgid_plural;
905
    var n;
906
    var category;
907
    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
908
};
909
910
Gettext.prototype.dpgettext = function (domain, msgctxt, msgid) {
911
    var msgid_plural;
912
    var n;
913
    var category;
914
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
915
};
916
917
Gettext.prototype.dcpgettext = function (domain, msgctxt, msgid, category) {
918
    var msgid_plural;
919
    var n;
920
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
921
};
922
923
// npgettext
924
Gettext.prototype.npgettext = function (msgctxt, msgid, msgid_plural, n) {
925
    var category;
926
    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
927
};
928
929
Gettext.prototype.dnpgettext = function (domain, msgctxt, msgid, msgid_plural, n) {
930
    var category;
931
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
932
};
933
934
// this has all the options, so we use it for all of them.
935
Gettext.prototype.dcnpgettext = function (domain, msgctxt, msgid, msgid_plural, n, category) {
936
    if (! this.isValidObject(msgid)) return '';
937
938
    var plural = this.isValidObject(msgid_plural);
939
    var msg_ctxt_id = this.isValidObject(msgctxt) ? msgctxt+Gettext.context_glue+msgid : msgid;
940
941
    var domainname = this.isValidObject(domain)      ? domain :
942
                     this.isValidObject(this.domain) ? this.domain :
943
                                                       'messages';
944
945
    // category is always LC_MESSAGES. We ignore all else
946
    var category_name = 'LC_MESSAGES';
947
    var category = 5;
948
949
    var locale_data = new Array();
950
    if (typeof(Gettext._locale_data) != 'undefined' &&
951
        this.isValidObject(Gettext._locale_data[domainname])) {
952
        locale_data.push( Gettext._locale_data[domainname] );
953
954
    } else if (typeof(Gettext._locale_data) != 'undefined') {
955
        // didn't find domain we're looking for. Search all of them.
956
        for (var dom in Gettext._locale_data) {
957
            locale_data.push( Gettext._locale_data[dom] );
958
        }
959
    }
960
961
    var trans = [];
962
    var found = false;
963
    var domain_used; // so we can find plural-forms if needed
964
    if (locale_data.length) {
965
        for (var i=0; i<locale_data.length; i++) {
966
            var locale = locale_data[i];
967
            if (this.isValidObject(locale.msgs[msg_ctxt_id])) {
968
                // make copy of that array (cause we'll be destructive)
969
                for (var j=0; j<locale.msgs[msg_ctxt_id].length; j++) {
970
                    trans[j] = locale.msgs[msg_ctxt_id][j];
971
                }
972
                trans.shift(); // throw away the msgid_plural
973
                domain_used = locale;
974
                found = true;
975
                // only break if found translation actually has a translation.
976
                if ( trans.length > 0 && trans[0].length != 0 )
977
                    break;
978
            }
979
        }
980
    }
981
982
    // default to english if we lack a match, or match has zero length
983
    if ( trans.length == 0 || trans[0].length == 0 ) {
984
        trans = [ msgid, msgid_plural ];
985
    }
986
987
    var translation = trans[0];
988
    if (plural) {
989
        var p;
990
        if (found && this.isValidObject(domain_used.head.plural_func) ) {
991
            var rv = domain_used.head.plural_func(n);
992
            if (! rv.plural) rv.plural = 0;
993
            if (! rv.nplural) rv.nplural = 0;
994
            // if plurals returned is out of bound for total plural forms
995
            if (rv.nplural <= rv.plural) rv.plural = 0;
996
            p = rv.plural;
997
        } else {
998
            p = (n != 1) ? 1 : 0;
999
        }
1000
        if (this.isValidObject(trans[p]))
1001
            translation = trans[p];
1002
    }
1003
1004
    return translation;
1005
};
1006
1007
1008
/*
1009
1010
=head2 strargs (string, argument_array)
1011
1012
  string : a string that potentially contains formatting characters.
1013
  argument_array : an array of positional replacement values
1014
1015
This is a utility method to provide some way to support positional parameters within a string, as javascript lacks a printf() method.
1016
1017
The format is similar to printf(), but greatly simplified (ie. fewer features).
1018
1019
Any percent signs followed by numbers are replaced with the corrosponding item from the B<argument_array>.
1020
1021
Example:
1022
1023
    var string = "%2 roses are red, %1 violets are blue";
1024
    var args   = new Array("10", "15");
1025
    var result = Gettext.strargs(string, args);
1026
    // result is "15 roses are red, 10 violets are blue"
1027
1028
The format numbers are 1 based, so the first itme is %1.
1029
1030
A lone percent sign may be escaped by preceeding it with another percent sign.
1031
1032
A percent sign followed by anything other than a number or another percent sign will be passed through as is.
1033
1034
Some more examples should clear up any abmiguity. The following were called with the orig string, and the array as Array("[one]", "[two]") :
1035
1036
  orig string "blah" becomes "blah"
1037
  orig string "" becomes ""
1038
  orig string "%%" becomes "%"
1039
  orig string "%%%" becomes "%%"
1040
  orig string "%%%%" becomes "%%"
1041
  orig string "%%%%%" becomes "%%%"
1042
  orig string "tom%%dick" becomes "tom%dick"
1043
  orig string "thing%1bob" becomes "thing[one]bob"
1044
  orig string "thing%1%2bob" becomes "thing[one][two]bob"
1045
  orig string "thing%1asdf%2asdf" becomes "thing[one]asdf[two]asdf"
1046
  orig string "%1%2%3" becomes "[one][two]"
1047
  orig string "tom%1%%2%aDick" becomes "tom[one]%2%aDick"
1048
1049
This is especially useful when using plurals, as the string will nearly always contain the number.
1050
1051
It's also useful in translated strings where the translator may have needed to move the position of the parameters.
1052
1053
For example:
1054
1055
  var count = 14;
1056
  Gettext.strargs( gt.ngettext('one banana', '%1 bananas', count), [count] );
1057
1058
NOTE: this may be called as an instance method, or as a class method.
1059
1060
  // instance method:
1061
  var gt = new Gettext(params);
1062
  gt.strargs(string, args);
1063
1064
  // class method:
1065
  Gettext.strargs(string, args);
1066
1067
=cut
1068
1069
*/
1070
/* utility method, since javascript lacks a printf */
1071
Gettext.strargs = function (str, args) {
1072
    // make sure args is an array
1073
    if ( null == args ||
1074
         'undefined' == typeof(args) ) {
1075
        args = [];
1076
    } else if (args.constructor != Array) {
1077
        args = [args];
1078
    }
1079
1080
    // NOTE: javascript lacks support for zero length negative look-behind
1081
    // in regex, so we must step through w/ index.
1082
    // The perl equiv would simply be:
1083
    //    $string =~ s/(?<!\%)\%([0-9]+)/$args[$1]/g;
1084
    //    $string =~ s/\%\%/\%/g; # restore escaped percent signs
1085
1086
    var newstr = "";
1087
    while (true) {
1088
        var i = str.indexOf('%');
1089
        var match_n;
1090
1091
        // no more found. Append whatever remains
1092
        if (i == -1) {
1093
            newstr += str;
1094
            break;
1095
        }
1096
1097
        // we found it, append everything up to that
1098
        newstr += str.substr(0, i);
1099
1100
        // check for escpaed %%
1101
        if (str.substr(i, 2) == '%%') {
1102
            newstr += '%';
1103
            str = str.substr((i+2));
1104
1105
        // % followed by number
1106
        } else if ( match_n = str.substr(i).match(/^%(\d+)/) ) {
1107
            var arg_n = parseInt(match_n[1]);
1108
            var length_n = match_n[1].length;
1109
            if ( arg_n > 0 && args[arg_n -1] != null && typeof(args[arg_n -1]) != 'undefined' )
1110
                newstr += args[arg_n -1];
1111
            str = str.substr( (i + 1 + length_n) );
1112
1113
        // % followed by some other garbage - just remove the %
1114
        } else {
1115
            newstr += '%';
1116
            str = str.substr((i+1));
1117
        }
1118
    }
1119
1120
    return newstr;
1121
}
1122
1123
/* instance method wrapper of strargs */
1124
Gettext.prototype.strargs = function (str, args) {
1125
    return Gettext.strargs(str, args);
1126
}
1127
1128
/* verify that something is an array */
1129
Gettext.prototype.isArray = function (thisObject) {
1130
    return this.isValidObject(thisObject) && thisObject.constructor == Array;
1131
};
1132
1133
/* verify that an object exists and is valid */
1134
Gettext.prototype.isValidObject = function (thisObject) {
1135
    if (null == thisObject) {
1136
        return false;
1137
    } else if ('undefined' == typeof(thisObject) ) {
1138
        return false;
1139
    } else {
1140
        return true;
1141
    }
1142
};
1143
1144
Gettext.prototype.sjax = function (uri) {
1145
    var xmlhttp;
1146
    if (window.XMLHttpRequest) {
1147
        xmlhttp = new XMLHttpRequest();
1148
    } else if (navigator.userAgent.toLowerCase().indexOf('msie 5') != -1) {
1149
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
1150
    } else {
1151
        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
1152
    }
1153
1154
    if (! xmlhttp)
1155
        throw new Error("Your browser doesn't do Ajax. Unable to support external language files.");
1156
1157
    xmlhttp.open('GET', uri, false);
1158
    try { xmlhttp.send(null); }
1159
    catch (e) { return; }
1160
1161
    // we consider status 200 and 0 as ok.
1162
    // 0 happens when we request local file, allowing this to run on local files
1163
    var sjax_status = xmlhttp.status;
1164
    if (sjax_status == 200 || sjax_status == 0) {
1165
        return xmlhttp.responseText;
1166
    } else {
1167
        var error = xmlhttp.statusText + " (Error " + xmlhttp.status + ")";
1168
        if (xmlhttp.responseText.length) {
1169
            error += "\n" + xmlhttp.responseText;
1170
        }
1171
        alert(error);
1172
        return;
1173
    }
1174
}
1175
1176
Gettext.prototype.JSON = function (data) {
1177
    return eval('(' + data + ')');
1178
}
1179
1180
1181
/*
1182
1183
=head1 NOTES
1184
1185
These are some notes on the internals
1186
1187
=over
1188
1189
=item LOCALE CACHING
1190
1191
Loaded locale data is currently cached class-wide. This means that if two scripts are both using Gettext.js, and both share the same gettext domain, that domain will only be loaded once. This will allow you to grab a new object many times from different places, utilize the same domain, and share a single translation file. The downside is that a domain won't be RE-loaded if a new object is instantiated on a domain that had already been instantiated.
1192
1193
=back
1194
1195
=head1 BUGS / TODO
1196
1197
=over
1198
1199
=item error handling
1200
1201
Currently, there are several places that throw errors. In GNU Gettext, there are no fatal errors, which allows text to still be displayed regardless of how broken the environment becomes. We should evaluate and determine where we want to stand on that issue.
1202
1203
=item syncronous only support (no ajax support)
1204
1205
Currently, fetching language data is done purely syncronous, which means the page will halt while those files are fetched/loaded.
1206
1207
This is often what you want, as then following translation requests will actually be translated. However, if all your calls are done dynamically (ie. error handling only or something), loading in the background may be more adventagous.
1208
1209
It's still recommended to use the statically defined <script ...> method, which should have the same delay, but it will cache the result.
1210
1211
=item domain support
1212
1213
domain support while using shortcut methods like C<_('string')> or C<i18n('string')>.
1214
1215
Under normal apps, the domain is usually set globally to the app, and a single language file is used. Under javascript, you may have multiple libraries or applications needing translation support, but the namespace is essentially global.
1216
1217
It's recommended that your app initialize it's own shortcut with it's own domain.  (See examples/wrapper/i18n.js for an example.)
1218
1219
Basically, you'll want to accomplish something like this:
1220
1221
    // in some other .js file that needs i18n
1222
    this.i18nObj = new i18n;
1223
    this.i18n = this.i18nObj.init('domain');
1224
    // do translation
1225
    alert( this.i18n("string") );
1226
1227
If you use this raw Gettext object, then this is all handled for you, as you have your own object then, and will be calling C<myGettextObject.gettext('string')> and such.
1228
1229
1230
=item encoding
1231
1232
May want to add encoding/reencoding stuff. See GNU iconv, or the perl module Locale::Recode from libintl-perl.
1233
1234
=back
1235
1236
1237
=head1 COMPATABILITY
1238
1239
This has been tested on the following browsers. It may work on others, but these are all those to which I have access.
1240
1241
    FF1.5, FF2, FF3, IE6, IE7, Opera9, Opera10, Safari3.1, Chrome
1242
1243
    *FF = Firefox
1244
    *IE = Internet Explorer
1245
1246
1247
=head1 REQUIRES
1248
1249
bin/po2json requires perl, and the perl modules Locale::PO and JSON.
1250
1251
=head1 SEE ALSO
1252
1253
bin/po2json (included),
1254
examples/normal/index.html,
1255
examples/wrapper/i18n.html, examples/wrapper/i18n.js,
1256
Locale::gettext_pp(3pm), POSIX(3pm), gettext(1), gettext(3)
1257
1258
=head1 AUTHOR
1259
1260
Copyright (C) 2008, Joshua I. Miller E<lt>unrtst@cpan.orgE<gt>, all rights reserved. See the source code for details.
1261
1262
=cut
1263
1264
*/
(-)a/koha-tmpl/intranet-tmpl/js/i18n.js (+51 lines)
Line 0 Link Here
1
(function() {
2
  var params = {
3
    "domain": "Koha"
4
  };
5
  if (typeof json_locale_data !== 'undefined') {
6
    params.locale_data = json_locale_data;
7
  }
8
9
  Koha.i18n = {
10
    gt: new Gettext(params),
11
12
    expand: function(text, vars) {
13
      var replace_callback = function(match, name) {
14
        return name in vars ? vars[name] : match;
15
      };
16
      return text.replace(/\{(.*?)\}/g, replace_callback);
17
    }
18
  };
19
})();
20
21
function __(msgid) {
22
  return Koha.i18n.gt.gettext(msgid);
23
}
24
25
function __x(msgid, vars) {
26
  return Koha.i18n.expand(__(msgid), vars);
27
}
28
29
function __n(msgid, msgid_plural, count) {
30
  return Koha.i18n.gt.ngettext(msgid, msgid_plural, count);
31
}
32
33
function __nx(msgid, msgid_plural, count, vars) {
34
  return Koha.i18n.expand(__n(msgid, msgid_plural, count), vars);
35
}
36
37
function __p(msgctxt, msgid) {
38
  return Koha.i18n.gt.pgettext(msgctxt, msgid);
39
}
40
41
function __px(msgctxt, msgid, vars) {
42
  return Koha.i18n.expand(__p(msgctxt, msgid), vars);
43
}
44
45
function __np(msgctxt, msgid, msgid_plural, count) {
46
  return Koha.i18n.gt.npgettext(msgctxt, msgid, msgid_plural, count);
47
}
48
49
function __npx(msgctxt, msgid, msgid_plural, count, vars) {
50
  return Koha.i18n.expand(__np(msgctxt, msgid, msgid_plural, count), vars);
51
}
(-)a/koha-tmpl/intranet-tmpl/prog/en/includes/doc-head-close.inc (+8 lines)
Lines 18-23 Link Here
18
[% INCLUDE intranetstylesheet.inc %]
18
[% INCLUDE intranetstylesheet.inc %]
19
[% IF ( bidi ) %]<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/right-to-left.css" />[% END %]
19
[% IF ( bidi ) %]<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/right-to-left.css" />[% END %]
20
20
21
<script>
22
var Koha = {};
23
</script>
24
25
<script src="[% themelang %]/js/locale_data.js"></script>
26
<script src="[% interface %]/js/Gettext.js"></script>
27
<script src="[% interface %]/js/i18n.js"></script>
28
21
[% IF ( login ) %]
29
[% IF ( login ) %]
22
    <link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/login.css" />
30
    <link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/css/login.css" />
23
[% END %]
31
[% END %]
(-)a/koha-tmpl/opac-tmpl/bootstrap/en/includes/doc-head-close.inc (+8 lines)
Lines 53-55 Link Here
53
<script type="text/javascript" src="[% interface %]/[% theme %]/lib/modernizr.min.js"></script>
53
<script type="text/javascript" src="[% interface %]/[% theme %]/lib/modernizr.min.js"></script>
54
<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/lib/font-awesome/css/font-awesome.min.css" />
54
<link rel="stylesheet" type="text/css" href="[% interface %]/[% theme %]/lib/font-awesome/css/font-awesome.min.css" />
55
[% PROCESS 'html_helpers.inc' %]
55
[% PROCESS 'html_helpers.inc' %]
56
57
<script>
58
var Koha = {};
59
</script>
60
61
<script src="[% themelang %]/js/locale_data.js"></script>
62
<script src="[% interface %]/[% theme %]/js/Gettext.js"></script>
63
<script src="[% interface %]/[% theme %]/js/i18n.js"></script>
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/Gettext.js (+1264 lines)
Line 0 Link Here
1
/*
2
Pure Javascript implementation of Uniforum message translation.
3
Copyright (C) 2008 Joshua I. Miller <unrtst@cpan.org>, all rights reserved
4
5
This program is free software; you can redistribute it and/or modify it
6
under the terms of the GNU Library General Public License as published
7
by the Free Software Foundation; either version 2, or (at your option)
8
any later version.
9
10
This program is distributed in the hope that it will be useful,
11
but WITHOUT ANY WARRANTY; without even the implied warranty of
12
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13
Library General Public License for more details.
14
15
You should have received a copy of the GNU Library General Public
16
License along with this program; if not, write to the Free Software
17
Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
18
USA.
19
20
=head1 NAME
21
22
Javascript Gettext - Javascript implemenation of GNU Gettext API.
23
24
=head1 SYNOPSIS
25
26
 // //////////////////////////////////////////////////////////
27
 // Optimum caching way
28
 <script language="javascript" src="/path/LC_MESSAGES/myDomain.json"></script>
29
 <script language="javascript" src="/path/Gettext.js'></script>
30
31
 // assuming myDomain.json defines variable json_locale_data
32
 var params = {  "domain" : "myDomain",
33
                 "locale_data" : json_locale_data
34
              };
35
 var gt = new Gettext(params);
36
 // create a shortcut if you'd like
37
 function _ (msgid) { return gt.gettext(msgid); }
38
 alert(_("some string"));
39
 // or use fully named method
40
 alert(gt.gettext("some string"));
41
 // change to use a different "domain"
42
 gt.textdomain("anotherDomain");
43
 alert(gt.gettext("some string"));
44
45
46
 // //////////////////////////////////////////////////////////
47
 // The other way to load the language lookup is a "link" tag
48
 // Downside is that not all browsers cache XMLHttpRequests the
49
 // same way, so caching of the language data isn't guarenteed
50
 // across page loads.
51
 // Upside is that it's easy to specify multiple files
52
 <link rel="gettext" href="/path/LC_MESSAGES/myDomain.json" />
53
 <script language="javascript" src="/path/Gettext.js'></script>
54
55
 var gt = new Gettext({ "domain" : "myDomain" });
56
 // rest is the same
57
58
59
 // //////////////////////////////////////////////////////////
60
 // The reson the shortcuts aren't exported by default is because they'd be
61
 // glued to the single domain you created. So, if you're adding i18n support
62
 // to some js library, you should use it as so:
63
64
 if (typeof(MyNamespace) == 'undefined') MyNamespace = {};
65
 MyNamespace.MyClass = function () {
66
     var gtParms = { "domain" : 'MyNamespace_MyClass' };
67
     this.gt = new Gettext(gtParams);
68
     return this;
69
 };
70
 MyNamespace.MyClass.prototype._ = function (msgid) {
71
     return this.gt.gettext(msgid);
72
 };
73
 MyNamespace.MyClass.prototype.something = function () {
74
     var myString = this._("this will get translated");
75
 };
76
77
 // //////////////////////////////////////////////////////////
78
 // Adding the shortcuts to a global scope is easier. If that's
79
 // ok in your app, this is certainly easier.
80
 var myGettext = new Gettext({ 'domain' : 'myDomain' });
81
 function _ (msgid) {
82
     return myGettext.gettext(msgid);
83
 }
84
 alert( _("text") );
85
86
 // //////////////////////////////////////////////////////////
87
 // Data structure of the json data
88
 // NOTE: if you're loading via the <script> tag, you can only
89
 // load one file, but it can contain multiple domains.
90
 var json_locale_data = {
91
     "MyDomain" : {
92
         "" : {
93
             "header_key" : "header value",
94
             "header_key" : "header value",
95
         "msgid" : [ "msgid_plural", "msgstr", "msgstr_plural", "msgstr_pluralN" ],
96
         "msgctxt\004msgid" : [ null, "msgstr" ],
97
         },
98
     "AnotherDomain" : {
99
         },
100
     }
101
102
=head1 DESCRIPTION
103
104
This is a javascript implementation of GNU Gettext, providing internationalization support for javascript. It differs from existing javascript implementations in that it will support all current Gettext features (ex. plural and context support), and will also support loading language catalogs from .mo, .po, or preprocessed json files (converter included).
105
106
The locale initialization differs from that of GNU Gettext / POSIX. Rather than setting the category, domain, and paths, and letting the libs find the right file, you must explicitly load the file at some point. The "domain" will still be honored. Future versions may be expanded to include support for set_locale like features.
107
108
109
=head1 INSTALL
110
111
To install this module, simply copy the file lib/Gettext.js to a web accessable location, and reference it from your application.
112
113
114
=head1 CONFIGURATION
115
116
Configure in one of two ways:
117
118
=over
119
120
=item 1. Optimal. Load language definition from statically defined json data.
121
122
    <script language="javascript" src="/path/locale/domain.json"></script>
123
124
    // in domain.json
125
    json_locale_data = {
126
        "mydomain" : {
127
            // po header fields
128
            "" : {
129
                "plural-forms" : "...",
130
                "lang" : "en",
131
                },
132
            // all the msgid strings and translations
133
            "msgid" : [ "msgid_plural", "translation", "plural_translation" ],
134
        },
135
    };
136
    // please see the included bin/po2json script for the details on this format
137
138
This method also allows you to use unsupported file formats, so long as you can parse them into the above format.
139
140
=item 2. Use AJAX to load language file.
141
142
Use XMLHttpRequest (actually, SJAX - syncronous) to load an external resource.
143
144
Supported external formats are:
145
146
=over
147
148
=item * Javascript Object Notation (.json)
149
150
(see bin/po2json)
151
152
    type=application/json
153
154
=item * Uniforum Portable Object (.po)
155
156
(see GNU Gettext's xgettext)
157
158
    type=application/x-po
159
160
=item * Machine Object (compiled .po) (.mo)
161
162
NOTE: .mo format isn't actually supported just yet, but support is planned.
163
164
(see GNU Gettext's msgfmt)
165
166
    type=application/x-mo
167
168
=back
169
170
=back
171
172
=head1 METHODS
173
174
The following methods are implemented:
175
176
  new Gettext(args)
177
  textdomain  (domain)
178
  gettext     (msgid)
179
  dgettext    (domainname, msgid)
180
  dcgettext   (domainname, msgid, LC_MESSAGES)
181
  ngettext    (msgid, msgid_plural, count)
182
  dngettext   (domainname, msgid, msgid_plural, count)
183
  dcngettext  (domainname, msgid, msgid_plural, count, LC_MESSAGES)
184
  pgettext    (msgctxt, msgid)
185
  dpgettext   (domainname, msgctxt, msgid)
186
  dcpgettext  (domainname, msgctxt, msgid, LC_MESSAGES)
187
  npgettext   (msgctxt, msgid, msgid_plural, count)
188
  dnpgettext  (domainname, msgctxt, msgid, msgid_plural, count)
189
  dcnpgettext (domainname, msgctxt, msgid, msgid_plural, count, LC_MESSAGES)
190
  strargs     (string, args_array)
191
192
193
=head2 new Gettext (args)
194
195
Several methods of loading locale data are included. You may specify a plugin or alternative method of loading data by passing the data in as the "locale_data" option. For example:
196
197
    var get_locale_data = function () {
198
        // plugin does whatever to populate locale_data
199
        return locale_data;
200
    };
201
    var gt = new Gettext( 'domain' : 'messages',
202
                          'locale_data' : get_locale_data() );
203
204
The above can also be used if locale data is specified in a statically included <SCRIPT> tag. Just specify the variable name in the call to new. Ex:
205
206
    var gt = new Gettext( 'domain' : 'messages',
207
                          'locale_data' : json_locale_data_variable );
208
209
Finally, you may load the locale data by referencing it in a <LINK> tag. Simply exclude the 'locale_data' option, and all <LINK rel="gettext" ...> items will be tried. The <LINK> should be specified as:
210
211
    <link rel="gettext" type="application/json" href="/path/to/file.json">
212
    <link rel="gettext" type="text/javascript"  href="/path/to/file.json">
213
    <link rel="gettext" type="application/x-po" href="/path/to/file.po">
214
    <link rel="gettext" type="application/x-mo" href="/path/to/file.mo">
215
216
args:
217
218
=over
219
220
=item domain
221
222
The Gettext domain, not www.whatev.com. It's usually your applications basename. If the .po file was "myapp.po", this would be "myapp".
223
224
=item locale_data
225
226
Raw locale data (in json structure). If specified, from_link data will be ignored.
227
228
=back
229
230
=cut
231
232
*/
233
234
Gettext = function (args) {
235
    this.domain         = 'messages';
236
    // locale_data will be populated from <link...> if not specified in args
237
    this.locale_data    = undefined;
238
239
    // set options
240
    var options = [ "domain", "locale_data" ];
241
    if (this.isValidObject(args)) {
242
        for (var i in args) {
243
            for (var j=0; j<options.length; j++) {
244
                if (i == options[j]) {
245
                    // don't set it if it's null or undefined
246
                    if (this.isValidObject(args[i]))
247
                        this[i] = args[i];
248
                }
249
            }
250
        }
251
    }
252
253
254
    // try to load the lang file from somewhere
255
    this.try_load_lang();
256
257
    return this;
258
}
259
260
Gettext.context_glue = "\004";
261
Gettext._locale_data = {};
262
263
Gettext.prototype.try_load_lang = function() {
264
    // check to see if language is statically included
265
    if (typeof(this.locale_data) != 'undefined') {
266
        // we're going to reformat it, and overwrite the variable
267
        var locale_copy = this.locale_data;
268
        this.locale_data = undefined;
269
        this.parse_locale_data(locale_copy);
270
271
        if (typeof(Gettext._locale_data[this.domain]) == 'undefined') {
272
            throw new Error("Error: Gettext 'locale_data' does not contain the domain '"+this.domain+"'");
273
        }
274
    }
275
276
277
    // try loading from JSON
278
    // get lang links
279
    var lang_link = this.get_lang_refs();
280
281
    if (typeof(lang_link) == 'object' && lang_link.length > 0) {
282
        // NOTE: there will be a delay here, as this is async.
283
        // So, any i18n calls made right after page load may not
284
        // get translated.
285
        // XXX: we may want to see if we can "fix" this behavior
286
        for (var i=0; i<lang_link.length; i++) {
287
            var link = lang_link[i];
288
            if (link.type == 'application/json') {
289
                if (! this.try_load_lang_json(link.href) ) {
290
                    throw new Error("Error: Gettext 'try_load_lang_json' failed. Unable to exec xmlhttprequest for link ["+link.href+"]");
291
                }
292
            } else if (link.type == 'application/x-po') {
293
                if (! this.try_load_lang_po(link.href) ) {
294
                    throw new Error("Error: Gettext 'try_load_lang_po' failed. Unable to exec xmlhttprequest for link ["+link.href+"]");
295
                }
296
            } else {
297
                // TODO: implement the other types (.mo)
298
                throw new Error("TODO: link type ["+link.type+"] found, and support is planned, but not implemented at this time.");
299
            }
300
        }
301
    }
302
};
303
304
// This takes the bin/po2json'd data, and moves it into an internal form
305
// for use in our lib, and puts it in our object as:
306
//  Gettext._locale_data = {
307
//      domain : {
308
//          head : { headfield : headvalue },
309
//          msgs : {
310
//              msgid : [ msgid_plural, msgstr, msgstr_plural ],
311
//          },
312
Gettext.prototype.parse_locale_data = function(locale_data) {
313
    if (typeof(Gettext._locale_data) == 'undefined') {
314
        Gettext._locale_data = { };
315
    }
316
317
    // suck in every domain defined in the supplied data
318
    for (var domain in locale_data) {
319
        // skip empty specs (flexibly)
320
        if ((! locale_data.hasOwnProperty(domain)) || (! this.isValidObject(locale_data[domain])))
321
            continue;
322
        // skip if it has no msgid's
323
        var has_msgids = false;
324
        for (var msgid in locale_data[domain]) {
325
            has_msgids = true;
326
            break;
327
        }
328
        if (! has_msgids) continue;
329
330
        // grab shortcut to data
331
        var data = locale_data[domain];
332
333
        // if they specifcy a blank domain, default to "messages"
334
        if (domain == "") domain = "messages";
335
        // init the data structure
336
        if (! this.isValidObject(Gettext._locale_data[domain]) )
337
            Gettext._locale_data[domain] = { };
338
        if (! this.isValidObject(Gettext._locale_data[domain].head) )
339
            Gettext._locale_data[domain].head = { };
340
        if (! this.isValidObject(Gettext._locale_data[domain].msgs) )
341
            Gettext._locale_data[domain].msgs = { };
342
343
        for (var key in data) {
344
            if (key == "") {
345
                var header = data[key];
346
                for (var head in header) {
347
                    var h = head.toLowerCase();
348
                    Gettext._locale_data[domain].head[h] = header[head];
349
                }
350
            } else {
351
                Gettext._locale_data[domain].msgs[key] = data[key];
352
            }
353
        }
354
    }
355
356
    // build the plural forms function
357
    for (var domain in Gettext._locale_data) {
358
        if (this.isValidObject(Gettext._locale_data[domain].head['plural-forms']) &&
359
            typeof(Gettext._locale_data[domain].head.plural_func) == 'undefined') {
360
            // untaint data
361
            var plural_forms = Gettext._locale_data[domain].head['plural-forms'];
362
            var pf_re = new RegExp('^(\\s*nplurals\\s*=\\s*[0-9]+\\s*;\\s*plural\\s*=\\s*(?:\\s|[-\\?\\|&=!<>+*/%:;a-zA-Z0-9_\(\)])+)', 'm');
363
            if (pf_re.test(plural_forms)) {
364
                //ex english: "Plural-Forms: nplurals=2; plural=(n != 1);\n"
365
                //pf = "nplurals=2; plural=(n != 1);";
366
                //ex russian: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10< =4 && (n%100<10 or n%100>=20) ? 1 : 2)
367
                //pf = "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)";
368
369
                var pf = Gettext._locale_data[domain].head['plural-forms'];
370
                if (! /;\s*$/.test(pf)) pf = pf.concat(';');
371
                /* We used to use eval, but it seems IE has issues with it.
372
                 * We now use "new Function", though it carries a slightly
373
                 * bigger performance hit.
374
                var code = 'function (n) { var plural; var nplurals; '+pf+' return { "nplural" : nplurals, "plural" : (plural === true ? 1 : plural ? plural : 0) }; };';
375
                Gettext._locale_data[domain].head.plural_func = eval("("+code+")");
376
                */
377
                var code = 'var plural; var nplurals; '+pf+' return { "nplural" : nplurals, "plural" : (plural === true ? 1 : plural ? plural : 0) };';
378
                Gettext._locale_data[domain].head.plural_func = new Function("n", code);
379
            } else {
380
                throw new Error("Syntax error in language file. Plural-Forms header is invalid ["+plural_forms+"]");
381
            }
382
383
        // default to english plural form
384
        } else if (typeof(Gettext._locale_data[domain].head.plural_func) == 'undefined') {
385
            Gettext._locale_data[domain].head.plural_func = function (n) {
386
                var p = (n != 1) ? 1 : 0;
387
                return { 'nplural' : 2, 'plural' : p };
388
                };
389
        } // else, plural_func already created
390
    }
391
392
    return;
393
};
394
395
396
// try_load_lang_po : do an ajaxy call to load in the .po lang defs
397
Gettext.prototype.try_load_lang_po = function(uri) {
398
    var data = this.sjax(uri);
399
    if (! data) return;
400
401
    var domain = this.uri_basename(uri);
402
    var parsed = this.parse_po(data);
403
404
    var rv = {};
405
    // munge domain into/outof header
406
    if (parsed) {
407
        if (! parsed[""]) parsed[""] = {};
408
        if (! parsed[""]["domain"]) parsed[""]["domain"] = domain;
409
        domain = parsed[""]["domain"];
410
        rv[domain] = parsed;
411
412
        this.parse_locale_data(rv);
413
    }
414
415
    return 1;
416
};
417
418
Gettext.prototype.uri_basename = function(uri) {
419
    var rv;
420
    if (rv = uri.match(/^(.*\/)?(.*)/)) {
421
        var ext_strip;
422
        if (ext_strip = rv[2].match(/^(.*)\..+$/))
423
            return ext_strip[1];
424
        else
425
            return rv[2];
426
    } else {
427
        return "";
428
    }
429
};
430
431
Gettext.prototype.parse_po = function(data) {
432
    var rv = {};
433
    var buffer = {};
434
    var lastbuffer = "";
435
    var errors = [];
436
    var lines = data.split("\n");
437
    for (var i=0; i<lines.length; i++) {
438
        // chomp
439
        lines[i] = lines[i].replace(/(\n|\r)+$/, '');
440
441
        var match;
442
443
        // Empty line / End of an entry.
444
        if (/^$/.test(lines[i])) {
445
            if (typeof(buffer['msgid']) != 'undefined') {
446
                var msg_ctxt_id = (typeof(buffer['msgctxt']) != 'undefined' &&
447
                                   buffer['msgctxt'].length) ?
448
                                  buffer['msgctxt']+Gettext.context_glue+buffer['msgid'] :
449
                                  buffer['msgid'];
450
                var msgid_plural = (typeof(buffer['msgid_plural']) != 'undefined' &&
451
                                    buffer['msgid_plural'].length) ?
452
                                   buffer['msgid_plural'] :
453
                                   null;
454
455
                // find msgstr_* translations and push them on
456
                var trans = [];
457
                for (var str in buffer) {
458
                    var match;
459
                    if (match = str.match(/^msgstr_(\d+)/))
460
                        trans[parseInt(match[1])] = buffer[str];
461
                }
462
                trans.unshift(msgid_plural);
463
464
                // only add it if we've got a translation
465
                // NOTE: this doesn't conform to msgfmt specs
466
                if (trans.length > 1) rv[msg_ctxt_id] = trans;
467
468
                buffer = {};
469
                lastbuffer = "";
470
            }
471
472
        // comments
473
        } else if (/^#/.test(lines[i])) {
474
            continue;
475
476
        // msgctxt
477
        } else if (match = lines[i].match(/^msgctxt\s+(.*)/)) {
478
            lastbuffer = 'msgctxt';
479
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
480
481
        // msgid
482
        } else if (match = lines[i].match(/^msgid\s+(.*)/)) {
483
            lastbuffer = 'msgid';
484
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
485
486
        // msgid_plural
487
        } else if (match = lines[i].match(/^msgid_plural\s+(.*)/)) {
488
            lastbuffer = 'msgid_plural';
489
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
490
491
        // msgstr
492
        } else if (match = lines[i].match(/^msgstr\s+(.*)/)) {
493
            lastbuffer = 'msgstr_0';
494
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
495
496
        // msgstr[0] (treak like msgstr)
497
        } else if (match = lines[i].match(/^msgstr\[0\]\s+(.*)/)) {
498
            lastbuffer = 'msgstr_0';
499
            buffer[lastbuffer] = this.parse_po_dequote(match[1]);
500
501
        // msgstr[n]
502
        } else if (match = lines[i].match(/^msgstr\[(\d+)\]\s+(.*)/)) {
503
            lastbuffer = 'msgstr_'+match[1];
504
            buffer[lastbuffer] = this.parse_po_dequote(match[2]);
505
506
        // continued string
507
        } else if (/^"/.test(lines[i])) {
508
            buffer[lastbuffer] += this.parse_po_dequote(lines[i]);
509
510
        // something strange
511
        } else {
512
            errors.push("Strange line ["+i+"] : "+lines[i]);
513
        }
514
    }
515
516
517
    // handle the final entry
518
    if (typeof(buffer['msgid']) != 'undefined') {
519
        var msg_ctxt_id = (typeof(buffer['msgctxt']) != 'undefined' &&
520
                           buffer['msgctxt'].length) ?
521
                          buffer['msgctxt']+Gettext.context_glue+buffer['msgid'] :
522
                          buffer['msgid'];
523
        var msgid_plural = (typeof(buffer['msgid_plural']) != 'undefined' &&
524
                            buffer['msgid_plural'].length) ?
525
                           buffer['msgid_plural'] :
526
                           null;
527
528
        // find msgstr_* translations and push them on
529
        var trans = [];
530
        for (var str in buffer) {
531
            var match;
532
            if (match = str.match(/^msgstr_(\d+)/))
533
                trans[parseInt(match[1])] = buffer[str];
534
        }
535
        trans.unshift(msgid_plural);
536
537
        // only add it if we've got a translation
538
        // NOTE: this doesn't conform to msgfmt specs
539
        if (trans.length > 1) rv[msg_ctxt_id] = trans;
540
541
        buffer = {};
542
        lastbuffer = "";
543
    }
544
545
546
    // parse out the header
547
    if (rv[""] && rv[""][1]) {
548
        var cur = {};
549
        var hlines = rv[""][1].split(/\\n/);
550
        for (var i=0; i<hlines.length; i++) {
551
            if (! hlines.length) continue;
552
553
            var pos = hlines[i].indexOf(':', 0);
554
            if (pos != -1) {
555
                var key = hlines[i].substring(0, pos);
556
                var val = hlines[i].substring(pos +1);
557
                var keylow = key.toLowerCase();
558
559
                if (cur[keylow] && cur[keylow].length) {
560
                    errors.push("SKIPPING DUPLICATE HEADER LINE: "+hlines[i]);
561
                } else if (/#-#-#-#-#/.test(keylow)) {
562
                    errors.push("SKIPPING ERROR MARKER IN HEADER: "+hlines[i]);
563
                } else {
564
                    // remove begining spaces if any
565
                    val = val.replace(/^\s+/, '');
566
                    cur[keylow] = val;
567
                }
568
569
            } else {
570
                errors.push("PROBLEM LINE IN HEADER: "+hlines[i]);
571
                cur[hlines[i]] = '';
572
            }
573
        }
574
575
        // replace header string with assoc array
576
        rv[""] = cur;
577
    } else {
578
        rv[""] = {};
579
    }
580
581
    // TODO: XXX: if there are errors parsing, what do we want to do?
582
    // GNU Gettext silently ignores errors. So will we.
583
    // alert( "Errors parsing po file:\n" + errors.join("\n") );
584
585
    return rv;
586
};
587
588
589
Gettext.prototype.parse_po_dequote = function(str) {
590
    var match;
591
    if (match = str.match(/^"(.*)"/)) {
592
        str = match[1];
593
    }
594
    // unescale all embedded quotes (fixes bug #17504)
595
    str = str.replace(/\\"/g, "\"");
596
    return str;
597
};
598
599
600
// try_load_lang_json : do an ajaxy call to load in the lang defs
601
Gettext.prototype.try_load_lang_json = function(uri) {
602
    var data = this.sjax(uri);
603
    if (! data) return;
604
605
    var rv = this.JSON(data);
606
    this.parse_locale_data(rv);
607
608
    return 1;
609
};
610
611
// this finds all <link> tags, filters out ones that match our
612
// specs, and returns a list of hashes of those
613
Gettext.prototype.get_lang_refs = function() {
614
    var langs = new Array();
615
    var links = document.getElementsByTagName("link");
616
    // find all <link> tags in dom; filter ours
617
    for (var i=0; i<links.length; i++) {
618
        if (links[i].rel == 'gettext' && links[i].href) {
619
            if (typeof(links[i].type) == 'undefined' ||
620
                links[i].type == '') {
621
                if (/\.json$/i.test(links[i].href)) {
622
                    links[i].type = 'application/json';
623
                } else if (/\.js$/i.test(links[i].href)) {
624
                    links[i].type = 'application/json';
625
                } else if (/\.po$/i.test(links[i].href)) {
626
                    links[i].type = 'application/x-po';
627
                } else if (/\.mo$/i.test(links[i].href)) {
628
                    links[i].type = 'application/x-mo';
629
                } else {
630
                    throw new Error("LINK tag with rel=gettext found, but the type and extension are unrecognized.");
631
                }
632
            }
633
634
            links[i].type = links[i].type.toLowerCase();
635
            if (links[i].type == 'application/json') {
636
                links[i].type = 'application/json';
637
            } else if (links[i].type == 'text/javascript') {
638
                links[i].type = 'application/json';
639
            } else if (links[i].type == 'application/x-po') {
640
                links[i].type = 'application/x-po';
641
            } else if (links[i].type == 'application/x-mo') {
642
                links[i].type = 'application/x-mo';
643
            } else {
644
                throw new Error("LINK tag with rel=gettext found, but the type attribute ["+links[i].type+"] is unrecognized.");
645
            }
646
647
            langs.push(links[i]);
648
        }
649
    }
650
    return langs;
651
};
652
653
654
/*
655
656
=head2 textdomain( domain )
657
658
Set domain for future gettext() calls
659
660
A  message  domain  is  a  set of translatable msgid messages. Usually,
661
every software package has its own message domain. The domain  name  is
662
used to determine the message catalog where a translation is looked up;
663
it must be a non-empty string.
664
665
The current message domain is used by the gettext, ngettext, pgettext,
666
npgettext functions, and by the dgettext, dcgettext, dngettext, dcngettext,
667
dpgettext, dcpgettext, dnpgettext and dcnpgettext functions when called
668
with a NULL domainname argument.
669
670
If domainname is not NULL, the current message domain is set to
671
domainname.
672
673
If domainname is undefined, null, or empty string, the function returns
674
the current message domain.
675
676
If  successful,  the  textdomain  function  returns the current message
677
domain, after possibly changing it. (ie. if you set a new domain, the
678
value returned will NOT be the previous domain).
679
680
=cut
681
682
*/
683
Gettext.prototype.textdomain = function (domain) {
684
    if (domain && domain.length) this.domain = domain;
685
    return this.domain;
686
}
687
688
/*
689
690
=head2 gettext( MSGID )
691
692
Returns the translation for B<MSGID>.  Example:
693
694
    alert( gt.gettext("Hello World!\n") );
695
696
If no translation can be found, the unmodified B<MSGID> is returned,
697
i. e. the function can I<never> fail, and will I<never> mess up your
698
original message.
699
700
One common mistake is to interpolate a variable into the string like this:
701
702
  var translated = gt.gettext("Hello " + full_name);
703
704
The interpolation will happen before it's passed to gettext, and it's
705
unlikely you'll have a translation for every "Hello Tom" and "Hello Dick"
706
and "Hellow Harry" that may arise.
707
708
Use C<strargs()> (see below) to solve this problem:
709
710
  var translated = Gettext.strargs( gt.gettext("Hello %1"), [full_name] );
711
712
This is espeically useful when multiple replacements are needed, as they
713
may not appear in the same order within the translation. As an English to
714
French example:
715
716
  Expected result: "This is the red ball"
717
  English: "This is the %1 %2"
718
  French:  "C'est le %2 %1"
719
  Code: Gettext.strargs( gt.gettext("This is the %1 %2"), ["red", "ball"] );
720
721
(The example is stupid because neither color nor thing will get
722
translated here ...).
723
724
=head2 dgettext( TEXTDOMAIN, MSGID )
725
726
Like gettext(), but retrieves the message for the specified
727
B<TEXTDOMAIN> instead of the default domain.  In case you wonder what
728
a textdomain is, see above section on the textdomain() call.
729
730
=head2 dcgettext( TEXTDOMAIN, MSGID, CATEGORY )
731
732
Like dgettext() but retrieves the message from the specified B<CATEGORY>
733
instead of the default category C<LC_MESSAGES>.
734
735
NOTE: the categories are really useless in javascript context. This is
736
here for GNU Gettext API compatability. In practice, you'll never need
737
to use this. This applies to all the calls including the B<CATEGORY>.
738
739
740
=head2 ngettext( MSGID, MSGID_PLURAL, COUNT )
741
742
Retrieves the correct translation for B<COUNT> items.  In legacy software
743
you will often find something like:
744
745
    alert( count + " file(s) deleted.\n" );
746
747
or
748
749
    printf(count + " file%s deleted.\n", $count == 1 ? '' : 's');
750
751
I<NOTE: javascript lacks a builtin printf, so the above isn't a working example>
752
753
The first example looks awkward, the second will only work in English
754
and languages with similar plural rules.  Before ngettext() was introduced,
755
the best practice for internationalized programs was:
756
757
    if (count == 1) {
758
        alert( gettext("One file deleted.\n") );
759
    } else {
760
        printf( gettext("%d files deleted.\n"), count );
761
    }
762
763
This is a nuisance for the programmer and often still not sufficient
764
for an adequate translation.  Many languages have completely different
765
ideas on numerals.  Some (French, Italian, ...) treat 0 and 1 alike,
766
others make no distinction at all (Japanese, Korean, Chinese, ...),
767
others have two or more plural forms (Russian, Latvian, Czech,
768
Polish, ...).  The solution is:
769
770
    printf( ngettext("One file deleted.\n",
771
                     "%d files deleted.\n",
772
                     count), // argument to ngettext!
773
            count);          // argument to printf!
774
775
In English, or if no translation can be found, the first argument
776
(B<MSGID>) is picked if C<count> is one, the second one otherwise.
777
For other languages, the correct plural form (of 1, 2, 3, 4, ...)
778
is automatically picked, too.  You don't have to know anything about
779
the plural rules in the target language, ngettext() will take care
780
of that.
781
782
This is most of the time sufficient but you will have to prove your
783
creativity in cases like
784
785
    "%d file(s) deleted, and %d file(s) created.\n"
786
787
That said, javascript lacks C<printf()> support. Supplied with Gettext.js
788
is the C<strargs()> method, which can be used for these cases:
789
790
    Gettext.strargs( gt.ngettext( "One file deleted.\n",
791
                                  "%d files deleted.\n",
792
                                  count), // argument to ngettext!
793
                     count); // argument to strargs!
794
795
NOTE: the variable replacement isn't done for you, so you must
796
do it yourself as in the above.
797
798
=head2 dngettext( TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT )
799
800
Like ngettext() but retrieves the translation from the specified
801
textdomain instead of the default domain.
802
803
=head2 dcngettext( TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT, CATEGORY )
804
805
Like dngettext() but retrieves the translation from the specified
806
category, instead of the default category C<LC_MESSAGES>.
807
808
809
=head2 pgettext( MSGCTXT, MSGID )
810
811
Returns the translation of MSGID, given the context of MSGCTXT.
812
813
Both items are used as a unique key into the message catalog.
814
815
This allows the translator to have two entries for words that may
816
translate to different foreign words based on their context. For
817
example, the word "View" may be a noun or a verb, which may be
818
used in a menu as File->View or View->Source.
819
820
    alert( pgettext( "Verb: To View", "View" ) );
821
    alert( pgettext( "Noun: A View", "View"  ) );
822
823
The above will both lookup different entries in the message catalog.
824
825
In English, or if no translation can be found, the second argument
826
(B<MSGID>) is returned.
827
828
=head2 dpgettext( TEXTDOMAIN, MSGCTXT, MSGID )
829
830
Like pgettext(), but retrieves the message for the specified
831
B<TEXTDOMAIN> instead of the default domain.
832
833
=head2 dcpgettext( TEXTDOMAIN, MSGCTXT, MSGID, CATEGORY )
834
835
Like dpgettext() but retrieves the message from the specified B<CATEGORY>
836
instead of the default category C<LC_MESSAGES>.
837
838
839
=head2 npgettext( MSGCTXT, MSGID, MSGID_PLURAL, COUNT )
840
841
Like ngettext() with the addition of context as in pgettext().
842
843
In English, or if no translation can be found, the second argument
844
(MSGID) is picked if B<COUNT> is one, the third one otherwise.
845
846
=head2 dnpgettext( TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT )
847
848
Like npgettext() but retrieves the translation from the specified
849
textdomain instead of the default domain.
850
851
=head2 dcnpgettext( TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT, CATEGORY )
852
853
Like dnpgettext() but retrieves the translation from the specified
854
category, instead of the default category C<LC_MESSAGES>.
855
856
=cut
857
858
*/
859
860
// gettext
861
Gettext.prototype.gettext = function (msgid) {
862
    var msgctxt;
863
    var msgid_plural;
864
    var n;
865
    var category;
866
    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
867
};
868
869
Gettext.prototype.dgettext = function (domain, msgid) {
870
    var msgctxt;
871
    var msgid_plural;
872
    var n;
873
    var category;
874
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
875
};
876
877
Gettext.prototype.dcgettext = function (domain, msgid, category) {
878
    var msgctxt;
879
    var msgid_plural;
880
    var n;
881
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
882
};
883
884
// ngettext
885
Gettext.prototype.ngettext = function (msgid, msgid_plural, n) {
886
    var msgctxt;
887
    var category;
888
    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
889
};
890
891
Gettext.prototype.dngettext = function (domain, msgid, msgid_plural, n) {
892
    var msgctxt;
893
    var category;
894
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
895
};
896
897
Gettext.prototype.dcngettext = function (domain, msgid, msgid_plural, n, category) {
898
    var msgctxt;
899
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category, category);
900
};
901
902
// pgettext
903
Gettext.prototype.pgettext = function (msgctxt, msgid) {
904
    var msgid_plural;
905
    var n;
906
    var category;
907
    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
908
};
909
910
Gettext.prototype.dpgettext = function (domain, msgctxt, msgid) {
911
    var msgid_plural;
912
    var n;
913
    var category;
914
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
915
};
916
917
Gettext.prototype.dcpgettext = function (domain, msgctxt, msgid, category) {
918
    var msgid_plural;
919
    var n;
920
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
921
};
922
923
// npgettext
924
Gettext.prototype.npgettext = function (msgctxt, msgid, msgid_plural, n) {
925
    var category;
926
    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
927
};
928
929
Gettext.prototype.dnpgettext = function (domain, msgctxt, msgid, msgid_plural, n) {
930
    var category;
931
    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
932
};
933
934
// this has all the options, so we use it for all of them.
935
Gettext.prototype.dcnpgettext = function (domain, msgctxt, msgid, msgid_plural, n, category) {
936
    if (! this.isValidObject(msgid)) return '';
937
938
    var plural = this.isValidObject(msgid_plural);
939
    var msg_ctxt_id = this.isValidObject(msgctxt) ? msgctxt+Gettext.context_glue+msgid : msgid;
940
941
    var domainname = this.isValidObject(domain)      ? domain :
942
                     this.isValidObject(this.domain) ? this.domain :
943
                                                       'messages';
944
945
    // category is always LC_MESSAGES. We ignore all else
946
    var category_name = 'LC_MESSAGES';
947
    var category = 5;
948
949
    var locale_data = new Array();
950
    if (typeof(Gettext._locale_data) != 'undefined' &&
951
        this.isValidObject(Gettext._locale_data[domainname])) {
952
        locale_data.push( Gettext._locale_data[domainname] );
953
954
    } else if (typeof(Gettext._locale_data) != 'undefined') {
955
        // didn't find domain we're looking for. Search all of them.
956
        for (var dom in Gettext._locale_data) {
957
            locale_data.push( Gettext._locale_data[dom] );
958
        }
959
    }
960
961
    var trans = [];
962
    var found = false;
963
    var domain_used; // so we can find plural-forms if needed
964
    if (locale_data.length) {
965
        for (var i=0; i<locale_data.length; i++) {
966
            var locale = locale_data[i];
967
            if (this.isValidObject(locale.msgs[msg_ctxt_id])) {
968
                // make copy of that array (cause we'll be destructive)
969
                for (var j=0; j<locale.msgs[msg_ctxt_id].length; j++) {
970
                    trans[j] = locale.msgs[msg_ctxt_id][j];
971
                }
972
                trans.shift(); // throw away the msgid_plural
973
                domain_used = locale;
974
                found = true;
975
                // only break if found translation actually has a translation.
976
                if ( trans.length > 0 && trans[0].length != 0 )
977
                    break;
978
            }
979
        }
980
    }
981
982
    // default to english if we lack a match, or match has zero length
983
    if ( trans.length == 0 || trans[0].length == 0 ) {
984
        trans = [ msgid, msgid_plural ];
985
    }
986
987
    var translation = trans[0];
988
    if (plural) {
989
        var p;
990
        if (found && this.isValidObject(domain_used.head.plural_func) ) {
991
            var rv = domain_used.head.plural_func(n);
992
            if (! rv.plural) rv.plural = 0;
993
            if (! rv.nplural) rv.nplural = 0;
994
            // if plurals returned is out of bound for total plural forms
995
            if (rv.nplural <= rv.plural) rv.plural = 0;
996
            p = rv.plural;
997
        } else {
998
            p = (n != 1) ? 1 : 0;
999
        }
1000
        if (this.isValidObject(trans[p]))
1001
            translation = trans[p];
1002
    }
1003
1004
    return translation;
1005
};
1006
1007
1008
/*
1009
1010
=head2 strargs (string, argument_array)
1011
1012
  string : a string that potentially contains formatting characters.
1013
  argument_array : an array of positional replacement values
1014
1015
This is a utility method to provide some way to support positional parameters within a string, as javascript lacks a printf() method.
1016
1017
The format is similar to printf(), but greatly simplified (ie. fewer features).
1018
1019
Any percent signs followed by numbers are replaced with the corrosponding item from the B<argument_array>.
1020
1021
Example:
1022
1023
    var string = "%2 roses are red, %1 violets are blue";
1024
    var args   = new Array("10", "15");
1025
    var result = Gettext.strargs(string, args);
1026
    // result is "15 roses are red, 10 violets are blue"
1027
1028
The format numbers are 1 based, so the first itme is %1.
1029
1030
A lone percent sign may be escaped by preceeding it with another percent sign.
1031
1032
A percent sign followed by anything other than a number or another percent sign will be passed through as is.
1033
1034
Some more examples should clear up any abmiguity. The following were called with the orig string, and the array as Array("[one]", "[two]") :
1035
1036
  orig string "blah" becomes "blah"
1037
  orig string "" becomes ""
1038
  orig string "%%" becomes "%"
1039
  orig string "%%%" becomes "%%"
1040
  orig string "%%%%" becomes "%%"
1041
  orig string "%%%%%" becomes "%%%"
1042
  orig string "tom%%dick" becomes "tom%dick"
1043
  orig string "thing%1bob" becomes "thing[one]bob"
1044
  orig string "thing%1%2bob" becomes "thing[one][two]bob"
1045
  orig string "thing%1asdf%2asdf" becomes "thing[one]asdf[two]asdf"
1046
  orig string "%1%2%3" becomes "[one][two]"
1047
  orig string "tom%1%%2%aDick" becomes "tom[one]%2%aDick"
1048
1049
This is especially useful when using plurals, as the string will nearly always contain the number.
1050
1051
It's also useful in translated strings where the translator may have needed to move the position of the parameters.
1052
1053
For example:
1054
1055
  var count = 14;
1056
  Gettext.strargs( gt.ngettext('one banana', '%1 bananas', count), [count] );
1057
1058
NOTE: this may be called as an instance method, or as a class method.
1059
1060
  // instance method:
1061
  var gt = new Gettext(params);
1062
  gt.strargs(string, args);
1063
1064
  // class method:
1065
  Gettext.strargs(string, args);
1066
1067
=cut
1068
1069
*/
1070
/* utility method, since javascript lacks a printf */
1071
Gettext.strargs = function (str, args) {
1072
    // make sure args is an array
1073
    if ( null == args ||
1074
         'undefined' == typeof(args) ) {
1075
        args = [];
1076
    } else if (args.constructor != Array) {
1077
        args = [args];
1078
    }
1079
1080
    // NOTE: javascript lacks support for zero length negative look-behind
1081
    // in regex, so we must step through w/ index.
1082
    // The perl equiv would simply be:
1083
    //    $string =~ s/(?<!\%)\%([0-9]+)/$args[$1]/g;
1084
    //    $string =~ s/\%\%/\%/g; # restore escaped percent signs
1085
1086
    var newstr = "";
1087
    while (true) {
1088
        var i = str.indexOf('%');
1089
        var match_n;
1090
1091
        // no more found. Append whatever remains
1092
        if (i == -1) {
1093
            newstr += str;
1094
            break;
1095
        }
1096
1097
        // we found it, append everything up to that
1098
        newstr += str.substr(0, i);
1099
1100
        // check for escpaed %%
1101
        if (str.substr(i, 2) == '%%') {
1102
            newstr += '%';
1103
            str = str.substr((i+2));
1104
1105
        // % followed by number
1106
        } else if ( match_n = str.substr(i).match(/^%(\d+)/) ) {
1107
            var arg_n = parseInt(match_n[1]);
1108
            var length_n = match_n[1].length;
1109
            if ( arg_n > 0 && args[arg_n -1] != null && typeof(args[arg_n -1]) != 'undefined' )
1110
                newstr += args[arg_n -1];
1111
            str = str.substr( (i + 1 + length_n) );
1112
1113
        // % followed by some other garbage - just remove the %
1114
        } else {
1115
            newstr += '%';
1116
            str = str.substr((i+1));
1117
        }
1118
    }
1119
1120
    return newstr;
1121
}
1122
1123
/* instance method wrapper of strargs */
1124
Gettext.prototype.strargs = function (str, args) {
1125
    return Gettext.strargs(str, args);
1126
}
1127
1128
/* verify that something is an array */
1129
Gettext.prototype.isArray = function (thisObject) {
1130
    return this.isValidObject(thisObject) && thisObject.constructor == Array;
1131
};
1132
1133
/* verify that an object exists and is valid */
1134
Gettext.prototype.isValidObject = function (thisObject) {
1135
    if (null == thisObject) {
1136
        return false;
1137
    } else if ('undefined' == typeof(thisObject) ) {
1138
        return false;
1139
    } else {
1140
        return true;
1141
    }
1142
};
1143
1144
Gettext.prototype.sjax = function (uri) {
1145
    var xmlhttp;
1146
    if (window.XMLHttpRequest) {
1147
        xmlhttp = new XMLHttpRequest();
1148
    } else if (navigator.userAgent.toLowerCase().indexOf('msie 5') != -1) {
1149
        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
1150
    } else {
1151
        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
1152
    }
1153
1154
    if (! xmlhttp)
1155
        throw new Error("Your browser doesn't do Ajax. Unable to support external language files.");
1156
1157
    xmlhttp.open('GET', uri, false);
1158
    try { xmlhttp.send(null); }
1159
    catch (e) { return; }
1160
1161
    // we consider status 200 and 0 as ok.
1162
    // 0 happens when we request local file, allowing this to run on local files
1163
    var sjax_status = xmlhttp.status;
1164
    if (sjax_status == 200 || sjax_status == 0) {
1165
        return xmlhttp.responseText;
1166
    } else {
1167
        var error = xmlhttp.statusText + " (Error " + xmlhttp.status + ")";
1168
        if (xmlhttp.responseText.length) {
1169
            error += "\n" + xmlhttp.responseText;
1170
        }
1171
        alert(error);
1172
        return;
1173
    }
1174
}
1175
1176
Gettext.prototype.JSON = function (data) {
1177
    return eval('(' + data + ')');
1178
}
1179
1180
1181
/*
1182
1183
=head1 NOTES
1184
1185
These are some notes on the internals
1186
1187
=over
1188
1189
=item LOCALE CACHING
1190
1191
Loaded locale data is currently cached class-wide. This means that if two scripts are both using Gettext.js, and both share the same gettext domain, that domain will only be loaded once. This will allow you to grab a new object many times from different places, utilize the same domain, and share a single translation file. The downside is that a domain won't be RE-loaded if a new object is instantiated on a domain that had already been instantiated.
1192
1193
=back
1194
1195
=head1 BUGS / TODO
1196
1197
=over
1198
1199
=item error handling
1200
1201
Currently, there are several places that throw errors. In GNU Gettext, there are no fatal errors, which allows text to still be displayed regardless of how broken the environment becomes. We should evaluate and determine where we want to stand on that issue.
1202
1203
=item syncronous only support (no ajax support)
1204
1205
Currently, fetching language data is done purely syncronous, which means the page will halt while those files are fetched/loaded.
1206
1207
This is often what you want, as then following translation requests will actually be translated. However, if all your calls are done dynamically (ie. error handling only or something), loading in the background may be more adventagous.
1208
1209
It's still recommended to use the statically defined <script ...> method, which should have the same delay, but it will cache the result.
1210
1211
=item domain support
1212
1213
domain support while using shortcut methods like C<_('string')> or C<i18n('string')>.
1214
1215
Under normal apps, the domain is usually set globally to the app, and a single language file is used. Under javascript, you may have multiple libraries or applications needing translation support, but the namespace is essentially global.
1216
1217
It's recommended that your app initialize it's own shortcut with it's own domain.  (See examples/wrapper/i18n.js for an example.)
1218
1219
Basically, you'll want to accomplish something like this:
1220
1221
    // in some other .js file that needs i18n
1222
    this.i18nObj = new i18n;
1223
    this.i18n = this.i18nObj.init('domain');
1224
    // do translation
1225
    alert( this.i18n("string") );
1226
1227
If you use this raw Gettext object, then this is all handled for you, as you have your own object then, and will be calling C<myGettextObject.gettext('string')> and such.
1228
1229
1230
=item encoding
1231
1232
May want to add encoding/reencoding stuff. See GNU iconv, or the perl module Locale::Recode from libintl-perl.
1233
1234
=back
1235
1236
1237
=head1 COMPATABILITY
1238
1239
This has been tested on the following browsers. It may work on others, but these are all those to which I have access.
1240
1241
    FF1.5, FF2, FF3, IE6, IE7, Opera9, Opera10, Safari3.1, Chrome
1242
1243
    *FF = Firefox
1244
    *IE = Internet Explorer
1245
1246
1247
=head1 REQUIRES
1248
1249
bin/po2json requires perl, and the perl modules Locale::PO and JSON.
1250
1251
=head1 SEE ALSO
1252
1253
bin/po2json (included),
1254
examples/normal/index.html,
1255
examples/wrapper/i18n.html, examples/wrapper/i18n.js,
1256
Locale::gettext_pp(3pm), POSIX(3pm), gettext(1), gettext(3)
1257
1258
=head1 AUTHOR
1259
1260
Copyright (C) 2008, Joshua I. Miller E<lt>unrtst@cpan.orgE<gt>, all rights reserved. See the source code for details.
1261
1262
=cut
1263
1264
*/
(-)a/koha-tmpl/opac-tmpl/bootstrap/js/i18n.js (+51 lines)
Line 0 Link Here
1
(function() {
2
  var params = {
3
    "domain": "Koha"
4
  };
5
  if (typeof json_locale_data !== 'undefined') {
6
    params.locale_data = json_locale_data;
7
  }
8
9
  Koha.i18n = {
10
    gt: new Gettext(params),
11
12
    expand: function(text, vars) {
13
      var replace_callback = function(match, name) {
14
        return name in vars ? vars[name] : match;
15
      };
16
      return text.replace(/\{(.*?)\}/g, replace_callback);
17
    }
18
  };
19
})();
20
21
function __(msgid) {
22
  return Koha.i18n.gt.gettext(msgid);
23
}
24
25
function __x(msgid, vars) {
26
  return Koha.i18n.expand(__(msgid), vars);
27
}
28
29
function __n(msgid, msgid_plural, count) {
30
  return Koha.i18n.gt.ngettext(msgid, msgid_plural, count);
31
}
32
33
function __nx(msgid, msgid_plural, count, vars) {
34
  return Koha.i18n.expand(__n(msgid, msgid_plural, count), vars);
35
}
36
37
function __p(msgctxt, msgid) {
38
  return Koha.i18n.gt.pgettext(msgctxt, msgid);
39
}
40
41
function __px(msgctxt, msgid, vars) {
42
  return Koha.i18n.expand(__p(msgctxt, msgid), vars);
43
}
44
45
function __np(msgctxt, msgid, msgid_plural, count) {
46
  return Koha.i18n.gt.npgettext(msgctxt, msgid, msgid_plural, count);
47
}
48
49
function __npx(msgctxt, msgid, msgid_plural, count, vars) {
50
  return Koha.i18n.expand(__np(msgctxt, msgid, msgid_plural, count), vars);
51
}
(-)a/misc/translator/LangInstaller.pm (-5 / +48 lines)
Lines 77-87 sub new { Link Here
77
    $self->{cp}              = `which cp`;
77
    $self->{cp}              = `which cp`;
78
    $self->{msgmerge}        = `which msgmerge`;
78
    $self->{msgmerge}        = `which msgmerge`;
79
    $self->{msgfmt}          = `which msgfmt`;
79
    $self->{msgfmt}          = `which msgfmt`;
80
    $self->{msgcat}          = `which msgcat`;
80
    $self->{xgettext}        = `which xgettext`;
81
    $self->{xgettext}        = `which xgettext`;
81
    $self->{sed}             = `which sed`;
82
    $self->{sed}             = `which sed`;
83
    $self->{po2json}         = "$Bin/po2json";
82
    chomp $self->{cp};
84
    chomp $self->{cp};
83
    chomp $self->{msgmerge};
85
    chomp $self->{msgmerge};
84
    chomp $self->{msgfmt};
86
    chomp $self->{msgfmt};
87
    chomp $self->{msgcat};
85
    chomp $self->{xgettext};
88
    chomp $self->{xgettext};
86
    chomp $self->{sed};
89
    chomp $self->{sed};
87
90
Lines 605-621 sub extract_messages { Link Here
605
    $self->extract_messages_from_templates($tempdir, @tt_files);
608
    $self->extract_messages_from_templates($tempdir, @tt_files);
606
    push @files_to_scan, @tt_files;
609
    push @files_to_scan, @tt_files;
607
610
608
    my $xgettext_cmd = "$self->{xgettext} -L Perl --from-code=UTF-8 "
611
    my $xgettext_common_args = "--force-po --from-code=UTF-8 "
609
        . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 -k__p:1c,2 "
612
        . "-k -k__ -k__x -k__n:1,2 -k__nx:1,2 -k__xn:1,2 "
610
        . "-k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 -kN__ -kN__n:1,2 "
613
        . "-k__p:1c,2 -k__px:1c,2 -k__np:1c,2,3 -k__npx:1c,2,3 "
611
        . "-kN__p:1c,2 -kN__np:1c,2,3 "
614
        . "-kN__ -kN__n:1,2 -kN__p:1c,2 -kN__np:1c,2,3";
612
        . "-o $Bin/$self->{domain}.pot -D $tempdir -D $intranetdir";
615
    my $xgettext_cmd = "$self->{xgettext} -L Perl $xgettext_common_args "
616
        . "-o $Bin/$self->{domain}-perl.pot -D $tempdir -D $intranetdir";
613
    $xgettext_cmd .= " $_" foreach (@files_to_scan);
617
    $xgettext_cmd .= " $_" foreach (@files_to_scan);
614
618
615
    if (system($xgettext_cmd) != 0) {
619
    if (system($xgettext_cmd) != 0) {
616
        die "system call failed: $xgettext_cmd";
620
        die "system call failed: $xgettext_cmd";
617
    }
621
    }
618
622
623
    my @js_files;
624
    find(sub {
625
        if ($File::Find::dir =~ m|/en/| && $_ =~ m/\.js$/) {
626
            my $filename = $File::Find::name;
627
            $filename =~ s|^$intranetdir/||;
628
            push @js_files, $filename;
629
        }
630
    }, "$intranetdir/koha-tmpl");
631
632
    $xgettext_cmd = "$self->{xgettext} -L JavaScript $xgettext_common_args "
633
        . "-o $Bin/$self->{domain}-js.pot -D $intranetdir";
634
    $xgettext_cmd .= " $_" foreach (@js_files);
635
636
    if (system($xgettext_cmd) != 0) {
637
        die "system call failed: $xgettext_cmd";
638
    }
639
640
    my $msgcat_cmd = "$self->{msgcat} -o $Bin/$self->{domain}.pot $Bin/$self->{domain}-perl.pot $Bin/$self->{domain}-js.pot";
641
    if (system($msgcat_cmd) != 0) {
642
        die "system call failed: $msgcat_cmd";
643
    }
644
619
    if ( -f "$Bin/$self->{domain}.pot" ) {
645
    if ( -f "$Bin/$self->{domain}.pot" ) {
620
        my $replace_charset_cmd = "$self->{sed} --in-place " .
646
        my $replace_charset_cmd = "$self->{sed} --in-place " .
621
            "$Bin/$self->{domain}.pot " .
647
            "$Bin/$self->{domain}.pot " .
Lines 648-659 sub install_messages { Link Here
648
        $self->create_messages();
674
        $self->create_messages();
649
    }
675
    }
650
    system "$self->{msgfmt} -o $mofile $pofile";
676
    system "$self->{msgfmt} -o $mofile $pofile";
677
678
    my $js_locale_data = 'var json_locale_data = {"Koha":' . `$self->{po2json} $pofile` . '};';
679
    my $progdir = $self->{context}->config('intrahtdocs') . '/prog';
680
    open my $fh, '>', "$progdir/$self->{lang}/js/locale_data.js";
681
    print $fh $js_locale_data;
682
    close $fh;
683
684
    my $opachtdocs = $self->{context}->config('opachtdocs');
685
    opendir(my $dh, $opachtdocs);
686
    for my $theme ( grep { not /^\.|lib/ } readdir($dh) ) {
687
        mkdir "$opachtdocs/$theme/$self->{lang}/js";
688
        open my $fh, '>', "$opachtdocs/$theme/$self->{lang}/js/locale_data.js";
689
        print $fh $js_locale_data;
690
        close $fh;
691
    }
651
}
692
}
652
693
653
sub remove_pot {
694
sub remove_pot {
654
    my $self = shift;
695
    my $self = shift;
655
696
656
    unlink "$Bin/$self->{domain}.pot";
697
    unlink "$Bin/$self->{domain}.pot";
698
    unlink "$Bin/$self->{domain}-perl.pot";
699
    unlink "$Bin/$self->{domain}-js.pot";
657
}
700
}
658
701
659
sub install {
702
sub install {
(-)a/misc/translator/po2json (-1 / +249 lines)
Line 0 Link Here
0
- 
1
#!/usr/bin/env perl
2
# PODNAME: po2json
3
# ABSTRACT: Command line tool for converting a po file into a Gettext.js compatible json dataset
4
5
# Copyright (C) 2008, Joshua I. Miller E<lt>unrtst@cpan.orgE<gt>, all
6
# rights reserved.
7
#
8
# This program is free software; you can redistribute it and/or modify it
9
# under the terms of the GNU Library General Public License as published
10
# by the Free Software Foundation; either version 2, or (at your option)
11
# any later version.
12
#
13
# This program is distributed in the hope that it will be useful,
14
# but WITHOUT ANY WARRANTY; without even the implied warranty of
15
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
16
# Library General Public License for more details.
17
#
18
# You should have received a copy of the GNU Library General Public
19
# License along with this program; if not, write to the Free Software
20
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
21
# USA.
22
23
24
use strict;
25
use JSON 2.53;
26
use Locale::PO 0.21;
27
use File::Basename qw(basename);
28
29
my $gettext_context_glue = "\004";
30
31
sub usage {
32
    return "$0 {-p} {file.po} > {outputfile.json}
33
    -p  : do pretty-printing of json data\n";
34
}
35
36
&main;
37
38
sub main
39
{
40
    my ($src_fh, $src);
41
42
    my $pretty = 0;
43
    if ($ARGV[0] =~ /^--?p$/) {
44
        shift @ARGV;
45
        $pretty = 1;
46
    }
47
48
    if (length($ARGV[0]))
49
    {
50
        if ($ARGV[0] =~ /^-h/) {
51
            print &usage;
52
            exit 1;
53
        }
54
55
        unless (-r $ARGV[0]) {
56
            print "ERROR: Unable to read file [$ARGV[0]]\n";
57
            die &usage;
58
        }
59
60
        $src = $ARGV[0];
61
    } else {
62
        die &usage;
63
    }
64
65
    # we'll be building this data struct
66
    my $json = {};
67
68
    my $plural_form_count;
69
    # get po object stack
70
    my $pos = Locale::PO->load_file_asarray($src) or die "Can't parse po file [$src].";
71
72
73
    foreach my $po (@$pos)
74
    {
75
        my $qmsgid1 = $po->msgid;
76
        my $msgid1 = $po->dequote( $qmsgid1 );
77
78
        # on the header
79
        if (length($msgid1) == 0)
80
        {
81
            my $qmsgstr = $po->msgstr;
82
            my $cur = $po->dequote( $qmsgstr );
83
            my %cur;
84
            foreach my $h (split(/\\n/, $cur))
85
            {
86
                next unless length($h);
87
                my @h = split(':', $h, 2);
88
89
                if (length($cur{$h[0]})) {
90
                    warn "SKIPPING DUPLICATE HEADER LINE: $h\n";
91
                } elsif ($h[0] =~ /#-#-#-#-#/) {
92
                    warn "SKIPPING ERROR MARKER IN HEADER: $h\n";
93
                } elsif (@h == 2) {
94
                    $cur{$h[0]} = $h[1];
95
                } else {
96
                    warn "PROBLEM LINE IN HEADER: $h\n";
97
                    $cur{$h} = '';
98
                }
99
            }
100
101
            # init header ref
102
            $$json{''} ||= {};
103
104
            # populate header ref
105
            foreach my $key (keys %cur) {
106
                $$json{''}{$key} = length($cur{$key}) ? $cur{$key} : '';
107
            }
108
109
            # save plural form count
110
            if ($$json{''}{'Plural-Forms'}) {
111
                my $t = $$json{''}{'Plural-Forms'};
112
                $t =~ s/^\s*//;
113
                if ($t =~ /nplurals=(\d+)/) {
114
                    $plural_form_count = $1;
115
                } else {
116
                    die "ERROR parsing plural forms header [$t]\n";
117
                }
118
            } else {
119
                warn "NO PLURAL FORM HEADER FOUND - DEFAULTING TO 2\n";
120
                # just default to 2
121
                $plural_form_count = 2;
122
            }
123
124
        # on a normal msgid
125
        } else {
126
            my $qmsgctxt = $po->msgctxt;
127
            my $msgctxt = $po->dequote($qmsgctxt) if $qmsgctxt;
128
129
            # build the new msgid key
130
            my $msg_ctxt_id = defined($msgctxt) ? join($gettext_context_glue, ($msgctxt, $msgid1)) : $msgid1;
131
132
            # build translation side
133
            my @trans;
134
135
            # msgid plural side
136
            my $qmsgid_plural = $po->msgid_plural;
137
            my $msgid2 = $po->dequote( $qmsgid_plural ) if $qmsgid_plural;
138
            push(@trans, $msgid2);
139
140
            # translated string
141
            # this shows up different if we're plural
142
            if (defined($msgid2) && length($msgid2))
143
            {
144
                my $plurals = $po->msgstr_n;
145
                for (my $i=0; $i<$plural_form_count; $i++)
146
                {
147
                    my $qstr = ref($plurals) ? $$plurals{$i} : undef;
148
                    my $str  = $po->dequote( $qstr ) if $qstr;
149
                    push(@trans, $str);
150
                }
151
152
            # singular
153
            } else {
154
                my $qmsgstr = $po->msgstr;
155
                my $msgstr = $po->dequote( $qmsgstr ) if $qmsgstr;
156
                push(@trans, $msgstr);
157
            }
158
159
            $$json{$msg_ctxt_id} = \@trans;
160
        }
161
    }
162
163
164
    my $jsonobj = new JSON;
165
    my $basename = basename($src);
166
    $basename =~ s/\.pot?$//;
167
    if ($pretty)
168
    {
169
        print $jsonobj->pretty->encode( { $basename => $json });
170
    } else {
171
        print $jsonobj->encode($json);
172
    }
173
}
174
175
__END__
176
177
=pod
178
179
=head1 NAME
180
181
po2json - Command line tool for converting a po file into a Gettext.js compatible json dataset
182
183
=head1 VERSION
184
185
version 0.019
186
187
=head1 SYNOPSIS
188
189
 po2json /path/to/domain.po > domain.json
190
191
=head1 DESCRIPTION
192
193
This takes a PO file, as is created from GNU Gettext's xgettext, and converts it into a JSON file.
194
195
The output is an annonymous associative array. So, if you plan to load this via a <script> tag, more processing will be require (the output from this program must be assigned to a named javascript variable). For example:
196
197
    echo -n "var json_locale_data = " > domain.json
198
    po2json /path/to/domain.po >> domain.json
199
    echo ";" >> domain.json
200
201
=head1 NAME
202
203
po2json - Convert a Uniforum format portable object file to javascript object notation.
204
205
=head1 OPTIONS
206
207
 -p : pretty-print the output. Makes the output more human-readable.
208
209
=head1 BUGS
210
211
Locale::PO has a potential bug (I don't know if this actually causes a problem or not). Given a .po file with an entry like:
212
213
    msgid ""
214
    "some string"
215
    msgstr ""
216
217
When $po->dump is run on that entry, it will output:
218
219
    msgid "some string"
220
    msgstr ""
221
222
The above is removing the first linebreak. I don't know if that is significant. If so, we'll have to rewrite using a different parser (or include our own parser).
223
224
=head1 REQUIRES
225
226
 Locale::PO
227
 JSON
228
229
=head1 SEE ALSO
230
231
 Locale::PO
232
 Gettext.js
233
234
=head1 AUTHOR
235
236
Copyright (C) 2008, Joshua I. Miller E<lt>unrtst@cpan.orgE<gt>, all rights reserved. See the source code for details.
237
238
=head1 AUTHOR
239
240
Torsten Raudssus <torsten@raudss.us>
241
242
=head1 COPYRIGHT AND LICENSE
243
244
This software is copyright (c) 2012 by DuckDuckGo, Inc. L<http://duckduckgo.com/>, Torsten Raudssus <torsten@raudss.us>.
245
246
This is free software; you can redistribute it and/or modify it under
247
the same terms as the Perl 5 programming language system itself.
248
249
=cut

Return to bug 15395