Publish Vulpes Preview 15 sources for desktop, Android and Tundra

Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
This commit is contained in:
OverSu 2026-09-30 23:13:03 +02:00
commit b38dea0597
25901 changed files with 9982925 additions and 369 deletions

View file

@ -0,0 +1,132 @@
'use strict';
suite('L10n bootstrap', function() {
suite('buildLocaleList', function() {
var buildLocaleList,
defaultLocale,
realGaiaVersion;
suiteSetup(function() {
var L10n = navigator.mozL10n._getInternalAPI();
defaultLocale = navigator.mozL10n.ctx.defaultLocale;
buildLocaleList = L10n.buildLocaleList.bind(navigator.mozL10n);
realGaiaVersion = navigator.mozL10n._config.appVersion;
navigator.mozL10n._config.appVersion = '2.2';
});
suiteTeardown(function() {
navigator.mozL10n._config.appVersion = realGaiaVersion;
realGaiaVersion = undefined;
defaultLocale = undefined;
buildLocaleList = undefined;
});
test('no meta, no extra', function() {
var meta = {};
var res = buildLocaleList(meta);
assert.equal(res[0], defaultLocale);
var obj = {};
obj[defaultLocale] = 'app';
assert.deepEqual(res[1], obj);
});
test('meta with just defaultLanguage, no extra', function() {
var meta = {defaultLanguage: 'ab-CD'};
var res = buildLocaleList(meta);
assert.equal(res[0], 'ab-CD');
assert.deepEqual(res[1], {'ab-CD': 'app'});
});
test('full meta, simple availableLanguages, no extra', function() {
var meta = {
defaultLanguage: 'ab-CD',
availableLanguages: {'lang1': NaN}
};
var res = buildLocaleList(meta);
assert.equal(res[0], 'ab-CD');
assert.deepEqual(res[1], {'ab-CD': 'app', 'lang1': 'app'});
});
test('full meta, no extra', function() {
var meta = {
defaultLanguage: 'ab-CD',
availableLanguages: {'lang1': 201501151000}
};
var res = buildLocaleList(meta);
assert.equal(res[0], 'ab-CD');
assert.deepEqual(res[1], {'ab-CD': 'app', 'lang1': 'app'});
});
test('full meta, extra, matching target', function() {
var meta = {
defaultLanguage: 'ab-CD',
availableLanguages: {'lang1': 201501151000}
};
var extraLangs = {
'lang2': [
{'revision': 201501151213, 'target': '2.2'}
]
};
var res = buildLocaleList(meta, extraLangs);
assert.equal(res[0], 'ab-CD');
assert.deepEqual(res[1], {
'ab-CD': 'app',
'lang1': 'app',
'lang2': 'extra'
});
});
test('full meta, extra, target not matching', function() {
var meta = {
defaultLanguage: 'ab-CD',
availableLanguages: {'lang1': 201501151000}
};
var extraLangs = {
'lang2': [
{'revision': 201501151213, 'target': '2.3'}
]
};
var res = buildLocaleList(meta, extraLangs);
assert.equal(res[0], 'ab-CD');
assert.deepEqual(res[1], {'ab-CD': 'app', 'lang1': 'app'});
});
test('full meta, extra, updated locale', function() {
var meta = {
defaultLanguage: 'ab-CD',
availableLanguages: {'lang1': 201501151000}
};
var extraLangs = {
'lang1': [
{'revision': 201501151213, 'target': '2.2'}
]
};
var res = buildLocaleList(meta, extraLangs);
assert.equal(res[0], 'ab-CD');
assert.deepEqual(res[1], {'ab-CD': 'app', 'lang1': 'extra'});
});
test('full meta, extra, updated locale obsolete', function() {
var meta = {
defaultLanguage: 'ab-CD',
availableLanguages: {'lang1': 201501161000}
};
var extraLangs = {
'lang1': [
{'revision': 201501151213, 'target': '2.2'}
]
};
var res = buildLocaleList(meta, extraLangs);
assert.equal(res[0], 'ab-CD');
assert.deepEqual(res[1], {'ab-CD': 'app', 'lang1': 'app'});
});
});
});

View file

@ -0,0 +1,265 @@
'use strict';
var strings = {
'lang1': [
'cropimage = Crop',
'cropimage2 = Crop2',
'input.placeholder = Placeholder',
'input2.placeholder = Placeholder 2',
'header1 = Header 1',
'header11 = Header 1.1',
'header2 = Header 2',
'header2.title = Header 2 Title',
'unreadEmails = You have {{ emails }} unread emails',
'bad.title = alert(1)',
'bad.onclick = alert(1)',
],
'lang2': [
'cropimage = Crop Lang2',
'cropimage2 = Crop2 Lang2',
'input = Foo Lang2',
'input.placeholder = Placeholder Lang2',
'header1 = Header 1 Lang2',
'header11 = Header 1.1 Lang2',
'header2 = Header 2 Lang2',
'header2.title = Header 2 Title Lang2',
]
};
suite('L10n bindings', function() {
var xhr;
var lang;
// Do not begin tests until the test locale has been loaded.
suiteSetup(function(done) {
xhr = sinon.useFakeXMLHttpRequest();
xhr.onCreate = function(request) {
setTimeout(function() {
request.respond(200, {}, strings[lang].join('\n'));
}, 0);
};
// en-US has already been loaded in setup.js and l10n.js is smart enough
// not to re-fetch resources; hence, set the lang to something new
lang = 'lang1';
navigator.mozL10n.ctx.registerLocales('en-US', ['lang1', 'lang2']);
navigator.mozL10n.language.code = lang;
navigator.mozL10n.once(done);
});
suiteTeardown(function() {
xhr.restore();
});
suite('mutations', function() {
suite('element', function() {
var elem;
suiteSetup(function() {
elem = document.createElement('div');
elem.setAttribute('data-l10n-id', 'cropimage');
});
suiteTeardown(function() {
document.body.removeChild(elem);
});
test('insert', function(done) {
document.body.appendChild(elem);
setTimeout(function() {
assert.equal(elem.textContent, 'Crop');
done();
});
});
test('change l10n-id', function(done) {
elem.setAttribute('data-l10n-id', 'cropimage2');
setTimeout(function() {
assert.equal(elem.textContent, 'Crop2');
done();
});
});
test('remove l10n-id', function(done) {
elem.removeAttribute('data-l10n-id');
setTimeout(function() {
assert.equal(elem.textContent, 'Crop2');
done();
});
});
});
suite('attribute', function() {
var elem;
suiteSetup(function() {
elem = document.createElement('input');
elem.setAttribute('data-l10n-id', 'input');
});
suiteTeardown(function() {
document.body.removeChild(elem);
});
test('insert', function(done) {
document.body.appendChild(elem);
setTimeout(function() {
assert.equal(elem.getAttribute('placeholder'), 'Placeholder');
done();
});
});
test('change l10n-id', function(done) {
elem.setAttribute('data-l10n-id', 'input2');
setTimeout(function() {
assert.equal(elem.getAttribute('placeholder'), 'Placeholder 2');
done();
});
});
test('remove l10n-id', function(done) {
elem.removeAttribute('data-l10n-id');
setTimeout(function() {
assert.equal(elem.getAttribute('placeholder'), 'Placeholder 2');
done();
});
});
});
suite('l10n args', function() {
var elem;
suiteSetup(function() {
elem = document.createElement('div');
elem.setAttribute('data-l10n-args', JSON.stringify({
emails: 5
}));
elem.setAttribute('data-l10n-id', 'unreadEmails');
});
suiteTeardown(function() {
document.body.removeChild(elem);
});
test('insert', function(done) {
document.body.appendChild(elem);
setTimeout(function() {
assert.equal(elem.textContent, 'You have 5 unread emails');
done();
});
});
test('change l10n-args', function(done) {
elem.setAttribute('data-l10n-args', JSON.stringify({
emails: 6
}));
setTimeout(function() {
assert.equal(elem.textContent, 'You have 6 unread emails');
done();
});
});
});
suite('DOMFragment', function() {
var frag, h1, h2;
suiteSetup(function() {
frag = document.createElement('div');
h1 = document.createElement('h1');
h2 = document.createElement('h2');
h1.setAttribute('data-l10n-id', 'header1');
h2.setAttribute('data-l10n-id', 'header2');
frag.appendChild(h1);
frag.appendChild(h2);
});
suiteTeardown(function() {
document.body.removeChild(frag);
});
test('insert', function(done) {
document.body.appendChild(frag);
setTimeout(function() {
assert.equal(h1.textContent, 'Header 1');
assert.equal(h2.textContent, 'Header 2');
assert.equal(h2.title, 'Header 2 Title');
done();
});
});
test('change l10n-id', function(done) {
h1.setAttribute('data-l10n-id', 'header11');
setTimeout(function() {
assert.equal(h1.textContent, 'Header 1.1');
done();
});
});
test('remove l10n-id', function(done) {
h2.removeAttribute('data-l10n-id');
setTimeout(function() {
assert.equal(h2.textContent, 'Header 2');
done();
});
});
});
suite('Language change', function() {
var elem, frag, h1, h2;
suiteSetup(function() {
lang = 'lang2';
elem = document.createElement('div');
frag = document.createElement('div');
h1 = document.createElement('h1');
h2 = document.createElement('h2');
elem.setAttribute('data-l10n-id', 'cropimage');
h1.setAttribute('data-l10n-id', 'header1');
h2.setAttribute('data-l10n-id', 'header2');
frag.appendChild(h1);
frag.appendChild(h2);
document.body.appendChild(elem);
document.body.appendChild(frag);
});
suiteTeardown(function(done) {
document.body.removeChild(frag);
lang = 'lang1';
navigator.mozL10n.ctx.addEventListener('ready', function onReady() {
navigator.mozL10n.ctx.removeEventListener('ready', onReady);
done();
});
navigator.mozL10n.ctx.requestLocales(lang);
});
test('retranslation', function(done) {
navigator.mozL10n.ctx.addEventListener('ready', function onReady() {
navigator.mozL10n.ctx.removeEventListener('ready', onReady);
setTimeout(function() {
assert.equal(elem.textContent, 'Crop Lang2');
assert.equal(h1.textContent, 'Header 1 Lang2');
assert.equal(h2.textContent, 'Header 2 Lang2');
assert.equal(h2.title, 'Header 2 Title Lang2');
done();
});
});
navigator.mozL10n.ctx.requestLocales(lang);
});
});
});
suite('whitelisting', function() {
test('does not allow attributes not on the whitelist', function() {
var elem = document.createElement('div');
elem.setAttribute('data-l10n-id', 'bad');
navigator.mozL10n.translateFragment(elem);
assert.equal(elem.getAttribute('title'), 'alert(1)');
assert.equal(elem.getAttribute('onclick'), undefined);
});
});
});

View file

@ -0,0 +1,285 @@
'use strict';
var strings = {
'lang1': [
'text-em = Hello, <em title="WORLD">world</em>!',
'text-em-sup = Hello, <em title="WORLD">world</em><sup>(foo)</sup>!',
'text-entity-name = Hello &amp; world',
'text-entity-nnnn = Hello &#0038; world',
'text-entity-hhhh = Hello &#x0026; world',
'struct-a = Read <a title="MORE">more</a>.',
'struct-em-a = <em>Read</em> <a>more</a>.',
'struct-a-a = Read <a>more</a> or <a>cancel</a>.',
'struct-button = <button>Submit</button>',
'struct-button-a = <button>Submit</button> or <a>cancel</a>.',
'struct-a-button = <a>Cancel</a> or <button>submit</button>.',
'struct-input-input = <input placeholder="Type here"> and ' +
'<input value="send">.',
'filter-button = Hello, <button title="WORLD">world</button>!',
'filter-em-onclick = Hello, <em onclick="alert(2)">world</em>!',
'filter-button-onclick = Hello, <button onclick="alert(2)">world</button>!',
'filter-href = Read <a href="#B">more</a>.',
'filter-input-value = <input value="Other value">',
'filter-input-type = <input type="submit" value="Submit"> the form.',
'filter-nested-button = <em>No <button>button</button>.</em>',
],
'lang2': [
'struct-a = Czytaj <a>więcej</a>.'
]
};
suite('L10n DOM overlays', function() {
var xhr;
var lang;
// Do not begin tests until the test locale has been loaded.
suiteSetup(function(done) {
xhr = sinon.useFakeXMLHttpRequest();
xhr.onCreate = function(request) {
setTimeout(function() {
request.respond(200, {}, strings[lang].join('\n'));
}, 0);
};
// en-US has already been loaded in setup.js and l10n.js is smart enough
// not to re-fetch resources; hence, set the lang to something new
lang = 'lang1';
navigator.mozL10n.ctx.registerLocales('en-US', ['lang1', 'lang2']);
navigator.mozL10n.language.code = lang;
navigator.mozL10n.once(done);
});
suiteTeardown(function() {
xhr.restore();
});
suite('text-level semantics', function() {
var elem;
setup(function() {
elem = document.createElement('div');
});
test('em is allowed', function() {
navigator.mozL10n.setAttributes(elem, 'text-em');
navigator.mozL10n.translateFragment(elem);
assert.equal(elem.innerHTML, 'Hello, <em title="WORLD">world</em>!');
});
test('em and sup are both allowed', function() {
navigator.mozL10n.setAttributes(elem, 'text-em-sup');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML, 'Hello, <em title="WORLD">world</em><sup>(foo)</sup>!');
});
test('entity name is allowed', function() {
navigator.mozL10n.setAttributes(elem, 'text-entity-name');
navigator.mozL10n.translateFragment(elem);
assert.equal(elem.innerHTML, 'Hello &amp; world');
});
test('entity decimal codepoint is allowed', function() {
navigator.mozL10n.setAttributes(elem, 'text-entity-nnnn');
navigator.mozL10n.translateFragment(elem);
assert.equal(elem.innerHTML, 'Hello &amp; world');
});
test('entity hexadecimal codepoint is allowed', function() {
navigator.mozL10n.setAttributes(elem, 'text-entity-hhhh');
navigator.mozL10n.translateFragment(elem);
assert.equal(elem.innerHTML, 'Hello &amp; world');
});
});
suite('structural overlay', function() {
var elem;
setup(function() {
elem = document.createElement('div');
});
test('href is overlaid', function() {
elem.innerHTML = '<a href="#A" title="TITLE">A</a>';
navigator.mozL10n.setAttributes(elem, 'struct-a');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'Read <a href="#A" title="MORE">more</a>.');
});
test('href is overlaid onto a', function() {
elem.innerHTML = '<a href="#A" title="TITLE">A</a>';
navigator.mozL10n.setAttributes(elem, 'struct-em-a');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'<em>Read</em> <a href="#A" title="TITLE">more</a>.');
});
test('hrefs are overlaid onto two a\'s', function() {
elem.innerHTML = '<a href="#A">A</a> <a href="#B">B</a>';
navigator.mozL10n.setAttributes(elem, 'struct-a-a');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'Read <a href="#A">more</a> or <a href="#B">cancel</a>.');
});
test('button is overlaid', function() {
elem.innerHTML = '<button onclick="alert(1)">BUTTON</button>';
navigator.mozL10n.setAttributes(elem, 'struct-button');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'<button onclick="alert(1)">Submit</button>');
});
test('button and a are overlaid', function() {
elem.innerHTML =
'<button onclick="alert(1)">BUTTON</button> <a href="#A">A</a>';
navigator.mozL10n.setAttributes(elem, 'struct-button-a');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'<button onclick="alert(1)">Submit</button> or ' +
'<a href="#A">cancel</a>.');
});
test('a and button are overlaid', function() {
elem.innerHTML =
'<button onclick="alert(1)">BUTTON</button> <a href="#A">A</a>';
navigator.mozL10n.setAttributes(elem, 'struct-a-button');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'<a href="#A">Cancel</a> or ' +
'<button onclick="alert(1)">submit</button>.');
});
test('inputs are overlaid', function() {
elem.innerHTML =
'<input type="text"> <input type="submit" onclick="alert(1)">';
navigator.mozL10n.setAttributes(elem, 'struct-input-input');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'<input placeholder="Type here" type="text"> and ' +
'<input value="send" onclick="alert(1)" type="submit">.');
});
});
suite('filtering', function() {
var elem;
setup(function() {
elem = document.createElement('div');
});
test('button is not allowed', function() {
navigator.mozL10n.setAttributes(elem, 'filter-button');
navigator.mozL10n.translateFragment(elem);
assert.equal(elem.innerHTML, 'Hello, world!');
});
test('onclick on em is not allowed', function() {
navigator.mozL10n.setAttributes(elem, 'filter-em-onclick');
navigator.mozL10n.translateFragment(elem);
assert.equal(elem.innerHTML, 'Hello, <em>world</em>!');
});
test('onclick on button is not allowed', function() {
elem.innerHTML = '<button onclick="alert(1)"></button>';
navigator.mozL10n.setAttributes(elem, 'filter-button-onclick');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'Hello, <button onclick="alert(1)">world</button>!');
});
test('href on a is not allowed', function() {
elem.innerHTML = '<a href="#A">A</a>';
navigator.mozL10n.setAttributes(elem, 'filter-href');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'Read <a href="#A">more</a>.');
});
test('value on text input not allowed', function() {
elem.innerHTML = '<input type="text" value="INPUT">';
navigator.mozL10n.setAttributes(elem, 'filter-input-value');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'<input value="INPUT" type="text">');
});
test('type on input not allowed', function() {
elem.innerHTML = '<input type="text" placeholder="INPUT">';
navigator.mozL10n.setAttributes(elem, 'filter-input-type');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'<input placeholder="INPUT" type="text"> the form.');
});
test('nested button is not allowed', function() {
elem.innerHTML = '';
navigator.mozL10n.setAttributes(elem, 'filter-nested-button');
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'<em>No button.</em>');
});
});
suite('Language change', function() {
var elem;
suiteSetup(function() {
lang = 'lang2';
elem = document.createElement('div');
elem.innerHTML = '<a href="#A"></a>';
navigator.mozL10n.setAttributes(elem, 'struct-a');
});
suiteTeardown(function(done) {
lang = 'lang1';
navigator.mozL10n.ctx.addEventListener('ready', function onReady() {
navigator.mozL10n.ctx.removeEventListener('ready', onReady);
done();
});
navigator.mozL10n.ctx.requestLocales(lang);
});
test('retranslation', function(done) {
navigator.mozL10n.translateFragment(elem);
assert.equal(
elem.innerHTML,
'Read <a title="MORE" href="#A">more</a>.');
navigator.mozL10n.ctx.addEventListener('ready', function onReady() {
navigator.mozL10n.ctx.removeEventListener('ready', onReady);
navigator.mozL10n.translateFragment(elem);
// XXX test known bad behavior; the title attribute leaks from lang1
// into lang2; see https://bugzil.la/922577
assert.equal(
elem.innerHTML,
'Czytaj <a title="MORE" href="#A">więcej</a>.');
done();
});
navigator.mozL10n.ctx.requestLocales(lang);
});
});
});

View file

@ -0,0 +1,108 @@
'use strict';
require('/shared/js/l10n.js');
require('/shared/js/l10n_date.js');
suite('l10n date', function(done) {
var l10nDate;
suiteSetup(function() {
l10nDate = navigator.mozL10n.DateTimeFormat();
});
suite('LocaleFormat', function() {
test('%x format', function() {
var result = l10nDate.localeFormat(new Date(), '%x');
assert.ok(/\d+\/\d+\/\d+/.test(result));
});
test('should handle 12 PM correctly', function() {
var date = new Date();
date.setHours(12);
var result = l10nDate.localeFormat(date, '%p');
assert.strictEqual(result, 'PM');
});
});
suite('relativeParts', function() {
test('negative minutes', function() {
var result = l10nDate.relativeParts(-50 * 60);
assert.deepEqual(result, {minutes: 50});
});
test('multiple values', function() {
var result = l10nDate.relativeParts(2 * 61 * 60);
assert.deepEqual(result, {hours: 2, minutes: 2});
});
test('round low values to minutes', function() {
var result = l10nDate.relativeParts(20);
assert.deepEqual(result, {minutes: 0});
var result2 = l10nDate.relativeParts(30);
assert.deepEqual(result2, {minutes: 1});
});
});
suite('prettyDate', function() {
test('maxDiff is null', function() {
var time = Date.now() - 86400 * 20 * 1000;
var result = l10nDate.fromNow(time);
assert.ok(result.indexOf('/') !== -1);
});
test('maxDiff is specified', function() {
var time = Date.now() - 86400 * 20 * 1000;
var result = l10nDate.fromNow(time, false, (86400 * 21));
assert.ok(result.indexOf('/') === -1);
assert.equal(result, '2 weeks ago');
});
test('useCompactFormat is null', function() {
var time = Date.now() - 86400 * 5 * 1000;
var result = l10nDate.fromNow(time);
assert.equal(result, '5 days ago');
});
test('useCompactFormat is true', function() {
var time = Date.now() - 86400 * 5 * 1000;
var result = l10nDate.fromNow(time, true);
assert.equal(result, '5d ago');
});
test('now', function() {
var time = Date.now() - 29 * 1000;
var result = l10nDate.fromNow(time);
assert.equal(result, 'just now');
time = Date.now() + 29 * 1000;
result = l10nDate.fromNow(time);
assert.equal(result, 'now');
});
test('in a minute', function() {
var time = Date.now() + 35 * 1000;
var result = l10nDate.fromNow(time);
assert.equal(result, 'in a minute');
time = Date.now() + 1.8 * 60 * 1000;
result = l10nDate.fromNow(time);
assert.equal(result, 'in a minute');
});
test('should discard ms if diff is over 1 minute', function() {
var time = Date.now() + 2 * 60 * 1000 - 500;
var result = l10nDate.fromNow(time);
assert.equal(result, 'in 2 minutes');
time = Date.now() + 2 * 60 * 60 * 1000 - 500;
result = l10nDate.fromNow(time);
assert.equal(result, 'in 2 hours');
});
});
});

View file

@ -0,0 +1,315 @@
suite('L10n', function() {
'use strict';
var _;
var _translateFragment;
var l10props = [
'cropimage = Crop',
'delete-n-items = {[ plural(n) ]}',
'delete-n-items[zero] = Nothing selected',
'delete-n-items[one] = Delete selected item?',
'delete-n-items[other] = Delete {{ n }} items?',
'textcontent-test = this is text content',
'dom-overlay-test = this is text content <button>(bar)</button>',
'attr-test.title = this is an attribute',
'euroSign = price: 10\\u20ac to 20\\u20ac',
'leadingSpaces = \\u0020\\u020\\u20%2F',
'trailingBackslash = backslash\\\\',
'multiLine = foo \\',
' bar \\',
' baz',
'update = {[ plural(n) ]}',
'update[zero] = <strong>No updates.</strong>',
'update[one] = <strong>{{n}} update available.</strong> \\',
' <span>Tap for more info.</span>',
'update[other] = <strong>{{n}} updates available.</strong> \\',
' <span>Tap for more info.</span>',
'overlay = {[ plural(n) ]}',
'overlay[zero] = <strong>No updates.</strong>',
'overlay[one] = <strong>{{n}} update available.</strong> \\',
' <span>Tap for more info.</span>',
'overlay[other] = <strong>{{n}} updates available.</strong> \\',
' <span>Tap for more info.</span>',
'inline-translation-test = static content provided by inlined JSON',
'a11y-label.ariaLabel = label via ARIA',
'a11y-label.ariaValueText = valuetext via ARIA',
'a11y-label.ariaMozHint = moz-hint via ARIA'
].join('\n');
var inlineL10Props = {
'inline-translation-test': 'static content provided by inlined JSON'
};
var key_cropImage = 'cropimage';
var key_delete = 'delete-n-items';
var key_euroSign = 'euroSign';
var key_leadingSpaces = 'leadingSpaces';
var key_multiLine = 'multiLine';
var key_backslash = 'trailingBackslash';
var xhr;
// Do not begin tests until the test locale has been loaded.
suiteSetup(function(done) {
xhr = sinon.useFakeXMLHttpRequest();
xhr.onCreate = function(request) {
setTimeout(function() {
request.respond(200, {}, l10props);
}, 0);
};
_ = navigator.mozL10n.get;
_translateFragment = navigator.mozL10n.translateFragment;
// en-US has already been loaded in setup.js and l10n.js is smart enough
// not to re-fetch resources; hence, set the lang to something new
var lang = 'fr';
var inline = document.createElement('script');
inline.setAttribute('type', 'application/l10n');
inline.setAttribute('lang', lang);
inline.textContent = JSON.stringify(inlineL10Props);
document.head.appendChild(inline);
navigator.mozL10n.ctx.registerLocales('en-US',
['fr', 'zh-TW', 'ar']);
navigator.mozL10n.language.code = lang;
navigator.mozL10n.once(done);
});
suiteTeardown(function() {
xhr.restore();
});
suite('get', function() {
test('existing key', function() {
assert.strictEqual(_(key_cropImage), 'Crop');
});
test('inexisting key', function() {
assert.strictEqual(_('bla'), '');
});
suite('plural', function() {
test('n=0', function() {
assert.strictEqual(_(key_delete, { n: 0 }), 'Nothing selected');
});
test('n=1', function() {
assert.strictEqual(_(key_delete, { n: 1 }), 'Delete selected item?');
});
test('n=2', function() {
assert.strictEqual(_(key_delete, { n: 2 }), 'Delete 2 items?');
});
});
test('unescape utf8 char codes', function() {
assert.strictEqual(_(key_euroSign), 'price: 10€ to 20€');
assert.strictEqual(_(key_leadingSpaces), ' %2F');
});
test('multiline string', function() {
assert.strictEqual(_(key_multiLine), 'foo bar baz');
});
test('escaped trailing backslash', function() {
assert.strictEqual(_(key_backslash), 'backslash\\');
});
});
suite('translateFragment', function() {
var elem;
setup(function() {
elem = document.createElement('div');
});
test('text content', function() {
elem.dataset.l10nId = 'textcontent-test';
_translateFragment(elem);
assert.equal(elem.textContent, 'this is text content');
});
test('properties', function() {
elem.dataset.l10nId = 'attr-test';
_translateFragment(elem);
assert.equal(elem.getAttribute('title'), 'this is an attribute');
});
test('ARIA labels', function() {
elem.dataset.l10nId = 'a11y-label';
_translateFragment(elem);
assert.equal(elem.getAttribute('aria-label'), 'label via ARIA');
});
test('ARIA valuetext', function() {
elem.dataset.l10nId = 'a11y-label';
_translateFragment(elem);
assert.equal(elem.getAttribute('aria-valuetext'), 'valuetext via ARIA');
});
test('ARIA moz-hint', function() {
elem.dataset.l10nId = 'a11y-label';
_translateFragment(elem);
assert.equal(elem.getAttribute('aria-moz-hint'), 'moz-hint via ARIA');
});
test('for a document fragment', function() {
var fragment = document.createDocumentFragment();
elem.dataset.l10nId = 'textcontent-test';
fragment.appendChild(elem);
_translateFragment(fragment);
assert.equal(elem.textContent, 'this is text content');
});
test('for a shadowRoot', function() {
var container = document.createElement('div');
container.createShadowRoot();
elem.dataset.l10nId = 'textcontent-test';
container.shadowRoot.appendChild(elem);
_translateFragment(container.shadowRoot);
assert.equal(elem.textContent, 'this is text content');
});
});
suite('localize + translate', function() {
var elem;
setup(function() {
elem = document.createElement('div');
});
test('text content', function() {
navigator.mozL10n.setAttributes(elem, 'textcontent-test');
_translateFragment(elem);
assert.equal(elem.textContent, 'this is text content');
});
test('properties', function() {
navigator.mozL10n.setAttributes(elem, 'attr-test');
_translateFragment(elem);
assert.equal(elem.getAttribute('title'), 'this is an attribute');
});
// XXX Remove in https://bugzil.la/1027117
suite('properties + pluralization', function() {
test('n=0', function() {
navigator.mozL10n.setAttributes(elem, 'update', { n: 0 });
_translateFragment(elem);
var info = elem.querySelector('strong');
var span = elem.querySelector('span');
assert.ok(info);
assert.isNull(span);
assert.equal(info.textContent, 'No updates.');
});
test('n=1', function() {
navigator.mozL10n.setAttributes(elem, 'update', { n: 1 });
_translateFragment(elem);
var info = elem.querySelector('strong');
var span = elem.querySelector('span');
assert.ok(info);
assert.ok(span);
assert.equal(info.textContent, '1 update available.');
});
test('n=2', function() {
navigator.mozL10n.setAttributes(elem, 'update', { n: 2 });
_translateFragment(elem);
var info = elem.querySelector('strong');
var span = elem.querySelector('span');
assert.ok(info);
assert.ok(span);
assert.equal(info.textContent, '2 updates available.');
});
});
suite('DOM overlays + pluralization', function() {
test('n=0', function() {
navigator.mozL10n.setAttributes(elem, 'overlay', { n: 0 });
_translateFragment(elem);
var info = elem.querySelector('strong');
var span = elem.querySelector('span');
assert.ok(info);
assert.isNull(span);
assert.equal(info.textContent, 'No updates.');
});
test('n=1', function() {
navigator.mozL10n.setAttributes(elem, 'overlay', { n: 1 });
_translateFragment(elem);
var info = elem.querySelector('strong');
var span = elem.querySelector('span');
assert.ok(info);
assert.ok(span);
assert.equal(info.textContent, '1 update available.');
});
test('n=2', function() {
navigator.mozL10n.setAttributes(elem, 'overlay', { n: 2 });
_translateFragment(elem);
var info = elem.querySelector('strong');
var span = elem.querySelector('span');
assert.ok(info);
assert.ok(span);
assert.equal(info.textContent, '2 updates available.');
});
});
test('element with child, translation without a child', function() {
elem.innerHTML = 'here is a button <button>(foo)</button>';
navigator.mozL10n.setAttributes(elem, 'textcontent-test');
_translateFragment(elem);
assert.equal(elem.innerHTML, 'this is text content');
});
test('element and translation with child', function() {
elem.innerHTML = 'here is a button <button>(foo)</button>';
navigator.mozL10n.setAttributes(elem, 'dom-overlay-test');
_translateFragment(elem);
assert.equal(elem.innerHTML,
'this is text content <button>(bar)</button>');
});
});
suite('translate existing', function() {
function setLang(lang, callback) {
// we want onLocalized to be invoked only *after* the language change
window.addEventListener('localized', function onLocalized() {
window.removeEventListener('localized', onLocalized);
callback();
});
navigator.mozL10n.language.code = lang;
}
var elem;
setup(function() {
elem = document.createElement('div');
document.body.appendChild(elem);
});
teardown(function() {
document.body.removeChild(elem);
});
test('inline translation', function(done) {
elem.dataset.l10nId = 'inline-translation-test';
assert.equal(elem.textContent, '');
setLang('zh-TW', function() {
assert.equal(elem.textContent,
'static content provided by inlined JSON');
done();
});
});
test('downloaded translation', function(done) {
elem.dataset.l10nId = 'cropimage';
assert.equal(elem.textContent, '');
setLang('ar', function() {
assert.equal(elem.textContent, 'Crop');
done();
});
});
});
});

View file

@ -0,0 +1,71 @@
/* global assert:true, it, describe, beforeEach */
/* global navigator, __dirname */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
var L20n = {
getContext: function() {
return new L10n.Context();
}
};
} else {
var assert = require('assert');
var L20n = require('../../../src/bindings/node');
}
if (typeof navigator !== 'undefined') {
var path =
'app://sharedtest.gaiamobile.org/test/unit/l10n/lib/context';
} else {
var path = __dirname;
}
describe('A simple context with one resource', function() {
var ctx;
beforeEach(function(done) {
ctx = L20n.getContext();
ctx.resLinks.push(path + '/fixtures/basic.properties');
ctx.ready(done);
ctx.registerLocales('en-US');
ctx.requestLocales('en-US');
});
it('should return the string value of brandName', function() {
var value = ctx.get('brandName');
assert.strictEqual(value, 'Firefox');
});
it('should return the value of about with the value' +
' of brandName in it', function() {
var value = ctx.get('about');
assert.strictEqual(value, 'About Firefox');
});
it('should return the value of cert with the value of ' +
'organization passed directly', function() {
var value = ctx.get('cert', {organization: 'Mozilla Foundation'});
assert.strictEqual(value, 'Certificate signed by Mozilla Foundation');
});
it('should return the correct plural form for 0', function() {
var value = ctx.get('unreadMessages', {unread: 0});
assert.strictEqual(value, '0 unread');
});
it('should return the correct plural form for 1', function() {
var value = ctx.get('unreadMessages', {unread: 1});
assert.strictEqual(value, 'One unread');
});
it('should return the correct plural form for 2', function() {
var value = ctx.get('unreadMessages', {unread: 2});
assert.strictEqual(value, '2 unread');
});
it('should return the correct plural form for 3', function() {
var value = ctx.get('unreadMessages', {unread: 3});
assert.strictEqual(value, '3 unread');
});
});

View file

@ -0,0 +1,110 @@
/* global assert:true, it, describe, beforeEach */
/* global navigator, __dirname */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
var Context = L10n.Context;
var path =
'app://sharedtest.gaiamobile.org/test/unit/l10n/lib/context';
} else {
var assert = require('assert');
var Context = require('../../../src/lib/context').Context;
var path = __dirname;
}
describe('One fallback locale', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.resLinks.push(path + '/fixtures/{locale}.properties');
ctx.once(done);
ctx.registerLocales('en-US', ['pl']);
ctx.requestLocales('pl');
});
describe('Translation in the first locale exists and is OK', function() {
it('[e]', function() {
assert.strictEqual(ctx.get('e'), 'E pl');
});
});
describe('ValueError in first locale', function() {
describe('Entity exists in second locale:', function() {
it('[ve]', function() {
assert.strictEqual(ctx.get('ve'), 'VE {{ boo }} pl');
});
});
describe('ValueError in second locale:', function() {
it('[vv]', function() {
assert.strictEqual(ctx.get('vv'), 'VV {{ boo }} pl');
});
});
describe('IndexError in second locale:', function() {
it('[vi]', function() {
assert.strictEqual(ctx.get('vi'), 'VI {{ boo }} pl');
});
});
describe('Entity missing in second locale:', function() {
it('[vm]', function() {
assert.strictEqual(ctx.get('vm'), 'VM {{ boo }} pl');
});
});
});
describe('IndexError in first locale', function() {
describe('Entity exists in second locale', function() {
it('[ie]', function() {
assert.strictEqual(ctx.get('ie'), 'ie');
});
});
describe('ValueError in second locale', function() {
it('[iv]', function() {
assert.strictEqual(ctx.get('iv'), 'iv');
});
});
describe('IndexError in second locale', function() {
it('[ii]', function() {
assert.strictEqual(ctx.get('ii'), 'ii');
});
});
describe('Entity missing in second locale:', function() {
it('[im]', function() {
assert.strictEqual(ctx.get('im'), 'im');
});
});
});
describe('Entity not found in first locale', function() {
describe('Entity exists in second locale:', function() {
it('[me]', function() {
assert.strictEqual(ctx.get('me'), 'ME en-US');
});
});
describe('ValueError in second locale:', function() {
it('[mv]', function() {
assert.strictEqual(ctx.get('mv'), 'MV {{ boo }} en-US');
});
});
describe('IndexError in second locale:', function() {
it('[mi]', function() {
assert.strictEqual(ctx.get('mi'), 'mi');
});
});
describe('Entity missing in second locale:', function() {
it('[mm]', function() {
assert.strictEqual(ctx.get('mm'), '');
});
});
});
});

View file

@ -0,0 +1,6 @@
brandName=Firefox
about=About {{ brandName }}
cert=Certificate signed by {{ organization }}
unreadMessages={[plural(unread)]}
unreadMessages[one]=One unread
unreadMessages[other]={{unread}} unread

View file

@ -0,0 +1,57 @@
foo=Foo en-US
# e - existing
# v - ValueError
# i - IndexError
# m - missing
me=ME en-US
mv=MV {{ boo }} en-US
mi={[plural(boo)]}
mi[key]=MI en-US
vve=VVE en-US
vvv=VVV {{ boo }} en-US
<vvi[boo] { key: "VVI en-US" }>
vie=VIE en-US
viv=VIV {{ boo }} en-US
vii={[plural(boo)]}
vii[key]=VII en-US
vme=VME en-US
vmv=VMV {{ boo }} en-US
vmi={[plural(boo)]}
vmi[key]=VMI en-US
ive=IVE en-US
ivv=IVV {{ boo }} en-US
ivi={[plural(boo)]}
ivi[key]=IVI en-US
iie=IIE en-US
iiv=IIV {{ boo }} en-US
iii={[plural(boo)]}
iii[key]=III en-US
ime=IME en-US
imv=IMV {{ boo }} en-US
imi={[plural(boo)]}
imi[key]=IMI en-US
mve=MVE en-US
mvv=MVV {{ boo }} en-US
mvi={[plural(boo)]}
mvi[key]=MVI en-US
mie=MIE en-US
miv=MIV {{ boo }} en-US
mii={[plural(boo)]}
mii[key]=MII en-US
mme=MME en-US
mmv=MMV {{ boo }} en-US
mmi={[plural(boo)]}
mmi[key]=MMI en-US

View file

@ -0,0 +1,69 @@
foo=Foo pl
# e - existing
# v - ValueError
# i - IndexError
# m - missing
e=E pl
v=V {{ boo }} pl
i={[plural(boo)]}
i[key]=i pl
ve=VE {{ boo }} pl
vv=VV {{ boo }} pl
vi=VI {{ boo }} pl
vm=VM {{ boo }} pl
ie={[plural(boo)]}
ie[key]=IE pl
iv={[plural(boo)]}
iv[key]=IV pl
ii={[plural(boo)]}
ii[key]=II pl
im={[plural(boo)]}
im[key]=IM pl
vve=VVE {{ boo }} pl
vvv=VVV {{ boo }} pl
vvi=VVI {{ boo }} pl
vvm=VVM {{ boo }} pl
vie=VIE {{ boo }} pl
viv=VIV {{ boo }} pl
vii=VII {{ boo }} pl
vim=VIM {{ boo }} pl
vme=VME {{ boo }} pl
vmv=VMV {{ boo }} pl
vmi=VMI {{ boo }} pl
vmm=VMM {{ boo }} pl
ive={[plural(boo)]}
ive[key]=IVE pl
ivv={[plural(boo)]}
ivv[key]=IVV pl
ivi={[plural(boo)]}
ivi[key]=IVI pl
ivm={[plural(boo)]}
ivm[key]=IVM pl
iie={[plural(boo)]}
iie[key]=IIE pl
iiv={[plural(boo)]}
iiv[key]=IIV pl
iii={[plural(boo)]}
iii[key]=III pl
iim={[plural(boo)]}
iim[key]=IIM pl
ime={[plural(boo)]}
ime[key]=IME pl
imv={[plural(boo)]}
imv[key]=IMV pl
imi={[plural(boo)]}
imi[key]=IMI pl
imm={[plural(boo)]}
imm[key]=IMM pl

View file

@ -0,0 +1,116 @@
/* global assert:true, it, describe, beforeEach */
/* global navigator, __dirname */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
var Context = L10n.Context;
var path =
'app://sharedtest.gaiamobile.org/test/unit/l10n/lib/context';
} else {
var assert = require('assert');
var Context = require('../../../src/lib/context').Context;
var path = __dirname;
}
function assertPromise(promise, expected, done) {
promise.then(function(value) {
assert.strictEqual(value, expected);
}).then(done, done);
}
describe('One fallback locale', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.resLinks.push(path + '/fixtures/{locale}.properties');
ctx.once(done);
ctx.registerLocales('en-US', ['pl']);
ctx.requestLocales('pl');
});
describe('Translation in the first locale exists and is OK', function() {
it('[e]', function(done) {
assertPromise(ctx.formatValue('e'), 'E pl', done);
});
});
describe('ValueError in first locale', function() {
describe('Entity exists in second locale:', function() {
it('[ve]', function(done) {
assertPromise(ctx.formatValue('ve'), 'VE {{ boo }} pl', done);
});
});
describe('ValueError in second locale:', function() {
it('[vv]', function(done) {
assertPromise(ctx.formatValue('vv'), 'VV {{ boo }} pl', done);
});
});
describe('IndexError in second locale:', function() {
it('[vi]', function(done) {
assertPromise(ctx.formatValue('vi'), 'VI {{ boo }} pl', done);
});
});
describe('Entity missing in second locale:', function() {
it('[vm]', function(done) {
assertPromise(ctx.formatValue('vm'), 'VM {{ boo }} pl', done);
});
});
});
describe('IndexError in first locale', function() {
describe('Entity exists in second locale', function() {
it('[ie]', function(done) {
assertPromise(ctx.formatValue('ie'), 'ie', done);
});
});
describe('ValueError in second locale', function() {
it('[iv]', function(done) {
assertPromise(ctx.formatValue('iv'), 'iv', done);
});
});
describe('IndexError in second locale', function() {
it('[ii]', function(done) {
assertPromise(ctx.formatValue('ii'), 'ii', done);
});
});
describe('Entity missing in second locale:', function() {
it('[im]', function(done) {
assertPromise(ctx.formatValue('im'), 'im', done);
});
});
});
describe('Entity not found in first locale', function() {
describe('Entity exists in second locale:', function() {
it('[me]', function(done) {
assertPromise(ctx.formatValue('me'), 'ME en-US', done);
});
});
describe('ValueError in second locale:', function() {
it('[mv]', function(done) {
assertPromise(ctx.formatValue('mv'), 'MV {{ boo }} en-US', done);
});
});
describe('IndexError in second locale:', function() {
it('[mi]', function(done) {
assertPromise(ctx.formatValue('mi'), 'mi', done);
});
});
describe('Entity missing in second locale:', function() {
it('[mm]', function(done) {
assertPromise(ctx.formatValue('mm'), 'mm', done);
});
});
});
});

View file

@ -0,0 +1,59 @@
/* global assert:true, it, describe, beforeEach */
/* global navigator */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
var Context = L10n.Context;
} else {
var assert = require('assert');
var Context = require('../../../src/lib/context').Context;
}
describe('Language negotiation without arguments', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.once(done);
ctx.registerLocales('en-US');
ctx.requestLocales('en-US');
});
it('used the en-US locale', function() {
assert.strictEqual(ctx.supportedLocales.length, 1);
assert.strictEqual(ctx.supportedLocales[0], 'en-US');
});
});
describe('Language negotiation with arguments', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.once(done);
ctx.registerLocales('en-US', ['pl']);
ctx.requestLocales('pl');
});
it('sets the correct fallback chain', function() {
assert.strictEqual(ctx.supportedLocales.length, 2);
assert.strictEqual(ctx.supportedLocales[0], 'pl');
assert.strictEqual(ctx.supportedLocales[1], 'en-US');
});
});
describe('Language negotiation with arguments and unsupported', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.once(done);
ctx.registerLocales('en-US', ['pl']);
ctx.requestLocales('fr', 'pl');
});
it('sets the correct fallback chain', function() {
assert.strictEqual(ctx.supportedLocales.length, 2);
assert.strictEqual(ctx.supportedLocales[0], 'pl');
assert.strictEqual(ctx.supportedLocales[1], 'en-US');
});
});

View file

@ -0,0 +1,145 @@
/* global assert:true, it, describe, beforeEach */
/* global navigator, __dirname */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
var Context = L10n.Context;
var path =
'app://sharedtest.gaiamobile.org/test/unit/l10n/lib/context';
} else {
var assert = require('assert');
var Context = require('../../../src/lib/context').Context;
var path = __dirname;
}
describe('A non-loading context', function() {
var ctx;
beforeEach(function() {
ctx = new Context();
ctx.resLinks.push(path + '/fixtures/{locale}.properties');
});
it('should throw on get', function() {
assert.throws(function(){
ctx.get('dummy');
}, /Context not ready/);
});
it('should throw on getEntity', function() {
assert.throws(function(){
ctx.getEntity('dummy');
}, /Context not ready/);
});
});
describe('A loading, non-ready context', function() {
var ctx;
beforeEach(function() {
ctx = new Context();
ctx.resLinks.push(path + '/fixtures/{locale}.properties');
ctx.registerLocales('en-US');
ctx.requestLocales('en-US');
});
it('should throw on requestLocales', function() {
assert.throws(function(){
ctx.requestLocales('en-US');
}, /Context not ready/);
});
it('should throw on get', function() {
assert.throws(function(){
ctx.get('dummy');
}, /Context not ready/);
});
it('should throw on getEntity', function() {
assert.throws(function(){
ctx.getEntity('dummy');
}, /Context not ready/);
});
it('should throw on requestLocales', function() {
assert.throws(function(){
ctx.requestLocales('en-US');
}, /Context not ready/);
});
});
describe('A loading, ready context', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.resLinks.push(path + '/fixtures/{locale}.properties');
ctx.once(done);
ctx.registerLocales('en-US');
ctx.requestLocales('en-US');
});
it('should not throw on get of a known entity', function() {
assert.doesNotThrow(function(){
ctx.get('foo');
});
assert.strictEqual(ctx.get('foo'), 'Foo en-US');
});
it('should not throw on get of an unknown entity', function() {
assert.doesNotThrow(function(){
ctx.get('missing');
});
assert.strictEqual(ctx.get('missing'), '');
});
it('should not throw on getEntity of a known entity', function() {
assert.doesNotThrow(function(){
ctx.getEntity('foo');
});
assert.strictEqual(ctx.getEntity('foo').value, 'Foo en-US');
});
it('should not throw on getEntity of an unknown entity', function() {
assert.doesNotThrow(function(){
ctx.getEntity('missing');
});
assert.strictEqual(ctx.getEntity('missing'), '');
});
it('should not throw on requestLocales', function(done) {
ctx.once(function() {
assert.strictEqual(ctx.isReady, true);
done();
});
assert.doesNotThrow(function(){
ctx.requestLocales('pl', 'en-US');
});
});
});
describe('A loading, ready context', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.resLinks.push(path + '/fixtures/{locale}.properties');
ctx.once(done);
ctx.registerLocales('en-US');
ctx.requestLocales('en-US');
});
it('should return translations for the built fallback chain ', function() {
// Bug 942183 - Error when localizeNode is done quickly after
// requestLocales https://bugzil.la/942183
// Changing locales triggers the build process for the 'pl' locale.
// However, synchronous methods called right after the change should still
// return translations for the previous fallback chain
ctx.requestLocales('pl');
assert.strictEqual(ctx.get('foo'), 'Foo en-US');
});
});

View file

@ -0,0 +1,49 @@
/* global assert:true, it, describe, beforeEach */
/* global navigator, __dirname */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
var Context = L10n.Context;
var path = 'app://sharedtest.gaiamobile.org/test/unit/l10n/context';
} else {
var assert = require('assert');
var Context = require('../../../src/lib/context').Context;
var path = __dirname;
}
describe('Missing resources', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.resLinks.push(path + '/fixtures/en-US.properties');
ctx.resLinks.push(path + '/fixtures/missing.properties');
ctx.once(done);
ctx.registerLocales('en-US');
ctx.requestLocales('en-US');
});
it('should get ready', function() {
assert.strictEqual(ctx.isReady, true);
});
});
describe('No valid resources', function() {
var ctx;
beforeEach(function(done) {
ctx = new Context();
ctx.resLinks.push(path + '/fixtures/missing.properties');
ctx.resLinks.push(path + '/fixtures/another.properties');
ctx.once(done);
ctx.registerLocales('en-US');
ctx.requestLocales('en-US');
});
it('should get ready', function() {
assert.strictEqual(ctx.isReady, true);
});
});

View file

@ -0,0 +1,66 @@
/* global it, describe, beforeEach */
/* global navigator */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
var Context = L10n.Context;
} else {
var Context = require('../../../src/lib/context').Context;
}
describe('ctx.ready', function() {
var ctx;
beforeEach(function() {
ctx = new Context();
ctx.registerLocales('en-US', ['pl']);
});
it('should fire asynchronously when context is ready', function(done) {
ctx.ready(function() {
done();
});
ctx.requestLocales('pl');
});
it('should fire asynchronously when language changes', function(done) {
var now = false;
ctx.ready(function() {
if (now) {
done();
}
});
ctx.once(function() {
now = true;
ctx.requestLocales('pl');
});
ctx.requestLocales('en-US');
});
it('should fire synchronously when context is ready', function(done) {
ctx.once(function() {
ctx.ready(function() {
done();
});
});
ctx.requestLocales('en-US');
});
it('should fire synchronously when language changes', function(done) {
var now = false;
ctx.once(function() {
ctx.ready(function() {
if (now) {
done();
}
});
setTimeout(function() {
now = true;
ctx.requestLocales('pl');
});
});
ctx.requestLocales('en-US');
});
});

View file

@ -0,0 +1,91 @@
/* global assert:true, it, beforeEach, describe */
/* global navigator */
/* jshint -W101 */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
} else {
var assert = require('assert');
var L10n = {
PropertiesParser: require('../../../../src/lib/format/properties/parser')
};
}
describe('Quadratic Blowup', function(){
var source;
beforeEach(function() {
source = [
'# Project Gutenberg\'s Alice\'s Adventures in Wonderland,',
'# by Lewis Carroll',
'#',
'# This eBook is for the use of anyone anywhere at no cost and with',
'# almost no restrictions whatsoever. You may copy it, give it away',
'# or re-use it under the terms of the Project Gutenberg License',
'# included with this eBook or online at www.gutenberg.org',
'alice=\\',
' CHAPTER I. Down the Rabbit-Hole\\',
' Alice was beginning to get very tired of sitting by her sister on\\',
' the bank, and of having nothing to do: once or twice she had peeped\\',
' into the book her sister was reading, but it had no pictures or\\',
' conversations in it, \'and what is the use of a book,\' thought\\',
' Alice \'without pictures or conversation?\'\\',
' So she was considering in her own mind (as well as she could, for\\',
' the hot day made her feel very sleepy and stupid), whether the\\',
' pleasure of making a daisy-chain would be worth the trouble of\\',
' getting up and picking the daisies, when suddenly a White Rabbit\\',
' with pink eyes ran close by her.\\',
' There was nothing so VERY remarkable in that; nor did Alice think\\',
' it so VERY much out of the way to hear the Rabbit say to itself,\\',
' \'Oh dear! Oh dear! I shall be late!\' (when she thought it over\\',
' afterwards, it occurred to her that she ought to have wondered at\\',
' this, but at the time it all seemed quite natural); but when the\\',
' Rabbit actually TOOK A WATCH OUT OF ITS WAISTCOAT-POCKET, and\\',
' looked at it, and then hurried on, Alice started to her feet, for\\',
' it flashed across her mind that she had never before seen a rabbit\\',
' with either a waistcoat-pocket, or a watch to take out of it, and\\',
' burning with curiosity, she ran across the field after it, and\\',
' fortunately was just in time to see it pop down a large rabbit-hole\\',
' under the hedge.\\',
' In another moment down went Alice after it, never once considering\\',
' how in the world she was to get out again.\\',
' The rabbit-hole went straight on like a tunnel for some way, and\\',
' then dipped suddenly down, so suddenly that Alice had not a moment\\',
' to think about stopping herself before she found herself falling\\',
' down a very deep well.',
'malice=\\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} \\',
' {{alice}} {{alice}} {{alice}} {{alice}} {{alice}} {{alice}}'
].join('\n');
});
it('throws', function() {
assert.throws(function() {
L10n.PropertiesParser.parse(null, source);
}, /too many placeables/i);
});
});

View file

@ -0,0 +1,134 @@
/* global it, assert:true, describe */
/* global navigator */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
} else {
var assert = require('assert');
var L10n = {
PropertiesParser: require('../../../../src/lib/format/properties/parser')
};
}
describe('L10n Parser', function() {
it('string value', function() {
var ast = L10n.PropertiesParser.parse(null, 'id = string');
assert.strictEqual(ast[0].$v, 'string');
});
it('empty value', function() {
var ast = L10n.PropertiesParser.parse(null, 'id =');
assert.equal(ast[0].$v, '');
});
it('empty value with white spaces', function() {
var ast = L10n.PropertiesParser.parse(null, 'id = ');
assert.equal(ast[0].$v, '');
});
it('basic errors', function() {
var strings = [
'',
'id',
'id ',
'+id',
'=id',
];
for (var i in strings) {
if (strings.hasOwnProperty(i)) {
var ast = L10n.PropertiesParser.parse(null, strings[i]);
assert.equal(Object.keys(ast).length, 0);
}
}
});
it('basic attributes', function() {
var ast = L10n.PropertiesParser.parse(null, 'id.attr1 = foo');
assert.equal(ast[0].attr1, 'foo');
});
it('empty attribute', function() {
var ast = L10n.PropertiesParser.parse(null, 'id.attr1 =');
assert.equal(ast[0].attr1, '');
});
it('empty attribute with white spaces', function() {
var ast = L10n.PropertiesParser.parse(null, 'id.attr1 = ');
assert.equal(ast[0].attr1, '');
});
it('attribute errors', function() {
var strings = [
['key.foo.bar = foo', /Nested attributes are not supported./],
['key.$attr = foo', /Attribute can't start/],
];
for (var i in strings) {
if (strings.hasOwnProperty(i)) {
/* jshint -W083 */
assert.throws(function() {
L10n.PropertiesParser.parse(null, strings[i][0]);
}, strings[i][1]);
}
}
});
it('plural macro', function() {
var ast = L10n.PropertiesParser.parse(null,
'id = {[ plural(m) ]} \nid[one] = foo');
assert.ok(ast[0].$v instanceof Object);
assert.equal(ast[0].$v.one, 'foo');
assert.equal(ast[0].$x.length, 2);
assert.equal(ast[0].$x[0].t, 'idOrVar');
assert.equal(ast[0].$x[0].v, 'plural');
assert.equal(ast[0].$x[1], 'm');
});
it('plural macro errors', function() {
var strings = [
'id = {[ plural(m) ] \nid[one] = foo',
'id = {[ plural(m) \nid[one] = foo',
'id = { plural(m) ]} \nid[one] = foo',
'id = plural(m) ]} \nid[one] = foo',
'id = {[ plural(m ]} \nid[one] = foo',
'id = {[ pluralm) ]} \nid[one] = foo',
];
var errorsThrown = 0;
for (var i in strings) {
if (strings.hasOwnProperty(i)) {
try {
L10n.PropertiesParser.parse(null, strings[i]);
} catch (e) {
errorsThrown += 1;
}
}
}
assert.equal(errorsThrown, strings.length);
});
it('comment', function() {
var ast = L10n.PropertiesParser.parse(null, '#test');
assert.equal(Object.keys(ast).length, 0);
});
it('comment errors', function() {
var strings = [
' # foo',
' ## foo',
'f# foo',
];
for (var i in strings) {
if (strings.hasOwnProperty(i)) {
var ast = L10n.PropertiesParser.parse(null, strings[i]);
assert.equal(Object.keys(ast).length, 0);
}
}
});
});

View file

@ -0,0 +1,198 @@
/* global it, assert:true, describe, before, beforeEach */
/* global navigator */
'use strict';
var assert;
var PropertiesParser;
describe('pseudo strategy', function() {
var PSEUDO, walkContent, strategy, source, ast, walked;
before(function(done) {
if (typeof navigator !== 'undefined') {
require('/shared/js/l10n.js', function() {
PSEUDO = navigator.mozL10n.qps;
var L10n = navigator.mozL10n._getInternalAPI();
PropertiesParser = L10n.PropertiesParser;
walkContent = L10n.walkContent;
done();
});
} else {
assert = require('assert');
PSEUDO = require('../../src/lib/pseudo').PSEUDO;
walkContent = require('../../src/lib/util').walkContent;
PropertiesParser =
require('../../src/lib/format/properties/parser');
done();
}
});
beforeEach(function() {
ast = PropertiesParser.parse(null, source);
walked = pseudolocalize(ast, strategy);
});
function pseudolocalize(arr, strategy) {
var obj = {};
arr.forEach(function(val) {
obj[val.$i] = walkContent(val, strategy);
});
return obj;
}
var foo = {t: 'idOrVar', v: 'foo'};
var bar = {t: 'idOrVar', v: 'bar'};
describe('accented English', function(){
before(function() {
strategy = PSEUDO['fr-x-psaccent'].translate;
source = [
'foo=Foo',
'bar={[ plural(n) ]}',
'bar[one]=One',
'bar[two]=Two',
'bar[few]=Few',
'bar[many]=Many',
'bar[other]=Other',
'baz.attr=An attribute',
'baz.attrComplex=An attribute referencing {{ foo }}',
'templateVar={name} wrote',
'dateFormat=%A, %b %Eb',
'twoPlaceables1={{ foo }} {{ bar }}',
'twoPlaceables2=Foo {{ foo }} and bar {{ bar }}',
'parens1=({{ foo }}) {{ bar }}',
'parens2=Foo ({{foo}}) [and/or {{bar}}]',
'parens3=Foo (and) bar',
'unicode=Foo \u0066\u006f\u006f\u0020',
'nonascii=Naïve coöperation résumé dæmon phœnix',
'html1=visit <a>url</a>',
'html2=type <input placeholder="your name"/>',
].join('\n');
});
it('walks the value', function(){
assert.strictEqual(walked.foo.$v, 'Ƒǿǿǿǿ');
assert.strictEqual(walked.bar.$v.one, 'Ǿǿƞḗḗ');
assert.strictEqual(walked.bar.$v.two, 'Ŧẇǿǿ');
assert.strictEqual(walked.bar.$v.few, 'Ƒḗḗẇ');
assert.strictEqual(walked.bar.$v.many, 'Ḿȧȧƞẏ');
assert.strictEqual(walked.bar.$v.other, 'Ǿǿŧħḗḗř');
assert.strictEqual(walked.baz.attr, 'Ȧȧƞ ȧȧŧŧřīīƀŭŭŧḗḗ');
assert.deepEqual(
walked.baz.attrComplex, ['Ȧȧƞ ȧȧŧŧřīīƀŭŭŧḗḗ řḗḗƒḗḗřḗḗƞƈīīƞɠ ', foo]);
assert.strictEqual(walked.templateVar.$v, '{name} ẇřǿǿŧḗḗ');
assert.strictEqual(walked.dateFormat.$v, '%A, %b %Eb');
assert.deepEqual(walked.twoPlaceables1.$v, [foo, ' ', bar]);
assert.deepEqual(
walked.twoPlaceables2.$v, ['Ƒǿǿǿǿ ', foo, ' ȧȧƞḓ ƀȧȧř ', bar]);
assert.deepEqual(
walked.parens1.$v, ['(', foo, ') ', bar]);
assert.deepEqual(
walked.parens2.$v, ['Ƒǿǿǿǿ (', foo, ') [ȧȧƞḓ/ǿǿř ', bar, ']']);
assert.strictEqual(walked.parens3.$v, 'Ƒǿǿǿǿ (ȧȧƞḓ) ƀȧȧř');
assert.strictEqual(walked.unicode.$v, 'Ƒǿǿǿǿ ƒǿǿǿǿ ');
assert.strictEqual(
walked.nonascii.$v,
'Ƞȧȧïṽḗḗ ƈǿǿöƥḗḗřȧȧŧīīǿǿƞ řéşŭŭḿé ḓæḿǿǿƞ ƥħœƞīīẋ');
assert.strictEqual(walked.html1.$v, 'ṽīīşīīŧ <a>ŭŭřŀ</a>');
assert.strictEqual(
walked.html2.$v, 'ŧẏƥḗḗ <input placeholder="your name"/>');
});
});
describe('bidi English', function(){
/* jshint -W100 */
before(function() {
strategy = PSEUDO['ar-x-psbidi'].translate;
source = [
'foo=Foo',
'bar={[ plural(n) ]}',
'bar[one]=One',
'bar[two]=Two',
'bar[few]=Few',
'bar[many]=Many',
'bar[other]=Other',
'baz.attr=An attribute',
'baz.attrComplex=An attribute referencing {{ foo }}',
'templateVar={name} wrote',
'dateFormat=%A, %b %Eb',
'twoPlaceables1={{ foo }} {{ bar }}',
'twoPlaceables2=Foo {{ foo }} and bar {{ bar }}',
'parens1=({{ foo }}) {{ bar }}',
'parens2=Foo ({{foo}}) [and/or {{bar}}]',
'parens3=Foo (and) bar',
'unicode=Foo \u0066\u006f\u006f\u0020',
'nonascii=Naïve coöperation résumé dæmon phœnix',
'html1=visit <a>url</a>',
'html2=type <input placeholder="your name"/>',
].join('\n');
});
it('walks the value', function(){
assert.strictEqual(walked.foo.$v, '‮ɟoo‬');
assert.strictEqual(walked.bar.$v.one, '‮Ouǝ‬');
assert.strictEqual(walked.bar.$v.two, '‮⊥ʍo‬');
assert.strictEqual(walked.bar.$v.few, '‮ɟǝʍ‬');
assert.strictEqual(walked.bar.$v.many, '‮Wɐuʎ‬');
assert.strictEqual(walked.bar.$v.other, '‮Oʇɥǝɹ‬');
assert.strictEqual(
walked.baz.attr, '‮∀u‬ ‮ɐʇʇɹıqnʇǝ‬');
assert.deepEqual(
walked.baz.attrComplex,
['‮∀u‬ ‮ɐʇʇɹıqnʇǝ‬ ‮ɹǝɟǝɹǝuɔıuƃ‬ ',
foo]);
assert.strictEqual(walked.templateVar.$v, '{name} ‮ʍɹoʇǝ‬');
assert.strictEqual(walked.dateFormat.$v, '%A, %b %Eb');
assert.deepEqual(walked.twoPlaceables1.$v, [foo, ' ', bar]);
assert.deepEqual(
walked.twoPlaceables2.$v,
['‮ɟoo‬ ', foo, ' ‮ɐup‬ ‮qɐɹ‬ ',
bar]);
assert.deepEqual(
walked.parens1.$v, ['(', foo, ') ', bar]);
assert.deepEqual(
walked.parens2.$v,
['‮ɟoo‬ (', foo, ') [‮ɐup‬/‮oɹ‬ ',
bar, ']']);
assert.strictEqual(
walked.parens3.$v,
'‮ɟoo‬ (‮ɐup‬) ‮qɐɹ‬');
assert.strictEqual(
walked.unicode.$v,
'‮ɟoo‬ ‮ɟoo‬ ');
assert.strictEqual(
walked.html1.$v, '‮ʌısıʇ‬ <a>‮nɹʅ‬</a>');
assert.strictEqual(
walked.html2.$v,
'‮ʇʎdǝ‬ <input placeholder="your name"/>');
});
// XXX this requires Unicode support for JavaSript RegExp objects
// https://bugzil.la/258974
it.skip('walks the value', function(){
var walked = walkContent(ast, strategy);
assert.strictEqual(
walked.nonascii,
'‮Nɐïʌǝ‬ ‮ɔoödǝɹɐʇıou‬ ' +
'‮ɹésnɯé‬ ‮pæɯou‬ ' +
'‮dɥœuıx‬');
});
});
});

View file

@ -0,0 +1,127 @@
/* global assert:true, it, before, beforeEach, describe, requireApp */
'use strict';
if (typeof navigator !== 'undefined') {
requireApp('sharedtest/test/unit/l10n/lib/resolver/header.js');
} else {
var assert = require('assert');
var Resolver = require('./header').Resolver;
var createEntries = require('./header').createEntries;
}
describe('Attributes', function(){
var source, env;
beforeEach(function() {
env = createEntries(source);
});
describe('with string values', function(){
before(function() {
source = [
'foo=Foo',
'foo.attr=An attribute',
'foo.attrComplex=An attribute referencing {{ bar }}',
'bar=Bar'
].join('\n');
});
it('returns the value', function(){
var formatted = Resolver.format(null, env.foo.attrs.attr);
assert.strictEqual(formatted[1], 'An attribute');
});
it('returns the value with a placeable', function(){
var formatted = Resolver.format(null, env.foo.attrs.attrComplex);
assert.strictEqual(formatted[1], 'An attribute referencing Bar');
});
});
describe('with hash values', function(){
before(function() {
source = [
'update=Update',
'update.title={[ plural(n) ]}',
'update.title[one]=One update available'
].join('\n');
});
it('returns the value of the entity', function(){
var formatted = Resolver.format(null, env.update);
assert.strictEqual(formatted[1], 'Update');
});
it('returns the value of the attribute\'s member', function(){
var formatted = Resolver.format({n: 1}, env.update.attrs.title);
assert.strictEqual(formatted[1], 'One update available');
});
});
describe('with hash values, on entities with hash values ', function(){
before(function() {
source = [
'update={[ plural(n) ]}',
'update[one]=One update',
'update[other]={{ n }} updates',
'update.title={[ plural(k) ]}',
'update.title[one]=One update title',
'update.title[other]={{ k }} updates title'
].join('\n');
});
it('returns the value of the entity', function(){
var formatted = Resolver.format({n: 1, k: 2}, env.update);
assert.strictEqual(formatted[1], 'One update');
});
it('returns the value of the attribute', function(){
var formatted = Resolver.format({n: 1, k: 2}, env.update.attrs.title);
assert.strictEqual(formatted[1], '2 updates title');
});
});
describe('with relative self-references', function(){
before(function() {
source = [
'brandName=Firefox',
'brandName.title=Mozilla {{ brandName }}'
].join('\n');
});
it('returns the value of the entity', function(){
var value = Resolver.format(null, env.brandName)[1];
assert.strictEqual(value, 'Firefox');
});
it('returns the value of the attribute', function(){
var attr = Resolver.format(null, env.brandName.attrs.title)[1];
assert.strictEqual(attr, 'Mozilla Firefox');
});
});
describe('with cyclic self-references', function(){
before(function() {
source = [
'brandName=Firefox',
'brandName.title=Mozilla {{ brandName.title }}'
].join('\n');
});
it('returns the raw string of the attribute', function(){
var attr = Resolver.format(null, env.brandName.attrs.title)[1];
assert.strictEqual(attr, 'Mozilla {{ brandName.title }}');
});
});
});

View file

@ -0,0 +1,345 @@
/* global assert:true, it, before, beforeEach, describe, requireApp */
'use strict';
if (typeof navigator !== 'undefined') {
requireApp('sharedtest/test/unit/l10n/lib/resolver/header.js');
} else {
var assert = require('assert');
var Resolver = require('./header').Resolver;
var createEntries = require('./header').createEntries;
}
describe('Context data', function(){
var source, ctxdata, env;
beforeEach(function() {
env = createEntries(source);
});
describe('in entities', function(){
before(function() {
ctxdata = {
unreadNotifications: 3,
foo: 'Foo'
};
source = [
'unread=Unread notifications: {{ unreadNotifications }}',
'unreadPlural={[ plural(unreadNotifications) ]}',
'unreadPlural[one]=One unread notification',
'unreadPlural[other]={{ unreadNotifications}} unread notifications',
'foo=Bar',
'useFoo={{ foo }}'
].join('\n');
});
it('can be referenced from strings', function() {
var value = Resolver.format(ctxdata, env.unread)[1];
assert.strictEqual(value, 'Unread notifications: 3');
});
it('can be passed as argument to a macro', function() {
var value = Resolver.format(ctxdata, env.unreadPlural)[1];
assert.strictEqual(value, '3 unread notifications');
});
it('takes priority over entities of the same name', function() {
var value = Resolver.format(ctxdata, env.useFoo)[1];
assert.strictEqual(value, 'Foo');
});
});
describe('and simple errors', function(){
before(function() {
ctxdata = {
nested: {
}
};
source = [
'missingReference={{ missing }}',
'nestedReference={{ nested }}',
'watchReference={{ watch }}',
'hasOwnPropertyReference={{ hasOwnProperty }}',
'isPrototypeOfReference={{ isPrototypeOf }}',
'toStringReference={{ toString }}',
'protoReference={{ __proto__ }}',
].join('\n');
});
it('returns the raw string when a missing property of ctxdata is ' +
'referenced', function(){
var value = Resolver.format(ctxdata, env.missingReference)[1];
assert.strictEqual(value, '{{ missing }}');
});
it('returns the raw string when an object is referenced', function(){
var value = Resolver.format(ctxdata, env.nestedReference)[1];
assert.strictEqual(value, '{{ nested }}');
});
it('returns the raw string when watch is referenced', function(){
var value = Resolver.format(ctxdata, env.watchReference)[1];
assert.strictEqual(value, '{{ watch }}');
});
it('returns the raw string when hasOwnProperty is referenced', function(){
var value = Resolver.format(ctxdata, env.hasOwnPropertyReference)[1];
assert.strictEqual(value, '{{ hasOwnProperty }}');
});
it('returns the raw string when isPrototypeOf is referenced', function(){
var value = Resolver.format(ctxdata, env.isPrototypeOfReference)[1];
assert.strictEqual(value, '{{ isPrototypeOf }}');
});
it('returns the raw string when toString is referenced', function(){
var value = Resolver.format(ctxdata, env.toStringReference)[1];
assert.strictEqual(value, '{{ toString }}');
});
it('returns the raw string when __proto__ is referenced', function(){
var value = Resolver.format(ctxdata, env.protoReference)[1];
assert.strictEqual(value, '{{ __proto__ }}');
});
});
describe('and strings', function(){
before(function() {
ctxdata = {
str: 'string',
num: '1'
};
source = [
'stringProp={{ str }}',
'stringIndex={[ plural(str) ]}',
'stringIndex[one]=One',
'stringNumProp={{ num }}',
'stringNumIndex={[ plural(num) ]}',
'stringNumIndex[one]=One'
].join('\n');
});
it('returns a string value', function(){
assert.strictEqual(
Resolver.format(ctxdata, env.stringProp)[1], 'string');
});
it('throws when used in a macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.stringIndex);
}, 'Unresolvable value');
});
it('digit returns a string value', function(){
assert.strictEqual(
Resolver.format(ctxdata, env.stringNumProp)[1], '1');
});
it('digit throws when used in a macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.stringNumIndex);
}, 'Unresolvable value');
});
});
describe('and numbers', function(){
before(function() {
ctxdata = {
num: 1,
nan: NaN
};
source = [
'numProp={{ num }}',
'numIndex={[ plural(num) ]}',
'numIndex[one]=One',
'nanProp={{ nan }}',
'nanIndex={[ plural(nan) ]}',
'nanIndex[one]=One'
].join('\n');
});
it('returns a number value', function(){
assert.strictEqual(Resolver.format(ctxdata, env.numProp)[1], '1');
});
it('returns a value when used in macro', function(){
assert.strictEqual(Resolver.format(ctxdata, env.numIndex)[1], 'One');
});
it('returns the raw string when NaN is referenced', function(){
var value = Resolver.format(ctxdata, env.nanProp)[1];
assert.strictEqual(value, '{{ nan }}');
});
it('is undefined when NaN is used in macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.nanIndex);
}, 'Arg must be a string or a number: nan');
});
});
describe('and bools', function(){
before(function() {
ctxdata = {
bool: true
};
source = [
'boolProp={{ bool }}',
'boolIndex={[ plural(bool) ]}',
'boolIndex[one]=One'
].join('\n');
});
it('returns the raw string when referenced', function(){
var value = Resolver.format(ctxdata, env.boolProp)[1];
assert.strictEqual(value, '{{ bool }}');
});
it('is undefined when used in a macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.boolIndex);
}, 'Arg must be a string or a number: bool');
});
});
describe('and undefined', function(){
before(function() {
ctxdata = {
undef: undefined
};
source = [
'undefProp={{ undef }}',
'undefIndex={[ plural(undef) ]}',
'undefIndex[one]=One'
].join('\n');
});
it('returns the raw string when referenced', function(){
var value = Resolver.format(ctxdata, env.undefProp)[1];
assert.strictEqual(value, '{{ undef }}');
});
it('is undefined when used in a macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.undefIndex);
}, 'Arg must be a string or a number: undef');
});
});
describe('and null', function(){
before(function() {
ctxdata = {
nullable: null
};
source = [
'nullProp={{ nullable }}',
'nullIndex={[ plural(nullable) ]}',
'nullIndex[one]=One'
].join('\n');
});
it('returns the raw string', function(){
var value = Resolver.format(ctxdata, env.nullProp)[1];
assert.strictEqual(value, '{{ nullable }}');
});
it('is undefined when used in a macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.nullIndex);
}, 'Arg must be a string or a number: nullable');
});
});
describe('and arrays where first element is number', function(){
before(function() {
ctxdata = {
arr: [1, 2]
};
source = [
'arrProp={{ arr }}',
'arrIndex={[ plural(arr) ]}',
'arrIndex[one]=One'
].join('\n');
});
it('returns the raw string', function(){
var value = Resolver.format(ctxdata, env.arrProp)[1];
assert.strictEqual(value, '{{ arr }}');
});
it('is undefined when used in a macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.arrIndex);
}, 'Arg must be a string or a number: arr');
});
});
describe('and arrays where first element is not a number', function(){
before(function() {
ctxdata = {
arr: ['a', 'b']
};
source = [
'arrProp={{ arr }}',
'arrIndex={[ plural(arr) ]}',
'arrIndex[one]=One'
].join('\n');
});
it('returns the raw string', function(){
var value = Resolver.format(ctxdata, env.arrProp)[1];
assert.strictEqual(value, '{{ arr }}');
});
it('is undefined when used in a macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.arrIndex);
}, 'Arg must be a string or a number: arr');
});
});
describe('and objects', function(){
before(function() {
ctxdata = {
obj: {
key: 'value'
}
};
source = [
'objProp={{ obj }}',
'objIndex={[ plural(obj) ]}',
'objIndex[one]=One'
].join('\n');
});
it('returns the raw string', function(){
var value = Resolver.format(ctxdata, env.objProp)[1];
assert.strictEqual(value, '{{ obj }}');
});
it('throws used in a macro', function(){
assert.throws(function() {
Resolver.format(ctxdata, env.objIndex);
}, 'Arg must be a string or a number: obj');
});
});
});

View file

@ -0,0 +1,38 @@
/* global assert:true, it, beforeEach, describe, requireApp */
/* jshint -W101 */
'use strict';
if (typeof navigator !== 'undefined') {
requireApp('sharedtest/test/unit/l10n/lib/resolver/header.js');
} else {
var assert = require('assert');
var Resolver = require('./header.js').Resolver;
var createEntries = require('./header.js').createEntries;
}
// Bug 803931 - Compiler is vulnerable to the billion laughs attack
describe('Billion Laughs', function(){
var entries;
beforeEach(function() {
var source = [
'lol0=LOL',
'lol1={{lol0}} {{lol0}} {{lol0}} {{lol0}} {{lol0}} {{lol0}} {{lol0}} {{lol0}} {{lol0}} {{lol0}}',
'lol2={{lol1}} {{lol1}} {{lol1}} {{lol1}} {{lol1}} {{lol1}} {{lol1}} {{lol1}} {{lol1}} {{lol1}}',
'lol3={{lol2}} {{lol2}} {{lol2}} {{lol2}} {{lol2}} {{lol2}} {{lol2}} {{lol2}} {{lol2}} {{lol2}}',
// normally, we'd continue with lol4 through lol9 in the same manner,
// but this would make the test itself dangerous if the guards in the
// compiler fail. Given MAX_PLACEABLE_LENGTH of 2500, lol3 is enough
// to test this.
'lolz={{ lol3 }}'
].join('\n');
entries = createEntries(source);
});
it('Resolver.format() throws', function() {
assert.throws(function() {
Resolver.format(null, entries.lolz);
}, /too many characters in placeable/i);
});
});

View file

@ -0,0 +1,101 @@
/* global assert:true, it, before, beforeEach, describe, requireApp */
'use strict';
if (typeof navigator !== 'undefined') {
requireApp('sharedtest/test/unit/l10n/lib/resolver/header.js');
} else {
var assert = require('assert');
var Resolver = require('./header.js').Resolver;
var createEntries = require('./header.js').createEntries;
}
describe('Compiler errors:', function(){
var source, env;
beforeEach(function() {
env = createEntries(source);
});
describe('A complex string referencing an existing entity', function(){
before(function() {
source = [
'file=File',
'prompt={[ plural(n) ]}',
'prompt[one]=One {{ file }}',
'prompt[other]=Files'
].join('\n');
});
it('works with the default index', function(){
assert.strictEqual(
Resolver.format({n: 1}, env.prompt)[1], 'One File');
});
});
describe('A complex string referencing a missing entity', function(){
before(function() {
source = [
'prompt={[ plural(n) ]}',
'prompt[one]=One {{ file }}',
'prompt[other]=Files'
].join('\n');
});
it('returns the raw string', function(){
var value = Resolver.format({n: 1}, env.prompt)[1];
assert.strictEqual(value, 'One {{ file }}');
});
});
describe('A ctxdata variable in the index, with "other"', function(){
before(function() {
source = [
'file=File',
'prompt={[ plural(n) ]}',
'prompt[one]=One {{ file }}',
'prompt[other]=Files'
].join('\n');
});
it('is found', function(){
assert.strictEqual(
Resolver.format({n: 1}, env.prompt)[1], 'One File');
});
it('throws an IndexError if n is not defined', function(){
assert.throws(function() {
Resolver.format(null, env.prompt);
}, 'Unknown reference: n');
});
});
describe('A ctxdata variable in the index, without "other"', function(){
before(function() {
source = [
'file=File',
'prompt={[ plural(n) ]}',
'prompt[one]=One {{ file }}',
].join('\n');
});
it('is found', function(){
assert.strictEqual(
Resolver.format({n: 1}, env.prompt)[1], 'One File');
});
it('throws an IndexError if n is not defined', function(){
assert.throws(function() {
Resolver.format(null, env.prompt);
}, 'Unknown reference: n');
});
});
});

View file

@ -0,0 +1,33 @@
/* global navigator, exports */
/* exported Resolver */
'use strict';
if (typeof navigator !== 'undefined') {
var L10n = navigator.mozL10n._getInternalAPI();
var Resolver = L10n.Resolver;
} else {
var L10n = {
PropertiesParser:
require('../../../src/lib/format/properties/parser'),
Resolver: require('../../../src/lib/resolver'),
getPluralRule: require('../../../src/lib/plurals').getPluralRule
};
exports.createEntries = createEntries;
exports.Resolver = L10n.Resolver;
}
function createEntries(source) {
/* jshint -W089 */
var entries = Object.create(null);
var ast = L10n.PropertiesParser.parse(null, source);
for (var i = 0, len = ast.length; i < len; i++) {
entries[ast[i].$i] = L10n.Resolver.createEntry(ast[i], entries);
}
entries.__plural = L10n.getPluralRule('en-US');
return entries;
}

View file

@ -0,0 +1,86 @@
/* global assert:true, it, before, beforeEach, describe, requireApp */
'use strict';
if (typeof navigator !== 'undefined') {
requireApp('sharedtest/test/unit/l10n/lib/resolver/header.js');
} else {
var assert = require('assert');
var Resolver = require('./header.js').Resolver;
var createEntries = require('./header.js').createEntries;
}
describe('Index', function(){
var source, env;
beforeEach(function() {
env = createEntries(source);
});
describe('Different values of index', function(){
before(function() {
source = [
'foo=one',
'indexEntity={[ foo ]}',
'indexEntity[one]=One entity',
'indexUncalledMacro={[ plural ]}',
'indexUncalledMacro[one]=One uncalled macro',
'indexCalledMacro={[ plural(n) ]}',
'indexCalledMacro[one]=One called macro',
].join('\n');
});
it('works when the index is a regular entity', function() {
var value = Resolver.format({n: 1}, env.indexEntity)[1];
assert.strictEqual(value, 'One entity');
});
it('throws when the index is an uncalled macro', function() {
assert.throws(function() {
Resolver.format({n: 1}, env.indexUncalledMacro);
}, 'Unresolvable value');
});
it('works when the index is a called macro', function() {
var value = Resolver.format({n: 1}, env.indexCalledMacro)[1];
assert.strictEqual(value, 'One called macro');
});
});
describe('Cyclic reference to the same entity', function(){
before(function() {
source = [
'foo={[ plural(foo) ]}',
'foo[one]=One'
].join('\n');
});
it('is undefined', function() {
assert.throws(function() {
Resolver.format(null, env.foo);
}, 'Cyclic reference detected: foo');
});
});
describe('Reference from an attribute to the value of the same ' +
'entity', function(){
before(function() {
source = [
'foo=Foo',
'foo.attr={[ plural(foo) ]}',
'foo.attr[one]=One'
].join('\n');
});
it('value of the attribute is undefined', function() {
assert.strictEqual(Resolver.format(null, env.foo)[1], 'Foo');
assert.throws(function() {
Resolver.format(null, env.foo.attrs.attr);
}, 'Unresolvable value');
});
});
});

View file

@ -0,0 +1,357 @@
/* global assert:true, it, before, beforeEach, describe, requireApp */
'use strict';
if (typeof navigator !== 'undefined') {
requireApp('sharedtest/test/unit/l10n/lib/resolver/header.js');
} else {
var assert = require('assert');
var Resolver = require('./header.js').Resolver;
var createEntries = require('./header.js').createEntries;
}
describe('Macros', function(){
var source, ctxdata, env;
beforeEach(function() {
env = createEntries(source);
});
describe('referencing macros', function(){
before(function() {
ctxdata = {
n: 1
};
source = [
'placeMacro={{ plural }}',
'placeRealMacro={{ __plural }}'
].join('\n');
});
it('throws when resolving (not calling) a macro in a complex ' +
'string', function() {
assert.strictEqual(
Resolver.format(ctxdata, env.placeMacro)[1], '{{ plural }}');
assert.strictEqual(
Resolver.format(ctxdata, env.placeRealMacro)[1], '{{ __plural }}');
});
});
describe('passing arguments', function(){
before(function() {
ctxdata = {
n: 1
};
source = [
'foo=Foo',
'useFoo={{ foo }}',
'bar={[ plural(n) ]}',
'bar[one]=One',
'bar.attr=Attr',
'passFoo={[ plural(foo) ]}',
'passFoo[one]=One',
'passUseFoo={[ plural(useFoo) ]}',
'passUseFoo[one]=One',
'passBar={[ plural(bar) ]}',
'passBar[one]=One',
'passPlural={[ plural(plural) ]}',
'passPlural[one]=One',
'passMissing={[ plural(missing) ]}',
'passMissing[one]=One',
'passWatch={[ plural(watch) ]}',
'passWatch[one]=One',
].join('\n');
});
it('throws if an entity is passed', function() {
assert.throws(function() {
Resolver.format(ctxdata, env.passFoo);
}, 'Unresolvable value');
});
it('throws if a complex entity is passed', function() {
assert.throws(function() {
Resolver.format(ctxdata, env.passUseFoo);
}, 'Unresolvable value');
});
it('throws if a hash entity is passed', function() {
assert.throws(function() {
Resolver.format(ctxdata, env.passBar);
}, 'Unresolvable value');
});
it('throws if a macro is passed', function() {
assert.throws(function() {
Resolver.format(ctxdata, env.passPlural);
}, 'Unresolvable value');
});
it('throws if a missing entry is passed', function() {
assert.throws(function() {
Resolver.format(ctxdata, env.passMissing);
}, 'Unknown reference: missing');
});
it('throws if a native function is passed', function() {
assert.throws(function() {
Resolver.format(ctxdata, env.passWatch);
}, 'Unknown reference: watch');
});
});
});
describe('A simple plural macro', function(){
var source, env;
beforeEach(function() {
env = createEntries(source);
env.__plural = function() {
return 'other';
};
});
before(function() {
source = [
'foo={[ plural(n) ]}',
'foo[zero]=Zero',
'foo[one]=One',
'foo[two]=Two',
'foo[few]=Few',
'foo[many]=Many',
'foo[other]=Other'
].join('\n');
});
it('returns zero for 0', function() {
var value = Resolver.format({n: 0}, env.foo)[1];
assert.strictEqual(value, 'Zero');
});
it('returns one for 1', function() {
var value = Resolver.format({n: 1}, env.foo)[1];
assert.strictEqual(value, 'One');
});
it('returns two for 2', function() {
var value = Resolver.format({n: 2}, env.foo)[1];
assert.strictEqual(value, 'Two');
});
it('returns other for 3', function() {
var value = Resolver.format({n: 3}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
it('throws for no arg', function() {
assert.throws(function() {
Resolver.format(null, env.foo);
}, 'Unknown reference: n');
});
});
describe('A more complex plural macro', function(){
var source, env;
beforeEach(function() {
env = createEntries(source);
env.__plural = function(n) {
// a made-up plural rule:
// [0, 1) -> other
// [1, Inf) -> many
return (n >= 0 && n < 1) ? 'other' : 'many';
};
});
describe('an entity with all plural forms defined', function(){
before(function() {
source = [
'foo={[ plural(n) ]}',
'foo[zero]=Zero',
'foo[one]=One',
'foo[two]=Two',
'foo[few]=Few',
'foo[many]=Many',
'foo[other]=Other'
].join('\n');
});
it('returns zero for 0', function() {
var value = Resolver.format({n: 0}, env.foo)[1];
assert.strictEqual(value, 'Zero');
});
it('returns one for 1', function() {
var value = Resolver.format({n: 1}, env.foo)[1];
assert.strictEqual(value, 'One');
});
it('returns two for 2', function() {
var value = Resolver.format({n: 2}, env.foo)[1];
assert.strictEqual(value, 'Two');
});
it('returns many for 3', function() {
var value = Resolver.format({n: 3}, env.foo)[1];
assert.strictEqual(value, 'Many');
});
it('returns many for 5', function() {
var value = Resolver.format({n: 5}, env.foo)[1];
assert.strictEqual(value, 'Many');
});
it('returns other for 0.5', function() {
var value = Resolver.format({n: 0.5}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
it('throws for no arg', function() {
assert.throws(function() {
Resolver.format(null, env.foo);
}, 'Unknown reference: n');
});
});
describe('an entity without the zero, one and two forms', function(){
before(function() {
source = [
'foo={[ plural(n) ]}',
'foo[many]=Many',
'foo[other]=Other'
].join('\n');
});
it('returns other for 0', function() {
var value = Resolver.format({n: 0}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
it('returns many for 1', function() {
var value = Resolver.format({n: 1}, env.foo)[1];
assert.strictEqual(value, 'Many');
});
it('returns many for 2', function() {
var value = Resolver.format({n: 2}, env.foo)[1];
assert.strictEqual(value, 'Many');
});
it('returns many for 3', function() {
var value = Resolver.format({n: 3}, env.foo)[1];
assert.strictEqual(value, 'Many');
});
it('returns many for 5', function() {
var value = Resolver.format({n: 5}, env.foo)[1];
assert.strictEqual(value, 'Many');
});
it('returns other for 0.5', function() {
var value = Resolver.format({n: 0.5}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
});
describe('an entity without the many form', function(){
before(function() {
source = [
'foo={[ plural(n) ]}',
'foo[other]=Other'
].join('\n');
});
it('returns other for 0', function() {
var value = Resolver.format({n: 0}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
it('returns other for 1', function() {
var value = Resolver.format({n: 1}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
it('returns other for 2', function() {
var value = Resolver.format({n: 2}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
it('returns other for 3', function() {
var value = Resolver.format({n: 3}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
it('returns other for 5', function() {
var value = Resolver.format({n: 5}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
it('returns other for 0.5', function() {
var value = Resolver.format({n: 0.5}, env.foo)[1];
assert.strictEqual(value, 'Other');
});
});
describe('an entity without the other form, but with the one ' +
'form', function(){
before(function() {
source = [
'foo={[ plural(n) ]}',
'foo[one]=One'
].join('\n');
});
it('throws for 0', function() {
assert.throws(function() {
Resolver.format({n: 0}, env.foo);
}, 'Unresolvable value');
});
it('returns one for 1', function() {
var value = Resolver.format({n: 1}, env.foo)[1];
assert.strictEqual(value, 'One');
});
it('throws for 2', function() {
assert.throws(function() {
Resolver.format({n: 2}, env.foo);
}, 'Unresolvable value');
});
it('throws for 3', function() {
assert.throws(function() {
Resolver.format({n: 3}, env.foo);
}, 'Unresolvable value');
});
it('throws for 5', function() {
assert.throws(function() {
Resolver.format({n: 5}, env.foo);
}, 'Unresolvable value');
});
it('throws for 0.5', function() {
assert.throws(function() {
Resolver.format({n: 0.5}, env.foo);
}, 'Unresolvable value');
});
});
});

View file

@ -0,0 +1,136 @@
/* global assert:true, it, before, beforeEach, describe, requireApp */
'use strict';
if (typeof navigator !== 'undefined') {
requireApp('sharedtest/test/unit/l10n/lib/resolver/header.js');
} else {
var assert = require('assert');
var Resolver = require('./header.js').Resolver;
var createEntries = require('./header.js').createEntries;
}
describe('Primitives:', function(){
var source, env;
beforeEach(function() {
env = createEntries(source);
});
describe('Simple string value', function(){
before(function() {
source = [
'foo=Foo'
].join('\n');
});
it('returns the value', function(){
assert.strictEqual(Resolver.format(null, env.foo)[1], 'Foo');
});
});
describe('Complex string value', function(){
before(function() {
source = [
'foo=Foo',
'bar={{ foo }} Bar',
'baz={{ missing }}',
'qux={{ malformed }'
].join('\n');
});
it('returns the value', function(){
var value = Resolver.format(null, env.bar)[1];
assert.strictEqual(value, 'Foo Bar');
});
it('returns the raw string if the referenced entity is ' +
'not found', function(){
var value = Resolver.format(null, env.baz)[1];
assert.strictEqual(value, '{{ missing }}');
});
});
describe('Complex string referencing an entity with null value', function(){
before(function() {
source = [
'foo.attr=Foo',
'bar={{ foo }} Bar',
].join('\n');
});
it('returns the null value', function(){
var value = Resolver.format(null, env.foo)[1];
assert.strictEqual(value, null);
});
it('returns the attribute', function(){
var attr = Resolver.format(null, env.foo.attrs.attr)[1];
assert.strictEqual(attr, 'Foo');
});
it('returns the raw string when the referenced entity has ' +
'null value', function(){
var value = Resolver.format(null, env.bar)[1];
assert.strictEqual(value, '{{ foo }} Bar');
});
});
describe('Cyclic reference', function(){
before(function() {
source = [
'foo={{ bar }}',
'bar={{ foo }}'
].join('\n');
});
it('returns the raw string', function(){
var value = Resolver.format(null, env.foo)[1];
assert.strictEqual(value, '{{ foo }}');
});
});
describe('Cyclic self-reference', function(){
before(function() {
source = [
'foo={{ foo }}'
].join('\n');
});
it('returns the raw string', function(){
var value = Resolver.format(null, env.foo)[1];
assert.strictEqual(value, '{{ foo }}');
});
});
describe('Cyclic self-reference in a hash', function(){
before(function() {
source = [
'foo={[ plural(n) ]}',
'foo[one]={{ foo }}',
'foo[two]=Bar',
'bar={{ foo }}'
].join('\n');
});
it('returns the raw string', function(){
var value = Resolver.format({n: 1}, env.foo)[1];
assert.strictEqual(value, '{{ foo }}');
});
it('returns the valid value if requested directly', function(){
var value = Resolver.format({n: 2}, env.bar)[1];
assert.strictEqual(value, 'Bar');
});
});
});

View file

@ -0,0 +1,157 @@
/* global it, assert:true, describe, before, beforeEach */
/* global navigator */
'use strict';
var assert, PropertiesParser;
var reVowels = /[AEIOUaeiou]/;
function digest(str) {
var cur = 0;
var i = str.length;
while (i) {
if (reVowels.test(str[--i])) {
cur++;
}
}
return cur;
}
describe('walkContent', function() {
var L10n, source, ast;
before(function(done) {
if (typeof navigator !== 'undefined') {
require('/build/l10n.js', function() {
L10n = navigator.mozL10n._getInternalAPI();
PropertiesParser = L10n.PropertiesParser;
done();
});
} else {
assert = require('assert');
L10n = {};
L10n.walkContent = require('../../src/lib/util').walkContent;
PropertiesParser = require('../../src/lib/format/properties/parser');
done();
}
});
beforeEach(function() {
ast = PropertiesParser.parse(null, source);
});
describe('simple strings and attributes', function(){
before(function() {
source = [
'foo=Foo',
'foo.attr=An attribute',
].join('\n');
});
it('walks the value', function(){
var walked = L10n.walkContent(ast[0], digest);
assert.strictEqual(walked.$v, 2);
});
it('walks the attribute', function(){
var walked = L10n.walkContent(ast[0], digest);
assert.strictEqual(walked.attr, 5);
});
});
describe('one-level-deep dict', function(){
before(function() {
source = [
'foo={[ plural(n) ]}',
'foo[one]=One',
'foo[two]=Two',
'foo[few]=Few',
'foo[many]=Many',
'foo[other]=Other',
].join('\n');
});
it('walks the values', function(){
var walked = L10n.walkContent(ast[0], digest);
assert.strictEqual(walked.$v.one, 2);
assert.strictEqual(walked.$v.two, 1);
assert.strictEqual(walked.$v.few, 1);
assert.strictEqual(walked.$v.many, 1);
assert.strictEqual(walked.$v.other, 2);
});
it('does not modify the index', function(){
var walked = L10n.walkContent(ast[0], digest);
assert.deepEqual(walked.$x, [{t: 'idOrVar', v: 'plural'}, 'n']);
});
});
// XXX parser currently doesn't support nested dicts
describe.skip('two-level-deep dict', function(){
before(function() {
source = [
'foo={[ plural(n) ]}',
'foo[other]={[ plural(m) ]}',
'foo[other][one]=One',
'foo[other][two]=Two',
'foo[other][few]=Few',
'foo[other][many]=Many',
'foo[other][other]=Other',
].join('\n');
});
it('walks the values', function(){
var walked = L10n.walkContent(ast[0], digest);
assert.strictEqual(walked.$v.other.one, 2);
assert.strictEqual(walked.$v.other.two, 1);
assert.strictEqual(walked.$v.other.few, 1);
assert.strictEqual(walked.$v.other.many, 1);
assert.strictEqual(walked.$v.other.other, 2);
});
it('does not modify the indexes', function(){
var walked = L10n.walkContent(ast[0], digest);
assert.deepEqual(
walked.$x, [{t: 'idOrVar', v: 'plural'}, 'n']);
assert.deepEqual(
walked.$v.other.$x, [{t: 'idOrVar', v: 'plural'}, 'n']);
});
});
describe('attribute that is a one-level-deep dict', function(){
before(function() {
source = [
'foo.attr={[ plural(n) ]}',
'foo.attr[one]=One',
'foo.attr[two]=Two',
'foo.attr[few]=Few',
'foo.attr[many]=Many',
'foo.attr[other]=Other',
].join('\n');
});
it('walks the values', function(){
var walked = L10n.walkContent(ast[0], digest);
assert.strictEqual(walked.attr.$v.one, 2);
assert.strictEqual(walked.attr.$v.two, 1);
assert.strictEqual(walked.attr.$v.few, 1);
assert.strictEqual(walked.attr.$v.many, 1);
assert.strictEqual(walked.attr.$v.other, 2);
});
it('does not modify the indexes', function(){
var walked = L10n.walkContent(ast[0], digest);
assert.deepEqual(walked.attr.$x, [{t: 'idOrVar', v: 'plural'}, 'n']);
});
});
});