Bug 21156: Add plural translation capabilities to JS files
[koha.git] / koha-tmpl / opac-tmpl / bootstrap / js / Gettext.js
blobce6bf96877833e0afb92470c9a752a961b0fda6b
1 /*
2 Pure Javascript implementation of Uniforum message translation.
3 Copyright (C) 2008 Joshua I. Miller <unrtst@cpan.org>, all rights reserved
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.
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.
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.
20 =head1 NAME
22 Javascript Gettext - Javascript implemenation of GNU Gettext API.
24 =head1 SYNOPSIS
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>
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"));
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>
55  var gt = new Gettext({ "domain" : "myDomain" });
56  // rest is the same
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:
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  };
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") );
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      }
102 =head1 DESCRIPTION
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).
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.
109 =head1 INSTALL
111 To install this module, simply copy the file lib/Gettext.js to a web accessable location, and reference it from your application.
114 =head1 CONFIGURATION
116 Configure in one of two ways:
118 =over
120 =item 1. Optimal. Load language definition from statically defined json data.
122     <script language="javascript" src="/path/locale/domain.json"></script>
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
138 This method also allows you to use unsupported file formats, so long as you can parse them into the above format.
140 =item 2. Use AJAX to load language file.
142 Use XMLHttpRequest (actually, SJAX - syncronous) to load an external resource.
144 Supported external formats are:
146 =over
148 =item * Javascript Object Notation (.json)
150 (see bin/po2json)
152     type=application/json
154 =item * Uniforum Portable Object (.po)
156 (see GNU Gettext's xgettext)
158     type=application/x-po
160 =item * Machine Object (compiled .po) (.mo)
162 NOTE: .mo format isn't actually supported just yet, but support is planned.
164 (see GNU Gettext's msgfmt)
166     type=application/x-mo
168 =back
170 =back
172 =head1 METHODS
174 The following methods are implemented:
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)
193 =head2 new Gettext (args)
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:
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() );
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:
206     var gt = new Gettext( 'domain' : 'messages',
207                           'locale_data' : json_locale_data_variable );
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:
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">
216 args:
218 =over
220 =item domain
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".
224 =item locale_data
226 Raw locale data (in json structure). If specified, from_link data will be ignored.
228 =back
230 =cut
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;
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     }
254     // try to load the lang file from somewhere
255     this.try_load_lang();
257     return this;
260 Gettext.context_glue = "\004";
261 Gettext._locale_data = {};
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);
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     }
277     // try loading from JSON
278     // get lang links
279     var lang_link = this.get_lang_refs();
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     }
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     }
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;
330         // grab shortcut to data
331         var data = locale_data[domain];
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 = { };
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     }
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)";
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             }
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     }
392     return;
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;
401     var domain = this.uri_basename(uri);
402     var parsed = this.parse_po(data);
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;
412         this.parse_locale_data(rv);
413     }
415     return 1;
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     }
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)+$/, '');
441         var match;
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;
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);
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;
468                 buffer = {};
469                 lastbuffer = "";
470             }
472         // comments
473         } else if (/^#/.test(lines[i])) {
474             continue;
476         // msgctxt
477         } else if (match = lines[i].match(/^msgctxt\s+(.*)/)) {
478             lastbuffer = 'msgctxt';
479             buffer[lastbuffer] = this.parse_po_dequote(match[1]);
481         // msgid
482         } else if (match = lines[i].match(/^msgid\s+(.*)/)) {
483             lastbuffer = 'msgid';
484             buffer[lastbuffer] = this.parse_po_dequote(match[1]);
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]);
491         // msgstr
492         } else if (match = lines[i].match(/^msgstr\s+(.*)/)) {
493             lastbuffer = 'msgstr_0';
494             buffer[lastbuffer] = this.parse_po_dequote(match[1]);
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]);
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]);
506         // continued string
507         } else if (/^"/.test(lines[i])) {
508             buffer[lastbuffer] += this.parse_po_dequote(lines[i]);
510         // something strange
511         } else {
512             errors.push("Strange line ["+i+"] : "+lines[i]);
513         }
514     }
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;
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);
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;
541         buffer = {};
542         lastbuffer = "";
543     }
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;
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();
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                 }
569             } else {
570                 errors.push("PROBLEM LINE IN HEADER: "+hlines[i]);
571                 cur[hlines[i]] = '';
572             }
573         }
575         // replace header string with assoc array
576         rv[""] = cur;
577     } else {
578         rv[""] = {};
579     }
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") );
585     return rv;
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;
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;
605     var rv = this.JSON(data);
606     this.parse_locale_data(rv);
608     return 1;
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             }
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             }
647             langs.push(links[i]);
648         }
649     }
650     return langs;
656 =head2 textdomain( domain )
658 Set domain for future gettext() calls
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.
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.
670 If domainname is not NULL, the current message domain is set to
671 domainname.
673 If domainname is undefined, null, or empty string, the function returns
674 the current message domain.
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).
680 =cut
683 Gettext.prototype.textdomain = function (domain) {
684     if (domain && domain.length) this.domain = domain;
685     return this.domain;
690 =head2 gettext( MSGID )
692 Returns the translation for B<MSGID>.  Example:
694     alert( gt.gettext("Hello World!\n") );
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.
700 One common mistake is to interpolate a variable into the string like this:
702   var translated = gt.gettext("Hello " + full_name);
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.
708 Use C<strargs()> (see below) to solve this problem:
710   var translated = Gettext.strargs( gt.gettext("Hello %1"), [full_name] );
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:
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"] );
721 (The example is stupid because neither color nor thing will get
722 translated here ...).
724 =head2 dgettext( TEXTDOMAIN, MSGID )
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.
730 =head2 dcgettext( TEXTDOMAIN, MSGID, CATEGORY )
732 Like dgettext() but retrieves the message from the specified B<CATEGORY>
733 instead of the default category C<LC_MESSAGES>.
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>.
740 =head2 ngettext( MSGID, MSGID_PLURAL, COUNT )
742 Retrieves the correct translation for B<COUNT> items.  In legacy software
743 you will often find something like:
745     alert( count + " file(s) deleted.\n" );
749     printf(count + " file%s deleted.\n", $count == 1 ? '' : 's');
751 I<NOTE: javascript lacks a builtin printf, so the above isn't a working example>
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:
757     if (count == 1) {
758         alert( gettext("One file deleted.\n") );
759     } else {
760         printf( gettext("%d files deleted.\n"), count );
761     }
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:
770     printf( ngettext("One file deleted.\n",
771                      "%d files deleted.\n",
772                      count), // argument to ngettext!
773             count);          // argument to printf!
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.
782 This is most of the time sufficient but you will have to prove your
783 creativity in cases like
785     "%d file(s) deleted, and %d file(s) created.\n"
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:
790     Gettext.strargs( gt.ngettext( "One file deleted.\n",
791                                   "%d files deleted.\n",
792                                   count), // argument to ngettext!
793                      count); // argument to strargs!
795 NOTE: the variable replacement isn't done for you, so you must
796 do it yourself as in the above.
798 =head2 dngettext( TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT )
800 Like ngettext() but retrieves the translation from the specified
801 textdomain instead of the default domain.
803 =head2 dcngettext( TEXTDOMAIN, MSGID, MSGID_PLURAL, COUNT, CATEGORY )
805 Like dngettext() but retrieves the translation from the specified
806 category, instead of the default category C<LC_MESSAGES>.
809 =head2 pgettext( MSGCTXT, MSGID )
811 Returns the translation of MSGID, given the context of MSGCTXT.
813 Both items are used as a unique key into the message catalog.
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.
820     alert( pgettext( "Verb: To View", "View" ) );
821     alert( pgettext( "Noun: A View", "View"  ) );
823 The above will both lookup different entries in the message catalog.
825 In English, or if no translation can be found, the second argument
826 (B<MSGID>) is returned.
828 =head2 dpgettext( TEXTDOMAIN, MSGCTXT, MSGID )
830 Like pgettext(), but retrieves the message for the specified
831 B<TEXTDOMAIN> instead of the default domain.
833 =head2 dcpgettext( TEXTDOMAIN, MSGCTXT, MSGID, CATEGORY )
835 Like dpgettext() but retrieves the message from the specified B<CATEGORY>
836 instead of the default category C<LC_MESSAGES>.
839 =head2 npgettext( MSGCTXT, MSGID, MSGID_PLURAL, COUNT )
841 Like ngettext() with the addition of context as in pgettext().
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.
846 =head2 dnpgettext( TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT )
848 Like npgettext() but retrieves the translation from the specified
849 textdomain instead of the default domain.
851 =head2 dcnpgettext( TEXTDOMAIN, MSGCTXT, MSGID, MSGID_PLURAL, COUNT, CATEGORY )
853 Like dnpgettext() but retrieves the translation from the specified
854 category, instead of the default category C<LC_MESSAGES>.
856 =cut
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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);
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 '';
938     var plural = this.isValidObject(msgid_plural);
939     var msg_ctxt_id = this.isValidObject(msgctxt) ? msgctxt+Gettext.context_glue+msgid : msgid;
941     var domainname = this.isValidObject(domain)      ? domain :
942                      this.isValidObject(this.domain) ? this.domain :
943                                                        'messages';
945     // category is always LC_MESSAGES. We ignore all else
946     var category_name = 'LC_MESSAGES';
947     var category = 5;
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] );
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     }
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     }
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     }
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     }
1004     return translation;
1010 =head2 strargs (string, argument_array)
1012   string : a string that potentially contains formatting characters.
1013   argument_array : an array of positional replacement values
1015 This is a utility method to provide some way to support positional parameters within a string, as javascript lacks a printf() method.
1017 The format is similar to printf(), but greatly simplified (ie. fewer features).
1019 Any percent signs followed by numbers are replaced with the corrosponding item from the B<argument_array>.
1021 Example:
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"
1028 The format numbers are 1 based, so the first itme is %1.
1030 A lone percent sign may be escaped by preceeding it with another percent sign.
1032 A percent sign followed by anything other than a number or another percent sign will be passed through as is.
1034 Some more examples should clear up any abmiguity. The following were called with the orig string, and the array as Array("[one]", "[two]") :
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"
1049 This is especially useful when using plurals, as the string will nearly always contain the number.
1051 It's also useful in translated strings where the translator may have needed to move the position of the parameters.
1053 For example:
1055   var count = 14;
1056   Gettext.strargs( gt.ngettext('one banana', '%1 bananas', count), [count] );
1058 NOTE: this may be called as an instance method, or as a class method.
1060   // instance method:
1061   var gt = new Gettext(params);
1062   gt.strargs(string, args);
1064   // class method:
1065   Gettext.strargs(string, args);
1067 =cut
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     }
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
1086     var newstr = "";
1087     while (true) {
1088         var i = str.indexOf('%');
1089         var match_n;
1091         // no more found. Append whatever remains
1092         if (i == -1) {
1093             newstr += str;
1094             break;
1095         }
1097         // we found it, append everything up to that
1098         newstr += str.substr(0, i);
1100         // check for escpaed %%
1101         if (str.substr(i, 2) == '%%') {
1102             newstr += '%';
1103             str = str.substr((i+2));
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) );
1113         // % followed by some other garbage - just remove the %
1114         } else {
1115             newstr += '%';
1116             str = str.substr((i+1));
1117         }
1118     }
1120     return newstr;
1123 /* instance method wrapper of strargs */
1124 Gettext.prototype.strargs = function (str, args) {
1125     return Gettext.strargs(str, args);
1128 /* verify that something is an array */
1129 Gettext.prototype.isArray = function (thisObject) {
1130     return this.isValidObject(thisObject) && thisObject.constructor == Array;
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     }
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     }
1154     if (! xmlhttp)
1155         throw new Error("Your browser doesn't do Ajax. Unable to support external language files.");
1157     xmlhttp.open('GET', uri, false);
1158     try { xmlhttp.send(null); }
1159     catch (e) { return; }
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     }
1176 Gettext.prototype.JSON = function (data) {
1177     return eval('(' + data + ')');
1183 =head1 NOTES
1185 These are some notes on the internals
1187 =over
1189 =item LOCALE CACHING
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.
1193 =back
1195 =head1 BUGS / TODO
1197 =over
1199 =item error handling
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.
1203 =item syncronous only support (no ajax support)
1205 Currently, fetching language data is done purely syncronous, which means the page will halt while those files are fetched/loaded.
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.
1209 It's still recommended to use the statically defined <script ...> method, which should have the same delay, but it will cache the result.
1211 =item domain support
1213 domain support while using shortcut methods like C<_('string')> or C<i18n('string')>.
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.
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.)
1219 Basically, you'll want to accomplish something like this:
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") );
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.
1230 =item encoding
1232 May want to add encoding/reencoding stuff. See GNU iconv, or the perl module Locale::Recode from libintl-perl.
1234 =back
1237 =head1 COMPATABILITY
1239 This has been tested on the following browsers. It may work on others, but these are all those to which I have access.
1241     FF1.5, FF2, FF3, IE6, IE7, Opera9, Opera10, Safari3.1, Chrome
1243     *FF = Firefox
1244     *IE = Internet Explorer
1247 =head1 REQUIRES
1249 bin/po2json requires perl, and the perl modules Locale::PO and JSON.
1251 =head1 SEE ALSO
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)
1258 =head1 AUTHOR
1260 Copyright (C) 2008, Joshua I. Miller E<lt>unrtst@cpan.orgE<gt>, all rights reserved. See the source code for details.
1262 =cut