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