-
Notifications
You must be signed in to change notification settings - Fork 400
Add accept-language support #770
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
Changes from all commits
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 |
---|---|---|
|
@@ -18,4 +18,6 @@ module.exports = { | |
], | ||
}, | ||
}, | ||
|
||
redirectLangPrefix: false, | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -59,11 +59,11 @@ export function normalizeLocale(locale) { | |
return langToLocale(localeToLang(locale)); | ||
} | ||
|
||
export function isValidLang(lang) { | ||
return validLangs.includes(lang); | ||
export function isValidLang(lang, { _validLangs = validLangs } = {}) { | ||
return _validLangs.includes(normalizeLang(lang)); | ||
} | ||
|
||
export function getLanguage(langOrLocale) { | ||
export function sanitizeLanguage(langOrLocale) { | ||
let language = normalizeLang(langOrLocale); | ||
// Only look in the un-mapped lang list. | ||
if (!langs.includes(language)) { | ||
|
@@ -73,24 +73,91 @@ export function getLanguage(langOrLocale) { | |
} | ||
|
||
export function isRtlLang(lang) { | ||
const language = getLanguage(lang); | ||
const language = sanitizeLanguage(lang); | ||
return rtlLangs.includes(language); | ||
} | ||
|
||
export function getDirection(lang) { | ||
return isRtlLang(lang) ? 'rtl' : 'ltr'; | ||
} | ||
|
||
export function getLangFromRouter(renderProps) { | ||
if (renderProps) { | ||
// Get the lang from the url param by default | ||
// if it exists. | ||
if (renderProps.params && renderProps.params.lang) { | ||
return getLanguage(renderProps.params.lang); | ||
} else if (renderProps.location && renderProps.location.query && | ||
renderProps.location.query.lang) { | ||
return getLanguage(renderProps.location.query.lang); | ||
|
||
function qualityCmp(a, b) { | ||
if (a.quality === b.quality) { | ||
return 0; | ||
} else if (a.quality < b.quality) { | ||
return 1; | ||
} | ||
return -1; | ||
} | ||
|
||
/* | ||
* Parses the HTTP accept-language header and returns a | ||
* sorted array of objects. Example object: | ||
* { lang: 'pl', quality: 0.7 } | ||
*/ | ||
export function parseAcceptLanguage(header) { | ||
// pl,fr-FR;q=0.3,en-US;q=0.1 | ||
if (!header || !header.split) { | ||
return []; | ||
} | ||
const rawLangs = header.split(','); | ||
const langList = rawLangs.map((rawLang) => { | ||
const parts = rawLang.split(';'); | ||
let q = 1; | ||
if (parts.length > 1 && parts[1].trim().indexOf('q=') === 0) { | ||
const qVal = parseFloat(parts[1].split('=')[1]); | ||
if (isNaN(qVal) === false) { | ||
q = qVal; | ||
} | ||
} | ||
return { | ||
lang: parts[0].trim(), | ||
quality: q, | ||
}; | ||
}); | ||
langList.sort(qualityCmp); | ||
return langList; | ||
} | ||
|
||
|
||
/* | ||
* Given an accept-language header and a list of currently | ||
* supported languages, returns the best match with no normalization. | ||
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 says "with no normalization" but 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. Oh, I guess this would match 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. Ah, never mind. It would treat |
||
* | ||
*/ | ||
export function getLangFromHeader(acceptLanguage, { _validLangs } = {}) { | ||
let userLang; | ||
if (acceptLanguage) { | ||
const langList = parseAcceptLanguage(acceptLanguage); | ||
for (const langPref of langList) { | ||
if (isValidLang(langPref.lang, { _validLangs })) { | ||
userLang = langPref.lang; | ||
break; | ||
// Match locale, even if region isn't supported | ||
} else if (isValidLang(langPref.lang.split('-')[0], { _validLangs })) { | ||
userLang = langPref.lang.split('-')[0]; | ||
break; | ||
} | ||
} | ||
} | ||
return userLang; | ||
} | ||
|
||
/* | ||
* Looks up the language from the router renderProps. | ||
* When that fails fall-back to accept-language. | ||
* | ||
*/ | ||
export function getFilteredUserLanguage({ renderProps, acceptLanguage } = {}) { | ||
let userLang; | ||
// Get the lang from the url param by default if it exists. | ||
if (renderProps && renderProps.params && renderProps.params.lang) { | ||
userLang = renderProps.params.lang; | ||
} | ||
// If we don't have a valid userLang set yet try accept-language. | ||
if (!isValidLang(userLang) && acceptLanguage) { | ||
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 think the export function getFilteredUserLanguage({ renderProps, acceptLanguage } = {}) {
if (isValidLang(renderProps.params.lang)) {
return renderProps.params.lang;
}
return sanitizeLanguage(getLangFromHeader(acceptLanguage));
} 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 don't know how we can trust renderProps is OK without checking it against the list of allowed values. It feels safer to assume we can't trust it to me. 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 just mean we should be able to trust that react-router gives us the same It feels like this function is doing a bit too much since it is finding possible langs, validating them and returning a match. I'm not sure if it's worth the refactor but it would be reasonable to me to make this essentially just do 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. Ah I see now. One problem is that not all apps will have localization. |
||
userLang = getLangFromHeader(acceptLanguage); | ||
} | ||
return defaultLang; | ||
return sanitizeLanguage(userLang); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -19,7 +19,7 @@ import ServerHtml from 'core/containers/ServerHtml'; | |
import config from 'config'; | ||
import { setLang, setJWT } from 'core/actions'; | ||
import log from 'core/logger'; | ||
import { getDirection, getLangFromRouter, langToLocale } from 'core/i18n/utils'; | ||
import { getDirection, getFilteredUserLanguage, langToLocale } from 'core/i18n/utils'; | ||
import I18nProvider from 'core/i18n/Provider'; | ||
import Jed from 'jed'; | ||
|
||
|
@@ -83,6 +83,11 @@ function baseServer(routes, createStore, { appInstanceName = appName } = {}) { | |
app.get('/', (req, res) => res.redirect(302, '/search')); | ||
} | ||
|
||
if (appInstanceName === 'disco' && isDevelopment) { | ||
app.get('/', (req, res) => | ||
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. react-router has a |
||
res.redirect(302, '/en-US/firefox/discovery/pane/48.0/Darwin/normal')); | ||
} | ||
|
||
app.use((req, res) => { | ||
if (isDevelopment) { | ||
log.info('Running in Development Mode'); | ||
|
@@ -113,19 +118,24 @@ function baseServer(routes, createStore, { appInstanceName = appName } = {}) { | |
fs.readFileSync(path.join(config.get('basePath'), 'dist/sri.json')) | ||
) : {}; | ||
|
||
const lang = getLangFromRouter(renderProps); | ||
const acceptLanguage = req.headers['accept-language']; | ||
// Get language from URL or fall-back to accept-language. | ||
const lang = getFilteredUserLanguage({ renderProps, acceptLanguage }); | ||
|
||
if (renderProps.params && renderProps.params.lang) { | ||
if (renderProps.params) { | ||
const origLang = renderProps.params.lang; | ||
if (lang !== origLang) { | ||
// If the original lang param (found via the router) matches | ||
// the first part of the original url path, redirect to the | ||
// normalized lang (which will be the default if invalid). | ||
// If a lang was provided but the lang looked up is different | ||
// redirect to the looked-up lang (or default). | ||
// eslint-disable-next-line no-unused-vars | ||
const [_, firstPart, ...rest] = req.originalUrl.split('/'); | ||
if (origLang === decodeURIComponent(firstPart)) { | ||
// The '' provides a leading / | ||
return res.redirect(301, ['', lang, ...rest].join('/')); | ||
return res.redirect(302, ['', lang, ...rest].join('/')); | ||
} else if (!origLang && config.get('redirectLangPrefix')) { | ||
// If there was no lang param. Redirect to the same URL with | ||
// a lang prepended. | ||
return res.redirect(302, `/${lang}${req.originalUrl}`); | ||
} | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,14 @@ | ||
import React from 'react'; | ||
import { IndexRoute, Route } from 'react-router'; | ||
import { Router, Route } from 'react-router'; | ||
|
||
import App from './containers/App'; | ||
import DiscoPane from './containers/DiscoPane'; | ||
|
||
export default ( | ||
<Route path="/" component={App}> | ||
<IndexRoute component={DiscoPane} /> | ||
<Router component={App}> | ||
<Route | ||
path="/:lang/firefox/discovery/pane/:version/:platform/:compatibilityMode" | ||
path="/(:lang/)firefox/discovery/pane/:version/:platform/:compatibilityMode" | ||
component={DiscoPane} | ||
/> | ||
</Route> | ||
</Router> | ||
); |
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.
This should probably be
path="/(:lang/)firefox/"
but that could wait if you'd like.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 wonder how this will work if you get something like
/firefox/addons/
, would it be smart enough to see that there is nolang
? Maybe we shouldn't worry about redirecting the lang other than at/
?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.
Yes this does work as long as the rest of the URL matches. See https://github.com/mozilla/addons-frontend/pull/770/files#diff-4f380033f80b73f222c107868068167aR72