New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add language() and languages() #11
Changes from 6 commits
d5e556d
a604f80
23f10ff
ce8c6b3
7be9b04
b36a403
bbfe1f7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,14 @@ | ||
'use strict'; | ||
|
||
var CharsetLib = require('./charset'); | ||
var EncodingLib = require('./encoding'); | ||
var Charset = require('./charset'); | ||
var Encoding = require('./encoding'); | ||
var Language = require('./language'); | ||
|
||
exports.charset = CharsetLib.charset; | ||
exports.charsets = CharsetLib.charsets; | ||
exports.charset = Charset.charset; | ||
exports.charsets = Charset.charsets; | ||
|
||
exports.encoding = EncodingLib.encoding; | ||
exports.encodings = EncodingLib.encodings; | ||
exports.encoding = Encoding.encoding; | ||
exports.encodings = Encoding.encodings; | ||
|
||
exports.language = Language.language; | ||
exports.languages = Language.languages; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
// Load modules | ||
|
||
var Boom = require('boom'); | ||
var Hoek = require('hoek'); | ||
|
||
|
||
// Declare internals | ||
|
||
var internals = {}; | ||
|
||
|
||
// https://tools.ietf.org/html/rfc7231#section-5.3.5 | ||
// Accept-Language: da, en-gb;q=0.8, en;q=0.7 | ||
|
||
|
||
exports.language = function (header, preferences) { | ||
|
||
Hoek.assert(!preferences || Array.isArray(preferences), 'Preferences must be an array'); | ||
var languages = exports.languages(header); | ||
|
||
if (languages.length === 0) { | ||
languages.push(''); | ||
} | ||
|
||
// No preferences. Take the first charset. | ||
|
||
if (!preferences || preferences.length === 0) { | ||
return languages[0]; | ||
} | ||
|
||
// If languages includes * return first preference | ||
|
||
if (languages.indexOf('*') !== -1) { | ||
return preferences[0]; | ||
} | ||
|
||
// Try to find the first match in the array of preferences | ||
|
||
preferences = preferences.map(function (str) { | ||
|
||
return str.toLowerCase(); | ||
}); | ||
|
||
for (var i = 0, il = languages.length; i < il; ++i) { | ||
if (preferences.indexOf(languages[i].toLowerCase()) !== -1) { | ||
return languages[i]; | ||
} | ||
} | ||
|
||
return ''; | ||
}; | ||
|
||
|
||
exports.languages = function (header) { | ||
|
||
if (header === undefined || typeof header !== 'string') { | ||
return []; | ||
} | ||
|
||
return header | ||
.split(',') | ||
.map(internals.getParts) | ||
.filter(internals.removeUnwanted) | ||
.sort(internals.compareByWeight) | ||
.map(internals.partToLanguage); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i feel like this could use some optimizing.. for now it's fine, but it's something to think about since it is something that potentially runs for every request. |
||
}; | ||
|
||
|
||
internals.getParts = function (item) { | ||
|
||
var result = { | ||
weight: 1, | ||
language: '' | ||
}; | ||
|
||
var match = item.match(internals.partsRegex); | ||
|
||
if (!match) { | ||
return result; | ||
} | ||
|
||
result.language = match[1]; | ||
if (match[2] && internals.isNumber(match[2]) ) { | ||
var weight = parseFloat(match[2]); | ||
if (weight === 0 || (weight >= 0.001 && weight <= 1)) { | ||
result.weight = weight; | ||
} | ||
} | ||
return result; | ||
}; | ||
|
||
|
||
// 1: token 2: qvalue | ||
internals.partsRegex = /\s*([^;]+)(?:\s*;\s*[qQ]\=([01](?:\.\d*)?))?\s*/; | ||
|
||
|
||
internals.removeUnwanted = function (item) { | ||
|
||
return item.weight !== 0 && item.language !== ''; | ||
}; | ||
|
||
|
||
internals.compareByWeight = function (a, b) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Two empty lines between functions. |
||
|
||
return a.weight < b.weight; | ||
}; | ||
|
||
|
||
internals.partToLanguage = function (item) { | ||
|
||
return item.language; | ||
}; | ||
|
||
|
||
internals.isNumber = function (n) { | ||
|
||
return !isNaN(parseFloat(n)); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
// Load modules | ||
|
||
var Accept = require('..'); | ||
var Code = require('code'); | ||
var Lab = require('lab'); | ||
|
||
|
||
// Declare internals | ||
|
||
var internals = {}; | ||
|
||
|
||
// Test shortcuts | ||
|
||
var lab = exports.lab = Lab.script(); | ||
var describe = lab.describe; | ||
var it = lab.it; | ||
var expect = Code.expect; | ||
|
||
|
||
describe('language()', function () { | ||
|
||
it('parses the header', function (done) { | ||
|
||
var language = Accept.language('da, en-GB, en'); | ||
expect(language).to.equal('da'); | ||
done(); | ||
}); | ||
|
||
it('respects weights', function (done) { | ||
|
||
var language = Accept.language('en;q=0.6, en-GB;q=0.8'); | ||
expect(language).to.equal('en-GB'); | ||
done(); | ||
}); | ||
|
||
it('requires the preferences parameter to be an array', function (done) { | ||
|
||
expect(function () { | ||
|
||
Accept.language('en;q=0.6, en-GB;q=0.8', 'en'); | ||
}).to.throw('Preferences must be an array'); | ||
done(); | ||
}); | ||
|
||
it('returns empty string with header is empty', function (done) { | ||
|
||
var language = Accept.language(''); | ||
expect(language).to.equal(''); | ||
done(); | ||
}); | ||
|
||
it('returns empty string if header is missing', function (done) { | ||
|
||
var language = Accept.language(); | ||
expect(language).to.equal(''); | ||
done(); | ||
}); | ||
|
||
it('ignores an empty preferences array', function (done) { | ||
|
||
var language = Accept.language('da, en-GB, en', []); | ||
expect(language).to.equal('da'); | ||
done(); | ||
}); | ||
|
||
it('returns empty string if none of the preferences match', function (done) { | ||
|
||
var language = Accept.language('da, en-GB, en', ['es']); | ||
expect(language).to.equal(''); | ||
done(); | ||
}); | ||
|
||
it('returns first preference if header has *', function (done) { | ||
|
||
var language = Accept.language('da, en-GB, en, *', ['en-US']); | ||
expect(language).to.equal('en-US'); | ||
done(); | ||
}); | ||
|
||
it('returns first found preference that header includes', function (done) { | ||
|
||
var language = Accept.language('da, en-GB, en', ['en-US', 'en-GB']); | ||
expect(language).to.equal('en-GB'); | ||
done(); | ||
}); | ||
|
||
it('returns preference with highest specificity', function (done) { | ||
|
||
var language = Accept.language('da, en-GB, en', ['en', 'en-GB']); | ||
expect(language).to.equal('en-GB'); | ||
done(); | ||
}); | ||
|
||
it('return language with heighest weight', function (done) { | ||
|
||
var language = Accept.language('da;q=0.5, en;q=1', ['da', 'en']); | ||
expect(language).to.equal('en'); | ||
done(); | ||
}); | ||
|
||
it('ignores preference case when matching', function (done) { | ||
|
||
var language = Accept.language('da, en-GB, en', ['en-us', 'en-gb']); // en-GB vs en-gb | ||
expect(language).to.equal('en-GB'); | ||
done(); | ||
}); | ||
}); | ||
|
||
|
||
// languages | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment adds nothing. |
||
describe('languages()', function () { | ||
|
||
it('parses the header', function (done) { | ||
|
||
var languages = Accept.languages('da, en-GB, en'); | ||
expect(languages).to.deep.equal(['da', 'en-GB', 'en']); | ||
done(); | ||
}); | ||
|
||
it('orders by weight(q)', function (done) { | ||
|
||
var languages = Accept.languages('da, en;q=0.7, en-GB;q=0.8'); | ||
expect(languages).to.deep.equal(['da', 'en-GB', 'en']); | ||
done(); | ||
}); | ||
|
||
it('maintains case', function (done) { | ||
|
||
var languages = Accept.languages('da, en-GB, en'); | ||
expect(languages).to.deep.equal(['da', 'en-GB', 'en']); | ||
done(); | ||
}); | ||
|
||
it('drops zero weighted charsets', function (done) { | ||
|
||
var languages = Accept.languages('da, en-GB, es;q=0, en'); | ||
expect(languages).to.deep.equal(['da', 'en-GB', 'en']); | ||
done(); | ||
}); | ||
|
||
it('ignores invalid weights', function (done) { | ||
|
||
var languages = Accept.languages('da, en-GB;q=1.1, es;q=a, en;q=0.0001'); | ||
expect(languages).to.deep.equal(['da', 'en-GB', 'es', 'en']); | ||
done(); | ||
}); | ||
|
||
it('return empty array when no header is present', function (done) { | ||
|
||
var languages = Accept.languages(); | ||
expect(languages).to.deep.equal([]); | ||
done(); | ||
}); | ||
|
||
it('return empty array when header is empty', function (done) { | ||
|
||
var languages = Accept.languages(''); | ||
expect(languages).to.deep.equal([]); | ||
done(); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I bet a for loop here would be much faster.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be. I've seen some old perf statements indicating a big difference, but nothing that's recent. My gut says there probably isn't a huge difference now days, but I haven't done anything to bear that out. For now I'll stick with the current map implementation, and make an issue to determine what needs to be done with that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
for loops are definitively faster, i've done benchmarks very recently. it's worth changing it over.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK. thanks for that. Saves me some time on benchmarking.