comparison annotator_files/lib/vendor/gettext.js @ 3:6356e78ccf5c

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