Permalink
webL10n/l10n.js
Newer
100644
1210 lines (1115 sloc)
33.9 KB
1
/**
2
* Copyright (c) 2011-2013 Fabien Cazenave, Mozilla.
3
*
4
* Permission is hereby granted, free of charge, to any person obtaining a copy
5
* of this software and associated documentation files (the "Software"), to
6
* deal in the Software without restriction, including without limitation the
7
* rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
8
* sell copies of the Software, and to permit persons to whom the Software is
9
* furnished to do so, subject to the following conditions:
10
*
11
* The above copyright notice and this permission notice shall be included in
12
* all copies or substantial portions of the Software.
13
*
14
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
19
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
20
* IN THE SOFTWARE.
21
*/
22
23
/*jshint browser: true, devel: true, es5: true, globalstrict: true */
26
document.webL10n = (function(window, document, undefined) {
27
var gL10nData = {};
28
var gTextData = '';
29
var gTextProp = 'textContent';
30
var gLanguage = '';
31
var gMacros = {};
32
var gReadyState = 'loading';
33
35
/**
36
* Synchronously loading l10n resources significantly minimizes flickering
37
* from displaying the app with non-localized strings and then updating the
38
* strings. Although this will block all script execution on this page, we
39
* expect that the l10n resources are available locally on flash-storage.
40
*
41
* As synchronous XHR is generally considered as a bad idea, we're still
42
* loading l10n resources asynchronously -- but we keep this in a setting,
43
* just in case... and applications using this library should hide their
44
* content until the `localized' event happens.
45
*/
46
47
var gAsyncResourceLoading = true; // read-only
48
49
50
/**
51
* Debug helpers
52
*
53
* gDEBUG == 0: don't display any console message
54
* gDEBUG == 1: display only warnings, not logs
55
* gDEBUG == 2: display all console messages
56
*/
57
60
function consoleLog(message) {
62
console.log('[l10n] ' + message);
74
* DOM helpers for the so-called "HTML API".
75
*
76
* These functions are written for modern browsers. For old versions of IE,
77
* they're overridden in the 'startup' section at the end of this file.
78
*/
79
80
function getL10nResourceLinks() {
81
return document.querySelectorAll('link[type="application/l10n"]');
82
}
83
84
function getL10nDictionary() {
85
var script = document.querySelector('script[type="application/l10n"]');
86
// TODO: support multiple and external JSON dictionaries
87
return script ? JSON.parse(script.innerHTML) : null;
88
}
89
90
function getTranslatableChildren(element) {
91
return element ? element.querySelectorAll('*[data-l10n-id]') : [];
92
}
93
94
function getL10nAttributes(element) {
95
if (!element)
96
return {};
97
98
var l10nId = element.getAttribute('data-l10n-id');
99
var l10nArgs = element.getAttribute('data-l10n-args');
100
var args = {};
101
if (l10nArgs) {
102
try {
103
args = JSON.parse(l10nArgs);
104
} catch (e) {
105
consoleWarn('could not parse arguments for #' + l10nId);
106
}
107
}
108
return { id: l10nId, args: args };
109
}
110
111
function fireL10nReadyEvent(lang) {
112
var evtObject = document.createEvent('Event');
113
evtObject.initEvent('localized', true, false);
114
evtObject.language = lang;
115
document.dispatchEvent(evtObject);
118
function xhrLoadText(url, onSuccess, onFailure) {
119
onSuccess = onSuccess || function _onSuccess(data) {};
120
onFailure = onFailure || function _onFailure() {
121
consoleWarn(url + ' not found.');
122
};
123
124
var xhr = new XMLHttpRequest();
125
xhr.open('GET', url, gAsyncResourceLoading);
126
if (xhr.overrideMimeType) {
127
xhr.overrideMimeType('text/plain; charset=utf-8');
128
}
129
xhr.onreadystatechange = function() {
130
if (xhr.readyState == 4) {
131
if (xhr.status == 200 || xhr.status === 0) {
132
onSuccess(xhr.responseText);
133
} else {
134
onFailure();
135
}
136
}
137
};
138
xhr.onerror = onFailure;
139
xhr.ontimeout = onFailure;
140
141
// in Firefox OS with the app:// protocol, trying to XHR a non-existing
142
// URL will raise an exception here -- hence this ugly try...catch.
143
try {
144
xhr.send(null);
145
} catch (e) {
146
onFailure();
147
}
148
}
149
152
* l10n resource parser:
153
* - reads (async XHR) the l10n resource matching `lang';
154
* - imports linked resources (synchronously) when specified;
155
* - parses the text data (fills `gL10nData' and `gTextData');
156
* - triggers success/failure callbacks when done.
158
* @param {string} href
159
* URL of the l10n resource to parse.
160
*
161
* @param {string} lang
162
* locale (language) to parse. Must be a lowercase string.
163
*
164
* @param {Function} successCallback
165
* triggered when the l10n resource has been successully parsed.
166
*
167
* @param {Function} failureCallback
168
* triggered when the an error has occured.
169
*
170
* @return {void}
171
* uses the following global variables: gL10nData, gTextData, gTextProp.
172
*/
173
174
function parseResource(href, lang, successCallback, failureCallback) {
175
var baseURL = href.replace(/[^\/]*$/, '') || './';
176
177
// handle escaped characters (backslashes) in a string
178
function evalString(text) {
179
if (text.lastIndexOf('\\') < 0)
180
return text;
181
return text.replace(/\\\\/g, '\\')
182
.replace(/\\n/g, '\n')
183
.replace(/\\r/g, '\r')
184
.replace(/\\t/g, '\t')
185
.replace(/\\b/g, '\b')
186
.replace(/\\f/g, '\f')
187
.replace(/\\{/g, '{')
188
.replace(/\\}/g, '}')
189
.replace(/\\"/g, '"')
190
.replace(/\\'/g, "'");
191
}
192
193
// parse *.properties text data into an l10n dictionary
194
// If gAsyncResourceLoading is false, then the callback will be called
195
// synchronously. Otherwise it is called asynchronously.
196
function parseProperties(text, parsedPropertiesCallback) {
198
199
// token expressions
200
var reBlank = /^\s*|\s*$/;
201
var reComment = /^\s*#|^\s*$/;
202
var reSection = /^\s*\[(.*)\]\s*$/;
203
var reImport = /^\s*@import\s+url\((.*)\)\s*$/i;
204
var reSplit = /^([^=\s]*)\s*=\s*(.+)$/; // TODO: escape EOLs with '\'
205
206
// parse the *.properties file into an associative array
207
function parseRawLines(rawText, extendedSyntax, parsedRawLinesCallback) {
208
var entries = rawText.replace(reBlank, '').split(/[\r\n]+/);
209
var currentLang = '*';
210
var genericLang = lang.split('-', 1)[0];
211
var skipLang = false;
212
var match = '';
213
214
function nextEntry() {
215
// Use infinite loop instead of recursion to avoid reaching the
216
// maximum recursion limit for content with many lines.
217
while (true) {
218
if (!entries.length) {
219
parsedRawLinesCallback();
220
return;
221
}
222
var line = entries.shift();
224
// comment or blank line?
225
if (reComment.test(line))
227
228
// the extended syntax supports [lang] sections and @import rules
229
if (extendedSyntax) {
230
match = reSection.exec(line);
231
if (match) { // section start?
232
// RFC 4646, section 4.4, "All comparisons MUST be performed
233
// in a case-insensitive manner."
234
235
currentLang = match[1].toLowerCase();
236
skipLang = (currentLang !== '*') &&
237
(currentLang !== lang) && (currentLang !== genericLang);
238
continue;
239
} else if (skipLang) {
240
continue;
241
}
242
match = reImport.exec(line);
243
if (match) { // @import rule?
244
loadImport(baseURL + match[1], nextEntry);
245
return;
246
}
249
// key-value pair
250
var tmp = line.match(reSplit);
251
if (tmp && tmp.length == 3) {
252
dictionary[tmp[1]] = evalString(tmp[2]);
253
}
257
}
258
259
// import another *.properties file
260
function loadImport(url, callback) {
262
parseRawLines(content, false, callback); // don't allow recursive imports
263
}, null);
267
parseRawLines(text, true, function() {
268
parsedPropertiesCallback(dictionary);
269
});
270
}
271
272
// load and parse l10n data (warning: global variables are used here)
274
gTextData += response; // mostly for debug
275
276
// parse *.properties text data into an l10n dictionary
277
parseProperties(response, function(data) {
278
279
// find attribute descriptions, if any
280
for (var key in data) {
281
var id, prop, index = key.lastIndexOf('.');
282
if (index > 0) { // an attribute has been specified
283
id = key.substring(0, index);
284
prop = key.substr(index + 1);
285
} else { // no attribute: assuming text content by default
286
id = key;
287
prop = gTextProp;
288
}
289
if (!gL10nData[id]) {
290
gL10nData[id] = {};
291
}
292
gL10nData[id][prop] = data[key];
295
// trigger callback
296
if (successCallback) {
297
successCallback();
298
}
299
});
300
}, failureCallback);
303
// load and parse all resources for the specified locale
304
function loadLocale(lang, callback) {
305
// RFC 4646, section 2.1 states that language tags have to be treated as
306
// case-insensitive. Convert to lowercase for case-insensitive comparisons.
307
if (lang) {
308
lang = lang.toLowerCase();
309
}
310
311
callback = callback || function _callback() {};
312
314
gLanguage = lang;
315
316
// check all <link type="application/l10n" href="..." /> nodes
317
// and load the resource files
318
var langLinks = getL10nResourceLinks();
319
var langCount = langLinks.length;
321
// we might have a pre-compiled dictionary instead
322
var dict = getL10nDictionary();
323
if (dict && dict.locales && dict.default_locale) {
324
consoleLog('using the embedded JSON directory, early way out');
325
gL10nData = dict.locales[lang];
326
if (!gL10nData) {
327
var defaultLocale = dict.default_locale.toLowerCase();
328
for (var anyCaseLang in dict.locales) {
329
anyCaseLang = anyCaseLang.toLowerCase();
330
if (anyCaseLang === lang) {
331
gL10nData = dict.locales[lang];
332
break;
333
} else if (anyCaseLang === defaultLocale) {
334
gL10nData = dict.locales[defaultLocale];
335
}
336
}
337
}
338
callback();
339
} else {
340
consoleLog('no resource to load, early way out');
341
}
342
// early way out
343
fireL10nReadyEvent(lang);
347
348
// start the callback when all resources are loaded
349
var onResourceLoaded = null;
350
var gResourceCount = 0;
351
onResourceLoaded = function() {
352
gResourceCount++;
353
if (gResourceCount >= langCount) {
355
fireL10nReadyEvent(lang);
357
}
358
};
359
360
// load all resource files
363
// Note: If |gAsyncResourceLoading| is false, then the following callbacks
364
// are synchronously called.
365
this.load = function(lang, callback) {
366
parseResource(href, lang, callback, function() {
368
// lang not found, used default resource instead
369
consoleWarn('"' + lang + '" resource not found');
370
gLanguage = '';
371
// Resource not loaded, but we still need to call the callback.
372
callback();
373
});
374
};
375
}
376
377
for (var i = 0; i < langCount; i++) {
378
var resource = new L10nResourceLink(langLinks[i]);
379
resource.load(lang, onResourceLoaded);
383
// clear all l10n data
384
function clear() {
385
gL10nData = {};
386
gTextData = '';
387
gLanguage = '';
388
// TODO: clear all non predefined macros.
389
// There's no such macro /yet/ but we're planning to have some...
394
* Get rules for plural forms (shared with JetPack), see:
395
* http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html
396
* https://github.com/mozilla/addon-sdk/blob/master/python-lib/plural-rules-generator.p
398
* @param {string} lang
399
* locale (language) used.
402
* returns a function that gives the plural form name for a given integer:
403
* var fun = getPluralRules('en');
404
* fun(1) -> 'one'
405
* fun(0) -> 'other'
406
* fun(1000) -> 'other'.
409
function getPluralRules(lang) {
410
var locales2rules = {
411
'af': 3,
412
'ak': 4,
413
'am': 4,
414
'ar': 1,
415
'asa': 3,
416
'az': 0,
417
'be': 11,
418
'bem': 3,
419
'bez': 3,
420
'bg': 3,
421
'bh': 4,
422
'bm': 0,
423
'bn': 3,
424
'bo': 0,
425
'br': 20,
426
'brx': 3,
427
'bs': 11,
428
'ca': 3,
429
'cgg': 3,
430
'chr': 3,
431
'cs': 12,
432
'cy': 17,
433
'da': 3,
434
'de': 3,
435
'dv': 3,
436
'dz': 0,
437
'ee': 3,
438
'el': 3,
439
'en': 3,
440
'eo': 3,
441
'es': 3,
442
'et': 3,
443
'eu': 3,
444
'fa': 0,
445
'ff': 5,
446
'fi': 3,
447
'fil': 4,
448
'fo': 3,
449
'fr': 5,
450
'fur': 3,
451
'fy': 3,
452
'ga': 8,
453
'gd': 24,
454
'gl': 3,
455
'gsw': 3,
456
'gu': 3,
457
'guw': 4,
458
'gv': 23,
459
'ha': 3,
460
'haw': 3,
461
'he': 2,
462
'hi': 4,
463
'hr': 11,
464
'hu': 0,
465
'id': 0,
466
'ig': 0,
467
'ii': 0,
468
'is': 3,
469
'it': 3,
470
'iu': 7,
471
'ja': 0,
472
'jmc': 3,
473
'jv': 0,
474
'ka': 0,
475
'kab': 5,
476
'kaj': 3,
477
'kcg': 3,
478
'kde': 0,
479
'kea': 0,
480
'kk': 3,
481
'kl': 3,
482
'km': 0,
483
'kn': 0,
484
'ko': 0,
485
'ksb': 3,
486
'ksh': 21,
487
'ku': 3,
488
'kw': 7,
489
'lag': 18,
490
'lb': 3,
491
'lg': 3,
492
'ln': 4,
493
'lo': 0,
494
'lt': 10,
495
'lv': 6,
496
'mas': 3,
497
'mg': 4,
498
'mk': 16,
499
'ml': 3,
500
'mn': 3,
501
'mo': 9,
502
'mr': 3,
503
'ms': 0,
504
'mt': 15,
505
'my': 0,
506
'nah': 3,
507
'naq': 7,
508
'nb': 3,
509
'nd': 3,
510
'ne': 3,
511
'nl': 3,
512
'nn': 3,
513
'no': 3,
514
'nr': 3,
515
'nso': 4,
516
'ny': 3,
517
'nyn': 3,
518
'om': 3,
519
'or': 3,
520
'pa': 3,
521
'pap': 3,
522
'pl': 13,
523
'ps': 3,
524
'pt': 3,
525
'rm': 3,
526
'ro': 9,
527
'rof': 3,
528
'ru': 11,
529
'rwk': 3,
530
'sah': 0,
531
'saq': 3,
532
'se': 7,
533
'seh': 3,
534
'ses': 0,
535
'sg': 0,
536
'sh': 11,
537
'shi': 19,
538
'sk': 12,
539
'sl': 14,
540
'sma': 7,
541
'smi': 7,
542
'smj': 7,
543
'smn': 7,
544
'sms': 7,
545
'sn': 3,
546
'so': 3,
547
'sq': 3,
548
'sr': 11,
549
'ss': 3,
550
'ssy': 3,
551
'st': 3,
552
'sv': 3,
553
'sw': 3,
554
'syr': 3,
555
'ta': 3,
556
'te': 3,
557
'teo': 3,
558
'th': 0,
559
'ti': 4,
560
'tig': 3,
561
'tk': 3,
562
'tl': 4,
563
'tn': 3,
564
'to': 0,
565
'tr': 0,
566
'ts': 3,
567
'tzm': 22,
568
'uk': 11,
569
'ur': 3,
570
've': 3,
571
'vi': 0,
572
'vun': 3,
573
'wa': 4,
574
'wae': 3,
575
'wo': 0,
576
'xh': 3,
577
'xog': 3,
578
'yo': 0,
579
'zh': 0,
580
'zu': 3
581
};
583
// utility functions for plural rules methods
584
function isIn(n, list) {
585
return list.indexOf(n) !== -1;
586
}
587
function isBetween(n, start, end) {
588
return start <= n && n <= end;
591
// list of all plural rules methods:
592
// map an integer to the plural form name to use
593
var pluralRules = {
594
'0': function(n) {
595
return 'other';
596
},
597
'1': function(n) {
598
if ((isBetween((n % 100), 3, 10)))
599
return 'few';
600
if (n === 0)
601
return 'zero';
602
if ((isBetween((n % 100), 11, 99)))
603
return 'many';
604
if (n == 2)
605
return 'two';
606
if (n == 1)
607
return 'one';
608
return 'other';
609
},
610
'2': function(n) {
611
if (n !== 0 && (n % 10) === 0)
612
return 'many';
613
if (n == 2)
614
return 'two';
615
if (n == 1)
616
return 'one';
617
return 'other';
618
},
619
'3': function(n) {
620
if (n == 1)
621
return 'one';
622
return 'other';
623
},
624
'4': function(n) {
625
if ((isBetween(n, 0, 1)))
626
return 'one';
627
return 'other';
628
},
629
'5': function(n) {
630
if ((isBetween(n, 0, 2)) && n != 2)
631
return 'one';
632
return 'other';
633
},
634
'6': function(n) {
635
if (n === 0)
636
return 'zero';
637
if ((n % 10) == 1 && (n % 100) != 11)
638
return 'one';
639
return 'other';
640
},
641
'7': function(n) {
642
if (n == 2)
643
return 'two';
644
if (n == 1)
645
return 'one';
646
return 'other';
647
},
648
'8': function(n) {
649
if ((isBetween(n, 3, 6)))
650
return 'few';
651
if ((isBetween(n, 7, 10)))
652
return 'many';
653
if (n == 2)
654
return 'two';
655
if (n == 1)
656
return 'one';
657
return 'other';
658
},
659
'9': function(n) {
660
if (n === 0 || n != 1 && (isBetween((n % 100), 1, 19)))
661
return 'few';
662
if (n == 1)
663
return 'one';
664
return 'other';
665
},
666
'10': function(n) {
667
if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19)))
668
return 'few';
669
if ((n % 10) == 1 && !(isBetween((n % 100), 11, 19)))
670
return 'one';
671
return 'other';
672
},
673
'11': function(n) {
674
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
675
return 'few';
676
if ((n % 10) === 0 ||
677
(isBetween((n % 10), 5, 9)) ||
678
(isBetween((n % 100), 11, 14)))
679
return 'many';
680
if ((n % 10) == 1 && (n % 100) != 11)
681
return 'one';
682
return 'other';
683
},
684
'12': function(n) {
685
if ((isBetween(n, 2, 4)))
686
return 'few';
687
if (n == 1)
688
return 'one';
689
return 'other';
690
},
691
'13': function(n) {
692
if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14)))
693
return 'few';
694
if (n != 1 && (isBetween((n % 10), 0, 1)) ||
695
(isBetween((n % 10), 5, 9)) ||
696
(isBetween((n % 100), 12, 14)))
697
return 'many';
698
if (n == 1)
699
return 'one';
700
return 'other';
701
},
702
'14': function(n) {
703
if ((isBetween((n % 100), 3, 4)))
704
return 'few';
705
if ((n % 100) == 2)
706
return 'two';
707
if ((n % 100) == 1)
708
return 'one';
709
return 'other';
710
},
711
'15': function(n) {
712
if (n === 0 || (isBetween((n % 100), 2, 10)))
713
return 'few';
714
if ((isBetween((n % 100), 11, 19)))
715
return 'many';
716
if (n == 1)
717
return 'one';
718
return 'other';
719
},
720
'16': function(n) {
721
if ((n % 10) == 1 && n != 11)
722
return 'one';
723
return 'other';
724
},
725
'17': function(n) {
726
if (n == 3)
727
return 'few';
728
if (n === 0)
729
return 'zero';
730
if (n == 6)
731
return 'many';
732
if (n == 2)
733
return 'two';
734
if (n == 1)
735
return 'one';
736
return 'other';
737
},
738
'18': function(n) {
739
if (n === 0)
740
return 'zero';
741
if ((isBetween(n, 0, 2)) && n !== 0 && n != 2)
742
return 'one';
743
return 'other';
744
},
745
'19': function(n) {
746
if ((isBetween(n, 2, 10)))
747
return 'few';
748
if ((isBetween(n, 0, 1)))
749
return 'one';
750
return 'other';
751
},
752
'20': function(n) {
753
if ((isBetween((n % 10), 3, 4) || ((n % 10) == 9)) && !(
754
isBetween((n % 100), 10, 19) ||
755
isBetween((n % 100), 70, 79) ||
756
isBetween((n % 100), 90, 99)
757
))
758
return 'few';
759
if ((n % 1000000) === 0 && n !== 0)
760
return 'many';
761
if ((n % 10) == 2 && !isIn((n % 100), [12, 72, 92]))
762
return 'two';
763
if ((n % 10) == 1 && !isIn((n % 100), [11, 71, 91]))
764
return 'one';
765
return 'other';
766
},
767
'21': function(n) {
768
if (n === 0)
769
return 'zero';
770
if (n == 1)
771
return 'one';
772
return 'other';
773
},
774
'22': function(n) {
775
if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99)))
776
return 'one';
777
return 'other';
778
},
779
'23': function(n) {
780
if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0)
781
return 'one';
782
return 'other';
783
},
784
'24': function(n) {
785
if ((isBetween(n, 3, 10) || isBetween(n, 13, 19)))
786
return 'few';
787
if (isIn(n, [2, 12]))
788
return 'two';
789
if (isIn(n, [1, 11]))
790
return 'one';
791
return 'other';
795
// return a function that gives the plural form name for a given integer
796
var index = locales2rules[lang.replace(/-.*$/, '')];
797
if (!(index in pluralRules)) {
798
consoleWarn('plural form unknown for [' + lang + ']');
799
return function() { return 'other'; };
804
// pre-defined 'plural' macro
805
gMacros.plural = function(str, param, key, prop) {
806
var n = parseFloat(param);
807
if (isNaN(n))
808
return str;
810
// TODO: support other properties (l20n still doesn't...)
811
if (prop != gTextProp)
812
return str;
813
814
// initialize _pluralRules
816
gMacros._pluralRules = getPluralRules(gLanguage);
818
var index = '[' + gMacros._pluralRules(n) + ']';
819
820
// try to find a [zero|one|two] key if it's defined
821
if (n === 0 && (key + '[zero]') in gL10nData) {
822
str = gL10nData[key + '[zero]'][prop];
823
} else if (n == 1 && (key + '[one]') in gL10nData) {
824
str = gL10nData[key + '[one]'][prop];
825
} else if (n == 2 && (key + '[two]') in gL10nData) {
826
str = gL10nData[key + '[two]'][prop];
827
} else if ((key + index) in gL10nData) {
828
str = gL10nData[key + index][prop];
829
} else if ((key + '[other]') in gL10nData) {
830
str = gL10nData[key + '[other]'][prop];
838
* l10n dictionary functions
839
*/
840
841
// fetch an l10n object, warn if not found, apply `args' if possible
842
function getL10nData(key, args, fallback) {
846
if (!fallback) {
847
return null;
848
}
849
data = fallback;
851
852
/** This is where l10n expressions should be processed.
853
* The plan is to support C-style expressions from the l20n project;
854
* until then, only two kinds of simple expressions are supported:
855
* {[ index ]} and {{ arguments }}.
856
*/
857
var rv = {};
858
for (var prop in data) {
859
var str = data[prop];
860
str = substIndexes(str, args, key, prop);
863
}
864
return rv;
865
}
866
867
// replace {[macros]} with their values
868
function substIndexes(str, args, key, prop) {
869
var reIndex = /\{\[\s*([a-zA-Z]+)\(([a-zA-Z]+)\)\s*\]\}/;
870
var reMatch = reIndex.exec(str);
871
if (!reMatch || !reMatch.length)
872
return str;
873
874
// an index/macro has been found
875
// Note: at the moment, only one parameter is supported
876
var macroName = reMatch[1];
877
var paramName = reMatch[2];
878
var param;
879
if (args && paramName in args) {
880
param = args[paramName];
881
} else if (paramName in gL10nData) {
882
param = gL10nData[paramName];
883
}
884
885
// there's no macro parser yet: it has to be defined in gMacros
886
if (macroName in gMacros) {
887
var macro = gMacros[macroName];
888
str = macro(str, param, key, prop);
889
}
890
return str;
891
}
892
895
var reArgs = /\{\{\s*(.+?)\s*\}\}/g;
896
return str.replace(reArgs, function(matched_text, arg) {
900
if (arg in gL10nData) {
901
return gL10nData[arg];
902
}
903
consoleLog('argument {{' + arg + '}} for #' + key + ' is undefined.');
904
return matched_text;
905
});
908
// translate an HTML element
909
function translateElement(element) {
910
var l10n = getL10nAttributes(element);
911
if (!l10n.id)
914
// get the related l10n object
915
var data = getL10nData(l10n.id, l10n.args);
917
consoleWarn('#' + l10n.id + ' is undefined.');
918
return;
921
// translate element (TODO: security checks?)
923
if (getChildElementCount(element) === 0) {
924
element[gTextProp] = data[gTextProp];
925
} else {
926
// this element has element children: replace the content of the first
927
// (non-empty) child textNode and clear other child textNodes
928
var children = element.childNodes;
929
var found = false;
930
for (var i = 0, l = children.length; i < l; i++) {
931
if (children[i].nodeType === 3 && /\S/.test(children[i].nodeValue)) {
940
// if no (non-empty) textNode is found, insert a textNode before the
941
// first element child.
943
var textNode = document.createTextNode(data[gTextProp]);
944
element.insertBefore(textNode, element.firstChild);
945
}
946
}
947
delete data[gTextProp];
948
}
949
951
element[k] = data[k];
955
// webkit browsers don't currently support 'children' on SVG elements...
956
function getChildElementCount(element) {
958
return element.children.length;
959
}
960
if (typeof element.childElementCount !== 'undefined') {
961
return element.childElementCount;
963
var count = 0;
964
for (var i = 0; i < element.childNodes.length; i++) {
965
count += element.nodeType === 1 ? 1 : 0;
966
}
967
return count;
968
}
969
970
// translate an HTML subtree
971
function translateFragment(element) {
972
element = element || document.documentElement;
974
// check all translatable children (= w/ a `data-l10n-id' attribute)
975
var children = getTranslatableChildren(element);
977
for (var i = 0; i < elementCount; i++) {
982
if (element.nodeType === 1) {
983
translateElement(element);
984
}
987
988
/**
989
* Startup & Public API
990
*
991
* Warning: this part of the code contains browser-specific chunks --
992
* that's where obsolete browsers, namely IE8 and earlier, are handled.
993
*
994
* Unlike the rest of the lib, this section is not shared with FirefoxOS/Gaia.
997
// load the default locale on startup
998
function l10nStartup() {
999
gReadyState = 'interactive';
1000
1001
// most browsers expose the UI language as `navigator.language'
1002
// but IE uses `navigator.userLanguage' instead
1003
var userLocale = navigator.language || navigator.userLanguage;
1004
consoleLog('loading [' + userLocale + '] resources, ' +
1005
(gAsyncResourceLoading ? 'asynchronously.' : 'synchronously.'));
1006
1007
// load the default locale and translate the document if required
1008
if (document.documentElement.lang === userLocale) {
1009
loadLocale(userLocale);
1016
if (document.addEventListener) { // modern browsers and IE9+
1017
if (document.readyState === 'loading') {
1018
// the document is not fully loaded yet: wait for DOMContentLoaded.
1019
document.addEventListener('DOMContentLoaded', l10nStartup);
1020
} else {
1021
// l10n.js is being loaded with <script defer> or <script async>,
1022
// the DOM is ready for parsing.
1023
window.setTimeout(l10nStartup);
1025
} else if (window.attachEvent) { // IE8 and before (= oldIE)
1026
// TODO: check if jQuery is loaded (CSS selector + JSON + events)
1029
if (!window.console) {
1030
consoleLog = function(message) {}; // just ignore console.log calls
1031
consoleWarn = function(message) {
1033
alert('[l10n] ' + message); // vintage debugging, baby!
1038
// XMLHttpRequest for IE6
1039
if (!window.XMLHttpRequest) {
1040
xhrLoadText = function(url, onSuccess, onFailure) {
1041
onSuccess = onSuccess || function _onSuccess(data) {};
1042
onFailure = onFailure || function _onFailure() {
1043
consoleWarn(url + ' not found.');
1044
};
1045
var xhr = new ActiveXObject('Microsoft.XMLHTTP');
1046
xhr.open('GET', url, gAsyncResourceLoading);
1047
xhr.onreadystatechange = function() {
1048
if (xhr.readyState == 4) {
1049
if (xhr.status == 200) {
1050
onSuccess(xhr.responseText);
1051
} else {
1052
onFailure();
1053
}
1054
}
1055
};
1056
xhr.send(null);
1061
if (!window.JSON) {
1062
getL10nAttributes = function(element) {
1063
if (!element)
1064
return {};
1065
var l10nId = element.getAttribute('data-l10n-id'),
1066
l10nArgs = element.getAttribute('data-l10n-args'),
1067
args = {};
1068
if (l10nArgs) try {
1069
args = eval(l10nArgs); // XXX yeah, I know...
1070
} catch (e) {
1071
consoleWarn('could not parse arguments for #' + l10nId);
1072
}
1073
return { id: l10nId, args: args };
1074
};
1075
}
1076
1077
// override `getTranslatableChildren' and `getL10nResourceLinks'
1078
if (!document.querySelectorAll) {
1079
getTranslatableChildren = function(element) {
1080
if (!element)
1081
return [];
1082
var nodes = element.getElementsByTagName('*'),
1083
l10nElements = [],
1084
n = nodes.length;
1085
for (var i = 0; i < n; i++) {
1086
if (nodes[i].getAttribute('data-l10n-id'))
1087
l10nElements.push(nodes[i]);
1089
return l10nElements;
1090
};
1091
getL10nResourceLinks = function() {
1092
var links = document.getElementsByTagName('link'),
1095
for (var i = 0; i < n; i++) {
1096
if (links[i].type == 'application/l10n')
1097
l10nLinks.push(links[i]);
1099
return l10nLinks;
1100
};
1101
}
1103
// override `getL10nDictionary'
1104
if (!window.JSON || !document.querySelectorAll) {
1105
getL10nDictionary = function() {
1106
var scripts = document.getElementsByName('script');
1107
for (var i = 0; i < scripts.length; i++) {
1108
if (scripts[i].type == 'application/l10n') {
1109
return eval(scripts[i].innerHTML);
1110
}
1111
}
1112
return null;
1113
};
1114
}
1115
1116
// fire non-standard `localized' DOM events
1117
if (document.createEventObject && !document.createEvent) {
1118
fireL10nReadyEvent = function(lang) {
1119
// hack to simulate a custom event in IE:
1120
// to catch this event, add an event handler to `onpropertychange'
1121
document.documentElement.localized = 1;
1122
};
1123
}
1126
window.attachEvent('onload', function() {
1127
gTextProp = document.textContent === null ? 'textContent' : 'innerText';
1132
// cross-browser API (sorry, oldIE doesn't support getters & setters)
1135
get: function(key, args, fallbackString) {
1136
var index = key.lastIndexOf('.');
1137
var prop = gTextProp;
1138
if (index > 0) { // An attribute has been specified
1139
prop = key.substr(index + 1);
1140
key = key.substring(0, index);
1141
}
1142
var fallback;
1143
if (fallbackString) {
1144
fallback = {};
1145
fallback[prop] = fallbackString;
1147
var data = getL10nData(key, args, fallback);
1148
if (data && prop in data) {
1149
return data[prop];
1150
}
1151
return '{{' + key + '}}';
1155
getData: function() { return gL10nData; },
1156
getText: function() { return gTextData; },
1158
// get|set the document language
1160
setLanguage: function(lang, callback) {
1161
loadLocale(lang, function() {
1162
if (callback)
1163
callback();
1164
translateFragment();
1165
});
1166
},
1167
1168
// get the direction (ltr|rtl) of the current language
1170
// http://www.w3.org/International/questions/qa-scripts
1171
// Arabic, Hebrew, Farsi, Pashto, Urdu
1172
var rtlList = ['ar', 'he', 'fa', 'ps', 'ur'];
1173
var shortCode = gLanguage.split('-', 1)[0];
1174
return (rtlList.indexOf(shortCode) >= 0) ? 'rtl' : 'ltr';
1175
},
1176
1177
// translate an element or document fragment
1178
translate: translateFragment,
1179
1180
// this can be used to prevent race conditions
1181
getReadyState: function() { return gReadyState; },
1182
ready: function(callback) {
1183
if (!callback) {
1184
return;
1185
} else if (gReadyState == 'complete' || gReadyState == 'interactive') {
1186
window.setTimeout(function() {
1187
callback();
1188
});
1190
document.addEventListener('localized', function once() {
1191
document.removeEventListener('localized', once);
1192
callback();
1193
});
1195
document.documentElement.attachEvent('onpropertychange', function once(e) {
1196
if (e.propertyName === 'localized') {
1197
document.documentElement.detachEvent('onpropertychange', once);
1198
callback();
1199
}
1200
});
1201
}
1202
}
1206
// gettext-like shortcut for document.webL10n.get