PageSourceSearch

https://supplies.voluforms.com/static/outpost/js-2/i18n/gettext/lib/Gettext.js

js voluforms.com collected 2026-09-28 09:34:59 UTC 44,351 bytes, 1,264 lines download raw bytes

1/*
2Pure Javascript implementation of Uniforum message translation.
3Copyright (C) 2008 Joshua I. Miller <[email protected]>, all rights reserved
4
5This program is free software; you can redistribute it and/or modify it
6under the terms of the GNU Library General Public License as published
7by the Free Software Foundation; either version 2, or (at your option)
8any later version.
9
10This program is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13Library General Public License for more details.
14
15You should have received a copy of the GNU Library General Public
16License along with this program; if not, write to the Free Software
17Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
18USA.
19
20=head1 NAME
21
22Javascript 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
104This 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
106The 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
111To 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
116Configure 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
138This 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
142Use XMLHttpRequest (actually, SJAX - syncronous) to load an external resource.
143
144Supported 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
162NOTE: .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
174The 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
195Several 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
204The 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
209Finally, 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
216args:
217
218=over
219
220=item domain
221
222The 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
226Raw locale data (in json structure). If specified, from_link data will be ignored.
227
228=back
229
230=cut
231
232*/
233
234Gettext = 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
260Gettext.context_glue = "\004";
261Gettext._locale_data = {};
262
263Gettext.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+"] foun
298d, 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//          },
312Gettext.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
397Gettext.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
418Gettext.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
431Gettext.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
589Gettext.prototype.parse_po_dequote = function(str) {
590    var match;
591    if (match = str.match(/^"(.*)"/)) {
592        str = match[1];
593    }
594    str = str.replace(/\\"/, "");
595    return str;
596};
597
598
599// try_load_lang_json : do an ajaxy call to load in the lang defs
600Gettext.prototype.try_load_lang_json = function(uri) {
601    var data = this.sjax(uri);
602    if (! data) return;
603
604    var rv = this.JSON(data);
605    this.parse_locale_data(rv);
606
607    return 1;
608};
609
610// this finds all <link> tags, filters out ones that match our
611// specs, and returns a list of hashes of those
612Gettext.prototype.get_lang_refs = function() {
613    var langs = new Array();
614    var links = document.getElementsByTagName("link");
615    // find all <link> tags in dom; filter ours
616    for (var i=0; i<links.length; i++) {
617        if (links[i].rel == 'gettext' && links[i].href) {
618            if (typeof(links[i].type) == 'undefined' ||
619                links[i].type == '') {
620                if (/\.json$/i.test(links[i].href)) {
621                    links[i].type = 'application/json';
622                } else if (/\.js$/i.test(links[i].href)) {
623                    links[i].type = 'application/json';
624                } else if (/\.po$/i.test(links[i].href)) {
625                    links[i].type = 'application/x-po';
626                } else if (/\.mo$/i.test(links[i].href)) {
627                    links[i].type = 'application/x-mo';
628                } else {
629                    throw new Error("LINK tag with rel=gettext found, but the type and extension are unrecognized.");
630                }
631            }
632
633            links[i].type = links[i].type.toLowerCase();
634            if (links[i].type == 'application/json') {
635                links[i].type = 'application/json';
636            } else if (links[i].type == 'text/javascript') {
637                links[i].type = 'application/json';
638            } else if (links[i].type == 'application/x-po') {
639                links[i].type = 'application/x-po';
640            } else if (links[i].type == 'application/x-mo') {
641                links[i].type = 'application/x-mo';
642            } else {
643                throw new Error("LINK tag with rel=gettext found, but the type attribute ["+links[i].type+"] is unrecognized.");
644            }
645
646            langs.push(links[i]);
647        }
648    }
649    return langs;
650};
651
652
653/*
654
655=head2 textdomain( domain )
656
657Set domain for future gettext() calls
658
659A  message  domain  is  a  set of translatable msgid messages. Usually,
660every software package has its own message domain. The domain  name  is
661used to determine the message catalog where a translation is looked up;
662it must be a non-empty string.
663
664The current message domain is used by the gettext, ngettext, pgettext,
665npgettext functions, and by the dgettext, dcgettext, dngettext, dcngettext,
666dpgettext, dcpgettext, dnpgettext and dcnpgettext functions when called
667with a NULL domainname argument.
668
669If domainname is not NULL, the current message domain is set to
670domainname.
671
672If domainname is undefined, null, or empty string, the function returns
673the current message domain.
674
675If  successful,  the  textdomain  function  returns the current message
676domain, after possibly changing it. (ie. if you set a new domain, the 
677value returned will NOT be the previous domain).
678
679=cut
680
681*/
682Gettext.prototype.textdomain = function (domain) {
683    if (domain && domain.length) this.domain = domain;
684    return this.domain;
685}
686
687/*
688
689=head2 gettext( MSGID )
690
691Returns the translation for B<MSGID>.  Example:
692
693    alert( gt.gettext("Hello World!\n") );
694
695If no translation can be found, the unmodified B<MSGID> is returned,
696i. e. the function can I<never> fail, and will I<never> mess up your
697original message.
698
699One common mistake is to interpolate a variable into the string like this:
700
701  var translated = gt.gettext("Hello " + full_name);
702
703The interpolation will happen before it's passed to gettext, and it's 
704unlikely you'll have a translation for every "Hello Tom" and "Hello Dick"
705and "Hellow Harry" that may arise.
706
707Use C<strargs()> (see below) to solve this problem:
708
709  var translated = Gettext.strargs( gt.gettext("Hello %1"), [full_name] );
710
711This is espeically useful when multiple replacements are needed, as they 
712may not appear in the same order within the translation. As an English to
713French example:
714
715  Expected result: "This is the red ball"
716  English: "This is the %1 %2"
717  French:  "C'est le %2 %1"
718  Code: Gettext.strargs( gt.gettext("This is the %1 %2"), ["red", "ball"] );
719
720(The example is stupid because neither color nor thing will get
721translated here ...).
722
723=head2 dgettext( TEXTDOMAIN, MSGID )
724
725Like gettext(), but retrieves the message for the specified 
726B<TEXTDOMAIN> instead of the default domain.  In case you wonder what
727a textdomain is, see above section on the textdomain() call.
728
729=head2 dcgettext( TEXTDOMAIN, MSGID, CATEGORY )
730
731Like dgettext() but retrieves the message from the specified B<CATEGORY>
732instead of the default category C<LC_MESSAGES>.
733
734NOTE: the categories are really useless in javascript context. This is
735here for GNU Gettext API compatability. In practice, you'll never need
736to use this. This applies to all the calls including the B<CATEGORY>.
737
738
739=head2 ngettext( MSGID, MSGID_PLURAL, COUNT )
740
741Retrieves the correct translation for B<COUNT> items.  In legacy software
742you will often find something like:
743
744    alert( count + " file(s) deleted.\n" );
745
746or
747
748    printf(count + " file%s deleted.\n", $count == 1 ? '' : 's');
749
750I<NOTE: javascript lacks a builtin printf, so the above isn't a working example>
751
752The first example looks awkward, the second will only work in English
753and languages with similar plural rules.  Before ngettext() was introduced,
754the best practice for internationalized programs was:
755
756    if (count == 1) {
757        alert( gettext("One file deleted.\n") );
758    } else {
759        printf( gettext("%d files deleted.\n"), count );
760    }
761
762This is a nuisance for the programmer and often still not sufficient
763for an adequate translation.  Many languages have completely different
764ideas on numerals.  Some (French, Italian, ...) treat 0 and 1 alike,
765others make no distinction at all (Japanese, Korean, Chinese, ...),
766others have two or more plural forms (Russian, Latvian, Czech,
767Polish, ...).  The solution is:
768
769    printf( ngettext("One file deleted.\n",
770                     "%d files deleted.\n",
771                     count), // argument to ngettext!
772            count);          // argument to printf!
773
774In English, or if no translation can be found, the first argument
775(B<MSGID>) is picked if C<count> is one, the second one otherwise.
776For other languages, the correct plural form (of 1, 2, 3, 4, ...)
777is automatically picked, too.  You don't have to know anything about
778the plural rules in the target language, ngettext() will take care
779of that.
780
781This is most of the time sufficient but you will have to prove your
782creativity in cases like
783
784    "%d file(s) deleted, and %d file(s) created.\n"
785
786That said, javascript lacks C<printf()> support. Supplied with Gettext.js
787is the C<strargs()> method, which can be used for these cases:
788
789    Gettext.strargs( gt.ngettext( "One file deleted.\n",
790                                  "%d files deleted.\n",
791                                  count), // argument to ngettext!
792                     count); // argument to strargs!
793
794NOTE: the variable replacement isn't done for you, so you must
795do it yourself as in the above.
796
797=head2 dngettext( TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT )
798
799Like ngettext() but retrieves the translation from the specified
800textdomain instead of the default domain.
801
802=head2 dcngettext( TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT, CATEGORY )
803
804Like dngettext() but retrieves the translation from the specified
805category, instead of the default category C<LC_MESSAGES>.
806
807
808=head2 pgettext( MSGCTXT, MSGID )
809
810Returns the translation of MSGID, given the context of MSGCTXT.
811
812Both items are used as a unique key into the message catalog.
813
814This allows the translator to have two entries for words that may
815translate to different foreign words based on their context. For
816example, the word "View" may be a noun or a verb, which may be
817used in a menu as File->View or View->Source.
818
819    alert( pgettext( "Verb: To View", "View" ) );
820    alert( pgettext( "Noun: A View", "View"  ) );
821
822The above will both lookup different entries in the message catalog.
823
824In English, or if no translation can be found, the second argument
825(B<MSGID>) is returned.
826
827=head2 dpgettext( TEXTDOMAIN, MSGCTXT, MSGID )
828
829Like pgettext(), but retrieves the message for the specified 
830B<TEXTDOMAIN> instead of the default domain.
831
832=head2 dcpgettext( TEXTDOMAIN, MSGCTXT, MSGID, CATEGORY )
833
834Like dpgettext() but retrieves the message from the specified B<CATEGORY>
835instead of the default category C<LC_MESSAGES>.
836
837
838=head2 npgettext( MSGCTXT, MSGID, MSGID_PLURAL, COUNT )
839
840Like ngettext() with the addition of context as in pgettext().
841
842In English, or if no translation can be found, the second argument
843(MSGID) is picked if B<COUNT> is one, the third one otherwise.
844
845=head2 dnpgettext( TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT )
846
847Like npgettext() but retrieves the translation from the specified
848textdomain instead of the default domain.
849
850=head2 dcnpgettext( TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT, CATEGORY )
851
852Like dnpgettext() but retrieves the translation from the specified
853category, instead of the default category C<LC_MESSAGES>.
854
855=cut
856
857*/
858
859// gettext
860Gettext.prototype.gettext = function (msgid) {
861    var msgctxt;
862    var msgid_plural;
863    var n;
864    var category;
865    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
866};
867
868Gettext.prototype.dgettext = function (domain, msgid) {
869    var msgctxt;
870    var msgid_plural;
871    var n;
872    var category;
873    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
874};
875
876Gettext.prototype.dcgettext = function (domain, msgid, category) {
877    var msgctxt;
878    var msgid_plural;
879    var n;
880    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
881};
882
883// ngettext
884Gettext.prototype.ngettext = function (msgid, msgid_plural, n) {
885    var msgctxt;
886    var category;
887    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
888};
889
890Gettext.prototype.dngettext = function (domain, msgid, msgid_plural, n) {
891    var msgctxt;
892    var category;
893    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
894};
895
896Gettext.prototype.dcngettext = function (domain, msgid, msgid_plural, n, category) {
897    var msgctxt;
898    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category, c
898ategory);
899};
900
901// pgettext
902Gettext.prototype.pgettext = function (msgctxt, msgid) {
903    var msgid_plural;
904    var n;
905    var category;
906    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
907};
908
909Gettext.prototype.dpgettext = function (domain, msgctxt, msgid) {
910    var msgid_plural;
911    var n;
912    var category;
913    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
914};
915
916Gettext.prototype.dcpgettext = function (domain, msgctxt, msgid, category) {
917    var msgid_plural;
918    var n;
919    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
920};
921
922// npgettext
923Gettext.prototype.npgettext = function (msgctxt, msgid, msgid_plural, n) {
924    var category;
925    return this.dcnpgettext(null, msgctxt, msgid, msgid_plural, n, category);
926};
927
928Gettext.prototype.dnpgettext = function (domain, msgctxt, msgid, msgid_plural, n) {
929    var category;
930    return this.dcnpgettext(domain, msgctxt, msgid, msgid_plural, n, category);
931};
932
933// this has all the options, so we use it for all of them.
934Gettext.prototype.dcnpgettext = function (domain, msgctxt, msgid, msgid_plural, n, category) {
935    if (! this.isValidObject(msgid)) return '';
936
937    var plural = this.isValidObject(msgid_plural);
938    var msg_ctxt_id = this.isValidObject(msgctxt) ? msgctxt+Gettext.context_glue+msgid : msgid;
939
940    var domainname = this.isValidObject(domain)      ? domain :
941                     this.isValidObject(this.domain) ? this.domain :
942                                                       'messages';
943
944    // category is always LC_MESSAGES. We ignore all else
945    var category_name = 'LC_MESSAGES';
946    var category = 5;
947
948    var locale_data = new Array();
949    if (typeof(Gettext._locale_data) != 'undefined' &&
950        this.isValidObject(Gettext._locale_data[domainname])) {
951        locale_data.push( Gettext._locale_data[domainname] );
952
953    } else if (typeof(Gettext._locale_data) != 'undefined') {
954        // didn't find domain we're looking for. Search all of them.
955        for (var dom in Gettext._locale_data) {
956            locale_data.push( Gettext._locale_data[dom] );
957        }
958    }
959
960    var trans = [];
961    var found = false;
962    var domain_used; // so we can find plural-forms if needed
963    if (locale_data.length) {
964        for (var i=0; i<locale_data.length; i++) {
965            var locale = locale_data[i];
966            if (this.isValidObject(locale.msgs[msg_ctxt_id])) {
967                // make copy of that array (cause we'll be destructive)
968                for (var j=0; j<locale.msgs[msg_ctxt_id].length; j++) {
969                    trans[j] = locale.msgs[msg_ctxt_id][j];
970                }
971                trans.shift(); // throw away the msgid_plural
972                domain_used = locale;
973                found = true;
974                // only break if found translation actually has a translation.
975                if ( trans.length > 0 && trans[0].length != 0 )
976                    break;
977            }
978        }
979    }
980
981    // default to english if we lack a match, or match has zero length
982    if ( trans.length == 0 || trans[0].length == 0 ) {
983        trans = [ msgid, msgid_plural ];
984    }
985
986    var translation = trans[0];
987    if (plural) {
988        var p;
989        if (found && this.isValidObject(domain_used.head.plural_func) ) {
990            var rv = domain_used.head.plural_func(n);
991            if (! rv.plural) rv.plural = 0;
992            if (! rv.nplural) rv.nplural = 0;
993            // if plurals returned is out of bound for total plural forms
994            if (rv.nplural <= rv.plural) rv.plural = 0;
995            p = rv.plural;
996        } else {
997            p = (n != 1) ? 1 : 0;
998        }
999        if (this.isValidObject(trans[p]))
1000            translation = trans[p];
1001    }
1002
1003    return translation;
1004};
1005
1006
1007/*
1008
1009=head2 strargs (string, argument_array)
1010
1011  string : a string that potentially contains formatting characters.
1012  argument_array : an array of positional replacement values
1013
1014This is a utility method to provide some way to support positional parameters within a string, as javascript lacks a printf() method.
1015
1016The format is similar to printf(), but greatly simplified (ie. fewer features).
1017
1018Any percent signs followed by numbers are replaced with the corrosponding item from the B<argument_array>.
1019
1020Example:
1021
1022    var string = "%2 roses are red, %1 violets are blue";
1023    var args   = new Array("10", "15");
1024    var result = Gettext.strargs(string, args);
1025    // result is "15 roses are red, 10 violets are blue"
1026
1027The format numbers are 1 based, so the first itme is %1.
1028
1029A lone percent sign may be escaped by preceeding it with another percent sign.
1030
1031A percent sign followed by anything other than a number or another percent sign will be passed through as is.
1032
1033Some more examples should clear up any abmiguity. The following were called with the orig string, and the array as Array("[one]", "[two]") :
1034
1035  orig string "blah" becomes "blah"
1036  orig string "" becomes ""
1037  orig string "%%" becomes "%"
1038  orig string "%%%" becomes "%%"
1039  orig string "%%%%" becomes "%%"
1040  orig string "%%%%%" becomes "%%%"
1041  orig string "tom%%dick" becomes "tom%dick"
1042  orig string "thing%1bob" becomes "thing[one]bob"
1043  orig string "thing%1%2bob" becomes "thing[one][two]bob"
1044  orig string "thing%1asdf%2asdf" becomes "thing[one]asdf[two]asdf"
1045  orig string "%1%2%3" becomes "[one][two]"
1046  orig string "tom%1%%2%aDick" becomes "tom[one]%2%aDick"
1047
1048This is especially useful when using plurals, as the string will nearly always contain the number.
1049
1050It's also useful in translated strings where the translator may have needed to move the position of the parameters.
1051
1052For example:
1053
1054  var count = 14;
1055  Gettext.strargs( gt.ngettext('one banana', '%1 bananas', count), [count] );
1056
1057NOTE: this may be called as an instance method, or as a class method.
1058
1059  // instance method:
1060  var gt = new Gettext(params);
1061  gt.strargs(string, args);
1062
1063  // class method:
1064  Gettext.strargs(string, args);
1065
1066=cut
1067
1068*/
1069/* utility method, since javascript lacks a printf */
1070Gettext.strargs = function (str, args) {
1071    // make sure args is an array
1072    if ( null == args ||
1073         'undefined' == typeof(args) ) {
1074        args = [];
1075    } else if (args.constructor != Array) {
1076        args = [args];
1077    }
1078
1079    // NOTE: javascript lacks support for zero length negative look-behind
1080    // in regex, so we must step through w/ index.
1081    // The perl equiv would simply be:
1082    //    $string =~ s/(?<!\%)\%([0-9]+)/$args[$1]/g;
1083    //    $string =~ s/\%\%/\%/g; # restore escaped percent signs
1084
1085    var newstr = "";
1086    while (true) {
1087        var i = str.indexOf('%');
1088        var match_n;
1089
1090        // no more found. Append whatever remains
1091        if (i == -1) {
1092            newstr += str;
1093            break;
1094        }
1095
1096        // we found it, append everything up to that
1097        newstr += str.substr(0, i);
1098
1099        // check for escpaed %%
1100        if (str.substr(i, 2) == '%%') {
1101            newstr += '%';
1102            str = str.substr((i+2));
1103
1104        // % followed by number
1105        } else if ( match_n = str.substr(i).match(/^%(\d+)/) ) {
1106            var arg_n = parseInt(match_n[1]);
1107            var length_n = match_n[1].length;
1108            if ( arg_n > 0 && args[arg_n -1] != null && typeof(args[arg_n -1]) != 'undefined' )
1109                newstr += args[arg_n -1];
1110            str = str.substr( (i + 1 + length_n) );
1111
1112        // % followed by some other garbage - just remove the %
1113        } else {
1114            newstr += '%';
1115            str = str.substr((i+1));
1116        }
1117    }
1118
1119    return newstr;
1120}
1121
1122/* instance method wrapper of strargs */
1123Gettext.prototype.strargs = function (str, args) {
1124    return Gettext.strargs(str, args);
1125}
1126
1127/* verify that something is an array */
1128Gettext.prototype.isArray = function (thisObject) {
1129    return this.isValidObject(thisObject) && thisObject.constructor == Array;
1130};
1131
1132/* verify that an object exists and is valid */
1133Gettext.prototype.isValidObject = function (thisObject) {
1134    if (null == thisObject) {
1135        return false;
1136    } else if ('undefined' == typeof(thisObject) ) {
1137        return false;
1138    } else {
1139        return true;
1140    }
1141};
1142
1143Gettext.prototype.sjax = function (uri) {
1144    var xmlhttp;
1145    if (window.XMLHttpRequest) {
1146        xmlhttp = new XMLHttpRequest();
1147    } else if (navigator.userAgent.toLowerCase().indexOf('msie 5') != -1) {
1148        xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
1149    } else {
1150        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
1151    }
1152
1153    if (! xmlhttp)
1154        throw new Error("Your browser doesn't do Ajax. Unable to supp
1154ort external language files.");
1155
1156    xmlhttp.open('GET', uri, false);
1157    try { xmlhttp.send(null); }
1158    catch (e) { return; }
1159
1160    // we consider status 200 and 0 as ok.
1161    // 0 happens when we request local file, allowing this to run on local files
1162    var sjax_status = xmlhttp.status;
1163    if (sjax_status == 200 || sjax_status == 0) {
1164        return xmlhttp.responseText;
1165    } else {
1166        var error = xmlhttp.statusText + " (Error " + xmlhttp.status + ")";
1167        if (xmlhttp.responseText.length) {
1168            error += "\n" + xmlhttp.responseText;
1169        }
1170        alert(error);
1171        return;
1172    }
1173}
1174
1175Gettext.prototype.JSON = function (data) {
1176    return eval('(' + data + ')');
1177}
1178
1179
1180/*
1181
1182=head1 NOTES
1183
1184These are some notes on the internals
1185
1186=over
1187
1188=item LOCALE CACHING
1189
1190Loaded 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.
1191
1192=back
1193
1194=head1 BUGS / TODO
1195
1196=over
1197
1198=item error handling
1199
1200Currently, 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.
1201
1202=item syncronous only support (no ajax support)
1203
1204Currently, fetching language data is done purely syncronous, which means the page will halt while those files are fetched/loaded.
1205
1206This 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.
1207
1208It's still recommended to use the statically defined <script ...> method, which should have the same delay, but it will cache the result.
1209
1210=item domain support
1211
1212domain support while using shortcut methods like C<_('string')> or C<i18n('string')>.
1213
1214Under 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.
1215
1216It's recommended that your app initialize it's own shortcut with it's own domain.  (See examples/wrapper/i18n.js for an example.)
1217
1218Basically, you'll want to accomplish something like this:
1219
1220    // in some other .js file that needs i18n
1221    this.i18nObj = new i18n;
1222    this.i18n = this.i18nObj.init('domain');
1223    // do translation
1224    alert( this.i18n("string") );
1225
1226If 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.
1227
1228
1229=item encoding
1230
1231May want to add encoding/reencoding stuff. See GNU iconv, or the perl module Locale::Recode from libintl-perl.
1232
1233=back
1234
1235
1236=head1 COMPATABILITY
1237
1238This has been tested on the following browsers. It may work on others, but these are all those to which I have access.
1239
1240    FF1.5, FF2, FF3, IE6, IE7, Opera9, Opera10, Safari3.1, Chrome
1241
1242    *FF = Firefox
1243    *IE = Internet Explorer
1244
1245
1246=head1 REQUIRES
1247
1248bin/po2json requires perl, and the perl modules Locale::PO and JSON.
1249
1250=head1 SEE ALSO
1251
1252bin/po2json (included),
1253examples/normal/index.html,
1254examples/wrapper/i18n.html, examples/wrapper/i18n.js,
1255Locale::gettext_pp(3pm), POSIX(3pm), gettext(1), gettext(3)
1256
1257=head1 AUTHOR
1258
1259Copyright (C) 2008, Joshua I. Miller E<lt>[email protected]<gt>, all rights reserved. See the source code for details.
1260
1261=cut
1262
1263*/
1264

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.