PageRenderTime 110ms CodeModel.GetById 34ms app.highlight 66ms RepoModel.GetById 1ms app.codeStats 0ms

/js/jsgettext/resources/Gettext.js

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