Skip to content
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

Merged
merged 7 commits into from Aug 12, 2015
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Expand Up @@ -14,14 +14,16 @@ Lead Maintainer - [Mark Bradshaw](https://github.com/mark-bradshaw)
- [`charsets(charsetHeader)`](#charsetscharsetheader)
- [`encoding(encodingHeader, [preferences])`](#encodingencodingheader-preferences)
- [`encodings(encodingHeader)`](#encodingsencodingheader)
- [`language(languageHeader, [preferences])`](#languagelanguageheader-preferences)
- [`languages(languageHeader)`](#languageslanguageheader)
- [Q Weightings](#q-weightings)
- [Encodings](#encodings)
- [Preferences](#preferences)
- [Identity](#identity)

## Introduction

Accept helps to answer the question of how best to respond to a HTTP request, based on the requesting browser's capabilities. Accept will parse the headers of a HTTP request and tell you what the preferred encoding is and what charsets are accepted.
Accept helps to answer the question of how best to respond to a HTTP request, based on the requesting browser's capabilities. Accept will parse the headers of a HTTP request and tell you what the preferred encoding is, what language should be used, and what charsets are accepted.

Additional details about Accept headers and content negotiation can be found in [IETF RFC 7231, Section 5.3](https://tools.ietf.org/html/rfc7231#section-5.3).

Expand Down Expand Up @@ -62,6 +64,25 @@ Given a string of acceptable encodings from a HTTP request Accept-Encoding heade
var encodings = Accept.encodings("compress;q=0.5, gzip;q=1.0"); // encodings === ["gzip", "compress", "identity"]
```

### `language(languageHeader, [preferences])`

Given a string of acceptable languages from a HTTP request Accept-Language header, and an optional array of language preferences, it will return a string indicating the best language that can be used in the HTTP response. It respects the [q weightings](#weightings) of the languages in the header, returning the matched preference with the highest weighting. The case of the preference does not have to match the case of the option in the header.

```
var language = Accept.language("en;q=0.7, en-GB;q=0.8"); // language === "en-GB"

// the case of the preference "en-gb" does not match the case of the header option "en-GB"
var language = Accept.language("en;q=0.7, en-GB;q=0.8", ["en-gb"]); // language === "en-GB"
```

### `languages(languageHeader)`

Given a string of acceptable languages from a HTTP request Accept-Language header it will return an array of strings indicating the possible languages that can be used in the HTTP response, in order from most preferred to least as determined by the [q weightings](#weightings).

```
var languages = Accept.languages("da, en;q=0.7, en-GB;q=0.8"); // languages === ["da", "en-GB", "en"]
```


## Q Weightings

Expand Down
16 changes: 10 additions & 6 deletions lib/index.js
@@ -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;
118 changes: 118 additions & 0 deletions lib/language.js
@@ -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) {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

Copy link
Member

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.

Copy link
Contributor Author

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.


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);
Copy link
Member

Choose a reason for hiding this comment

The 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) {
Copy link
Contributor

Choose a reason for hiding this comment

The 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));
};
2 changes: 1 addition & 1 deletion test/encoding.js
@@ -1,8 +1,8 @@
// Load modules

var Accept = require('..');
var Code = require('code');
var Lab = require('lab');
var Accept = require('..');


// Declare internals
Expand Down
162 changes: 162 additions & 0 deletions test/language.js
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The 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();
});
});