Add language routing

This commit is contained in:
Berkeley Martinez
2016-06-17 12:35:10 -07:00
parent ee4f1dbb73
commit 078560c3ba
18 changed files with 305 additions and 79 deletions

View File

@ -1,5 +1,5 @@
import React, { PropTypes } from 'react'; import React, { PropTypes } from 'react';
import { Link } from 'react-router'; import LangLink from '../../../../utils/Language-Link.jsx';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import FA from 'react-fontawesome'; import FA from 'react-fontawesome';
import PureComponent from 'react-pure-render/component'; import PureComponent from 'react-pure-render/component';
@ -54,7 +54,7 @@ export class Block extends PureComponent {
className={ challengeClassName } className={ challengeClassName }
key={ title } key={ title }
> >
<Link to={ `/challenges/${blockName}/${dashedName}` }> <LangLink to={ `/challenges/${blockName}/${dashedName}` }>
<span <span
onClick={ () => updateCurrentChallenge(challenge) } onClick={ () => updateCurrentChallenge(challenge) }
> >
@ -66,7 +66,7 @@ export class Block extends PureComponent {
'' ''
} }
</span> </span>
</Link> </LangLink>
</p> </p>
); );
}); });

View File

@ -24,6 +24,7 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
)) ))
.flatMap(({ type, payload: { dashedName, block } = {} }) => { .flatMap(({ type, payload: { dashedName, block } = {} }) => {
const state = getState(); const state = getState();
const lang = state.app.languageTag;
if (type === replaceChallenge) { if (type === replaceChallenge) {
const { challenge: newChallenge } = challengeSelector({ const { challenge: newChallenge } = challengeSelector({
...state, ...state,
@ -38,8 +39,10 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
return Observable.just(null); return Observable.just(null);
} }
const options = { service: 'map' }; const options = { service: 'map' };
options.params = { lang };
if (type === fetchChallenge) { if (type === fetchChallenge) {
options.params = { dashedName, block }; options.params.dashedName = dashedName;
options.params.block = block;
} }
return services.readService$(options) return services.readService$(options)
.flatMap(({ entities, result, redirect } = {}) => { .flatMap(({ entities, result, redirect } = {}) => {

View File

@ -2,7 +2,7 @@ import { modernChallenges, map, challenges } from './challenges';
import NotFound from '../components/NotFound/index.jsx'; import NotFound from '../components/NotFound/index.jsx';
export default { export default {
path: '/', path: '/:lang',
childRoutes: [ childRoutes: [
challenges, challenges,
modernChallenges, modernChallenges,

View File

@ -0,0 +1,42 @@
import React, { PropTypes } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router';
import supportedLanguages from '../../utils/supported-languages';
const toLowerCase = String.prototype.toLowerCase;
function addLang(url, lang) {
const maybeLang = toLowerCase.call(url.split('/')[1]);
if (supportedLanguages[maybeLang]) {
return url;
}
if (supportedLanguages[lang]) {
return `/${lang}${url}`;
}
return `/en${url}`;
}
const mapStateToProps = state => ({ lang: state.app.lang });
export class LangLink extends React.Component {
static displayName = 'LangLink';
static propTypes = {
to: PropTypes.string,
lang: PropTypes.string
};
render() {
const {
to,
lang,
...props
} = this.props;
return (
<Link
to={ addLang(to, lang) }
{ ...props }
/>
);
}
}
export default connect(mapStateToProps)(LangLink);

View File

@ -195,6 +195,11 @@
"theme": { "theme": {
"type": "string", "type": "string",
"default": "default" "default": "default"
},
"languageTag": {
"type": "string",
"description": "A IETF language tag",
"default": "en"
} }
}, },
"validations": [], "validations": [],
@ -256,6 +261,13 @@
"principalId": "$owner", "principalId": "$owner",
"permission": "ALLOW", "permission": "ALLOW",
"property": "updateTheme" "property": "updateTheme"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "updateLanguage"
} }
], ],
"methods": {} "methods": {}

View File

@ -0,0 +1,4 @@
export default {
en: 'English',
es: 'Spanish'
};

View File

@ -83,5 +83,5 @@ export default function about(app) {
} }
router.get('/about', showAbout); router.get('/about', showAbout);
app.use(router); app.use('/:lang', router);
} }

View File

@ -50,6 +50,7 @@ function findNonprofit(name) {
export default function commit(app) { export default function commit(app) {
const router = app.loopback.Router(); const router = app.loopback.Router();
const api = app.loopback.Router();
const { Pledge } = app.models; const { Pledge } = app.models;
router.get( router.get(
@ -68,19 +69,20 @@ export default function commit(app) {
renderDirectory renderDirectory
); );
router.post( api.post(
'/commit/stop-commitment', '/commit/stop-commitment',
sendNonUserToCommit, sendNonUserToCommit,
stopCommit stopCommit
); );
router.post( api.post(
'/commit/complete-goal', '/commit/complete-goal',
sendNonUserToCommit, sendNonUserToCommit,
completeCommitment completeCommitment
); );
app.use(router); app.use(api);
app.use(':/lang', router);
function commitToNonprofit(req, res, next) { function commitToNonprofit(req, res, next) {
const { user } = req; const { user } = req;

View File

@ -1,4 +1,5 @@
import { defaultProfileImage } from '../../common/utils/constantStrings.json'; import { defaultProfileImage } from '../../common/utils/constantStrings.json';
import supportedLanguages from '../../common/utils/supported-languages';
const message = const message =
'Learn to Code and Help Nonprofits'; 'Learn to Code and Help Nonprofits';
@ -6,24 +7,37 @@ const message =
module.exports = function(app) { module.exports = function(app) {
var router = app.loopback.Router(); var router = app.loopback.Router();
router.get('/', addDefaultImage, index); router.get('/', addDefaultImage, index);
app.use(
'/:lang',
(req, res, next) => {
// add url language to request for all routers
req._urlLang = req.params.lang;
next();
},
router
);
app.use(router); app.use(router);
function addDefaultImage(req, res, next) { function addDefaultImage(req, res, next) {
if (!req.user || req.user.picture) { if (!req.user || req.user.picture) {
return next(); return next();
} }
req.user.picture = defaultProfileImage; return req.user.update$({ picture: defaultProfileImage })
return req.user.save(function(err) { .subscribe(
if (err) { return next(err); } () => next(),
return next(); next
}); );
} }
function index(req, res) { function index(req, res, next) {
if (!supportedLanguages[req._urlLang]) {
return next();
}
if (req.user) { if (req.user) {
return res.redirect('/challenges/current-challenge'); return res.redirect('/challenges/current-challenge');
} }
return res.render('home', { title: message }); return res.render('home', { title: message });
} }
}; };

View File

@ -6,24 +6,32 @@ import secrets from '../../config/secrets';
module.exports = function(app) { module.exports = function(app) {
const router = app.loopback.Router(); const router = app.loopback.Router();
const User = app.models.User; const User = app.models.User;
router.get('/api/github', githubCalls); const noLangRouter = app.loopback.Router();
router.get('/chat', chat); noLangRouter.get('/api/github', githubCalls);
router.get('/coding-bootcamp-cost-calculator', bootcampCalculator); noLangRouter.get('/chat', chat);
router.get('/twitch', twitch); noLangRouter.get('/twitch', twitch);
router.get('/pmi-acp-agile-project-managers', agileProjectManagers); noLangRouter.get('/unsubscribe/:email', unsubscribeMonthly);
router.get('/pmi-acp-agile-project-managers-form', agileProjectManagersForm); noLangRouter.get(
'/unsubscribe-notifications/:email',
unsubscribeNotifications
);
noLangRouter.get('/unsubscribe-quincy/:email', unsubscribeQuincy);
noLangRouter.get('/submit-cat-photo', submitCatPhoto);
noLangRouter.get(
'/the-fastest-web-page-on-the-internet',
theFastestWebPageOnTheInternet
);
noLangRouter.get('/shop/cancel-stickers', cancelStickers);
noLangRouter.get('/shop/confirm-stickers', confirmStickers);
router.get('/unsubscribed', unsubscribed);
router.get('/nonprofits', nonprofits); router.get('/nonprofits', nonprofits);
router.get('/nonprofits-form', nonprofitsForm); router.get('/nonprofits-form', nonprofitsForm);
router.get('/unsubscribe/:email', unsubscribeMonthly); router.get('/pmi-acp-agile-project-managers', agileProjectManagers);
router.get('/unsubscribe-notifications/:email', unsubscribeNotifications); router.get('/pmi-acp-agile-project-managers-form', agileProjectManagersForm);
router.get('/unsubscribe-quincy/:email', unsubscribeQuincy); router.get('/coding-bootcamp-cost-calculator', bootcampCalculator);
router.get('/unsubscribed', unsubscribed);
router.get('/get-started', getStarted);
router.get('/submit-cat-photo', submitCatPhoto);
router.get('/stories', showTestimonials); router.get('/stories', showTestimonials);
router.get('/shop', showShop); router.get('/shop', showShop);
router.get('/shop/cancel-stickers', cancelStickers);
router.get('/shop/confirm-stickers', confirmStickers);
router.get('/all-stories', showAllTestimonials); router.get('/all-stories', showAllTestimonials);
router.get('/terms', terms); router.get('/terms', terms);
router.get('/privacy', privacy); router.get('/privacy', privacy);
@ -34,12 +42,9 @@ module.exports = function(app) {
); );
router.get('/code-of-conduct', codeOfConduct); router.get('/code-of-conduct', codeOfConduct);
router.get('/academic-honesty', academicHonesty); router.get('/academic-honesty', academicHonesty);
router.get(
'/the-fastest-web-page-on-the-internet',
theFastestWebPageOnTheInternet
);
app.use(router); app.use(noLangRouter);
app.use('/:lang', router);
function chat(req, res) { function chat(req, res) {
res.redirect('https://gitter.im/FreeCodeCamp/FreeCodeCamp'); res.redirect('https://gitter.im/FreeCodeCamp/FreeCodeCamp');
@ -242,12 +247,6 @@ module.exports = function(app) {
}); });
} }
function getStarted(req, res) {
res.render('resources/get-started', {
title: 'How to get started with Free Code Camp'
});
}
function githubCalls(req, res, next) { function githubCalls(req, res, next) {
var githubHeaders = { var githubHeaders = {
headers: { headers: {

View File

@ -12,8 +12,6 @@ const log = debug('fcc:react-server');
// add routes here as they slowly get reactified // add routes here as they slowly get reactified
// remove their individual controllers // remove their individual controllers
const routes = [ const routes = [
'/videos',
'/videos/*',
'/challenges', '/challenges',
'/challenges/*', '/challenges/*',
'/map' '/map'
@ -24,6 +22,12 @@ const devRoutes = [];
export default function reactSubRouter(app) { export default function reactSubRouter(app) {
var router = app.loopback.Router(); var router = app.loopback.Router();
router.get('/videos', (req, res) => res.redirect('/map'));
router.get(
'/videos/:dashedName',
(req, res) => res.redirect(`/challenges/${req.params.dashedName}`)
);
// These routes are in production // These routes are in production
routes.forEach((route) => { routes.forEach((route) => {
router.get(route, serveReactApp); router.get(route, serveReactApp);
@ -35,13 +39,15 @@ export default function reactSubRouter(app) {
}); });
} }
app.use(router); app.use('/:lang', router);
function serveReactApp(req, res, next) { function serveReactApp(req, res, next) {
const { lang } = req;
const serviceOptions = { req }; const serviceOptions = { req };
createApp({ createApp({
serviceOptions, serviceOptions,
location: req.path location: req.originalUrl,
initialState: { app: { languageTag: lang } }
}) })
// if react-router does not find a route send down the chain // if react-router does not find a route send down the chain
.filter(({ redirect, props }) => { .filter(({ redirect, props }) => {

View File

@ -3,6 +3,7 @@ import moment from 'moment-timezone';
import { Observable } from 'rx'; import { Observable } from 'rx';
import debugFactory from 'debug'; import debugFactory from 'debug';
import supportedLanguages from '../../common/utils/supported-languages';
import { import {
frontEndChallengeId, frontEndChallengeId,
dataVisChallengeId, dataVisChallengeId,
@ -123,8 +124,9 @@ function buildDisplayChallenges(challengeMap = {}, timezone) {
} }
module.exports = function(app) { module.exports = function(app) {
var router = app.loopback.Router(); const router = app.loopback.Router();
var User = app.models.User; const api = app.loopback.Router();
const User = app.models.User;
function findUserByUsername$(username, fields) { function findUserByUsername$(username, fields) {
return observeQuery( return observeQuery(
User, User,
@ -145,39 +147,39 @@ module.exports = function(app) {
router.get('/signin', getSignin); router.get('/signin', getSignin);
router.get('/signout', signout); router.get('/signout', signout);
router.get('/forgot', getForgot); router.get('/forgot', getForgot);
router.post('/forgot', postForgot); api.post('/forgot', postForgot);
router.get('/reset-password', getReset); router.get('/reset-password', getReset);
router.post('/reset-password', postReset); api.post('/reset-password', postReset);
router.get('/email-signup', getEmailSignup); router.get('/email-signup', getEmailSignup);
router.get('/email-signin', getEmailSignin); router.get('/email-signin', getEmailSignin);
router.get('/deprecated-signin', getDepSignin); router.get('/deprecated-signin', getDepSignin);
router.get('/update-email', getUpdateEmail); router.get('/update-email', getUpdateEmail);
router.get( api.get(
'/toggle-lockdown-mode', '/toggle-lockdown-mode',
sendNonUserToMap, sendNonUserToMap,
toggleLockdownMode toggleLockdownMode
); );
router.get( api.get(
'/toggle-announcement-email-mode', '/toggle-announcement-email-mode',
sendNonUserToMap, sendNonUserToMap,
toggleReceivesAnnouncementEmails toggleReceivesAnnouncementEmails
); );
router.get( api.get(
'/toggle-notification-email-mode', '/toggle-notification-email-mode',
sendNonUserToMap, sendNonUserToMap,
toggleReceivesNotificationEmails toggleReceivesNotificationEmails
); );
router.get( api.get(
'/toggle-quincy-email-mode', '/toggle-quincy-email-mode',
sendNonUserToMap, sendNonUserToMap,
toggleReceivesQuincyEmails toggleReceivesQuincyEmails
); );
router.post( api.post(
'/account/delete', '/account/delete',
ifNoUser401, ifNoUser401,
postDeleteAccount postDeleteAccount
); );
router.get( api.get(
'/account', '/account',
sendNonUserToMap, sendNonUserToMap,
getAccount getAccount
@ -188,32 +190,31 @@ module.exports = function(app) {
flashIfNotVerified, flashIfNotVerified,
getSettings getSettings
); );
// router.get('/vote1', vote1);
// router.get('/vote2', vote2);
// Ensure these are the last routes! // Ensure these are the last routes!
router.get( api.get(
'/:username/front-end-certification', '/:username/front-end-certification',
showCert.bind(null, certTypes.frontEnd) showCert.bind(null, certTypes.frontEnd)
); );
router.get( api.get(
'/:username/data-visualization-certification', '/:username/data-visualization-certification',
showCert.bind(null, certTypes.dataVis) showCert.bind(null, certTypes.dataVis)
); );
router.get( api.get(
'/:username/back-end-certification', '/:username/back-end-certification',
showCert.bind(null, certTypes.backEnd) showCert.bind(null, certTypes.backEnd)
); );
router.get( api.get(
'/:username/full-stack-certification', '/:username/full-stack-certification',
(req, res) => res.redirect(req.url.replace('full-stack', 'back-end')) (req, res) => res.redirect(req.url.replace('full-stack', 'back-end'))
); );
router.get('/:username', returnUser); router.get('/:username', returnUser);
app.use('/:lang', router);
app.use(router); app.use(router);
function getSignin(req, res) { function getSignin(req, res) {
@ -280,7 +281,7 @@ module.exports = function(app) {
function returnUser(req, res, next) { function returnUser(req, res, next) {
const username = req.params.username.toLowerCase(); const username = req.params.username.toLowerCase();
const { user, path } = req; const { user } = req;
// timezone of signed-in account // timezone of signed-in account
// to show all date related components // to show all date related components
@ -298,10 +299,7 @@ module.exports = function(app) {
return User.findOne$(query) return User.findOne$(query)
.filter(userPortfolio => { .filter(userPortfolio => {
if (!userPortfolio) { if (!userPortfolio) {
req.flash('errors', { next();
msg: `We couldn't find a page for ${ path }`
});
res.redirect('/');
} }
return !!userPortfolio; return !!userPortfolio;
}) })
@ -354,7 +352,8 @@ module.exports = function(app) {
calender, calender,
github: userPortfolio.githubURL, github: userPortfolio.githubURL,
moment, moment,
encodeFcc encodeFcc,
supportedLanguages
})); }));
}) })
.doOnNext(data => { .doOnNext(data => {

View File

@ -0,0 +1,27 @@
import supportedLanguages from '../../common/utils/supported-languages';
import passThroughs from '../utils/lang-passthrough-urls';
// import debug from 'debug';
// const log = debug('fcc:controller:lang-redirect');
const toLowerCase = String.prototype.toLowerCase;
export default function redirectLang(app) {
app.all('*', function(req, res, next) {
const { url, path } = req;
const langCode = toLowerCase.call(url.split('/')[1]);
if (passThroughs[langCode]) {
return next();
}
if (supportedLanguages[langCode]) {
req.flash('errors', {
msg: `404: We couldn't find path ${ path }`
});
return res.redirect('/map');
}
// language aware redirect
return res.redirect(url);
});
}

View File

@ -50,6 +50,7 @@
"./middlewares/global-locals": {}, "./middlewares/global-locals": {},
"./middlewares/revision-helpers": {}, "./middlewares/revision-helpers": {},
"./middlewares/migrate-completed-challenges": {}, "./middlewares/migrate-completed-challenges": {},
"./middlewares/add-lang": {},
"./middlewares/flash-cheaters": {} "./middlewares/flash-cheaters": {}
}, },
"files": {}, "files": {},

View File

@ -0,0 +1,66 @@
import supportedLanguages from '../../common/utils/supported-languages';
import passthroughs from '../utils/lang-passthrough-urls';
import debug from 'debug';
const log = debug('fcc:middlewares:lang');
const langTagRegex = /^[a-z]{2}(?:-[a-zA-Z]{2,3})?$/;
const toLowerCase = String.prototype.toLowerCase;
// redirect(statusOrUrl: String|Number, url?: String) => Void
function langRedirect(...args) {
const url = args.length === 2 ? args[1] : args[0];
const { lang } = this.req;
const maybeLang = toLowerCase.call(url.split('/')[1]);
if (
passthroughs[maybeLang] ||
supportedLanguages[maybeLang]
) {
return this._oldRedirect(...arguments);
}
// if language present add to url
if (lang) {
return this._oldRedirect(`/${lang}${url}`);
}
// default to english
return this._oldRedirect(`/en${url}`);
}
// prefer url lang over user lang
// if url lang is not supported move to user lang
// if user lang is not supported default to english
export default function addLang() {
return function(req, res, next) {
const { url, user = {} } = req;
const maybeLang = url.split('/')[1];
const restUrl = url.split('/').slice(2).join('/');
const userLang = user.languageTag;
let finalLang;
if (supportedLanguages[maybeLang]) {
finalLang = maybeLang;
} else if (supportedLanguages[userLang]) {
finalLang = userLang;
} else {
finalLang = 'en';
}
// found url lang tag that is not yet supported
// redirect to fix url with supported lang tag
if (langTagRegex.test(maybeLang) && !supportedLanguages[maybeLang]) {
log(`unsupported lang tag ${maybeLang}`);
return res.redirect(`/${finalLang}/${restUrl}`);
}
res.locals.supportedLanguages = supportedLanguages;
if (supportedLanguages[finalLang]) {
req.lang = finalLang;
res.locals.lang = finalLang;
}
res._oldRedirect = res.redirect;
res.redirect = langRedirect;
return next();
};
}

View File

@ -1,12 +1,13 @@
import { Observable } from 'rx'; import { Observable } from 'rx';
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr'; import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
import { nameify, dasherize, unDasherize } from '../utils';
import debug from 'debug'; import debug from 'debug';
import { nameify, dasherize, unDasherize } from '../utils';
import supportedLanguages from '../../common/utils/supported-languages';
const isDev = process.env.NODE_ENV !== 'production'; const isDev = process.env.NODE_ENV !== 'production';
const isBeta = !!process.env.BETA; const isBeta = !!process.env.BETA;
const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i; const challengesRegex = /^(bonfire|waypoint|zipline|basejump|checkpoint)/i;
const log = debug('fcc:challenges'); const log = debug('fcc:services:challenges');
const challenge = new Schema('challenge', { idAttribute: 'dashedName' }); const challenge = new Schema('challenge', { idAttribute: 'dashedName' });
const block = new Schema('block', { idAttribute: 'dashedName' }); const block = new Schema('block', { idAttribute: 'dashedName' });
const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' }); const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' });
@ -99,6 +100,34 @@ function cachedMap(Block) {
.shareReplay(); .shareReplay();
} }
function mapChallengeToLang({ translations = {}, ...challenge }, lang) {
if (!supportedLanguages[lang]) {
lang = 'en';
}
if (lang !== 'en') {
challenge.title =
translations[lang] && translations[lang].title ||
challenge.title;
challenge.description =
translations[lang] && translations[lang].description ||
challenge.description;
}
return challenge;
}
function getMapForLang(lang) {
return ({ entities: { challenge: challengeMap, ...entities }, result }) => {
entities.challenge = Object.keys(challengeMap)
.reduce((translatedChallengeMap, key) => {
translatedChallengeMap[key] =
mapChallengeToLang(challengeMap[key], lang);
return translatedChallengeMap;
}, {});
return { result, entities };
};
}
function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) { function shouldNotFilterComingSoon({ isComingSoon, isBeta: challengeIsBeta }) {
return isDev || return isDev ||
!isComingSoon || !isComingSoon ||
@ -123,7 +152,8 @@ function getFirstChallenge(challengeMap$) {
function getChallengeAndBlock( function getChallengeAndBlock(
challengeDashedName, challengeDashedName,
blockDashedName, blockDashedName,
challengeMap$ challengeMap$,
lang
) { ) {
return challengeMap$ return challengeMap$
.flatMap(({ entities }) => { .flatMap(({ entities }) => {
@ -134,7 +164,11 @@ function getChallengeAndBlock(
!challenge || !challenge ||
!shouldNotFilterComingSoon(challenge) !shouldNotFilterComingSoon(challenge)
) { ) {
return getChallengeByDashedName(challengeDashedName, challengeMap$); return getChallengeByDashedName(
challengeDashedName,
challengeMap$,
lang
);
} }
return Observable.just({ return Observable.just({
redirect: block.dashedName !== blockDashedName ? redirect: block.dashedName !== blockDashedName ?
@ -142,7 +176,7 @@ function getChallengeAndBlock(
false, false,
entities: { entities: {
challenge: { challenge: {
[challenge.dashedName]: challenge [challenge.dashedName]: mapChallengeToLang(challenge, lang)
} }
}, },
result: { result: {
@ -153,7 +187,7 @@ function getChallengeAndBlock(
}); });
} }
function getChallengeByDashedName(dashedName, challengeMap$) { function getChallengeByDashedName(dashedName, challengeMap$, lang) {
const challengeName = unDasherize(dashedName) const challengeName = unDasherize(dashedName)
.replace(challengesRegex, ''); .replace(challengesRegex, '');
const testChallengeName = new RegExp(challengeName, 'i'); const testChallengeName = new RegExp(challengeName, 'i');
@ -179,7 +213,11 @@ function getChallengeByDashedName(dashedName, challengeMap$) {
.map(challenge => ({ .map(challenge => ({
redirect: redirect:
`/challenges/${challenge.block}/${challenge.dashedName}`, `/challenges/${challenge.block}/${challenge.dashedName}`,
entities: { challenge: { [challenge.dashedName]: challenge } }, entities: {
challenge: {
[challenge.dashedName]: mapChallengeToLang(challenge, lang)
}
},
result: { result: {
challenge: challenge.dashedName, challenge: challenge.dashedName,
block: challenge.block block: challenge.block
@ -192,16 +230,19 @@ export default function mapService(app) {
const challengeMap$ = cachedMap(Block); const challengeMap$ = cachedMap(Block);
return { return {
name: 'map', name: 'map',
read: (req, resource, { block, dashedName } = {}, config, cb) => { read: (req, resource, { lang, block, dashedName } = {}, config, cb) => {
log(`${lang} language requested`);
if (block && dashedName) { if (block && dashedName) {
return getChallengeAndBlock(dashedName, block, challengeMap$) return getChallengeAndBlock(dashedName, block, challengeMap$, lang)
.subscribe(challenge => cb(null, challenge), cb); .subscribe(challenge => cb(null, challenge), cb);
} }
if (dashedName) { if (dashedName) {
return getChallengeByDashedName(dashedName, challengeMap$) return getChallengeByDashedName(dashedName, challengeMap$, lang)
.subscribe(challenge => cb(null, challenge), cb); .subscribe(challenge => cb(null, challenge), cb);
} }
return challengeMap$.subscribe(map => cb(null, map), cb); return challengeMap$
.map(getMapForLang(lang))
.subscribe(map => cb(null, map), cb);
} }
}; };
} }

View File

@ -0,0 +1,4 @@
export default [
'auth',
'services'
].reduce((throughs, route) => (throughs[route] = true, throughs), {});

View File

@ -103,7 +103,13 @@ block content
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/update-email') a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/update-email')
i.fa.fa-envelope i.fa.fa-envelope
| Update my Email | Update my Email
if supportedLanguages
.col-xs-12
select#lang-select.form-control.btn.btn-lg.btn-block.btn-primary.btn-link-social(name='langTag')
option(disabled selected=languageTag ? false : true) Prefered Language
for languageDisplay, lang in supportedLanguages
option(value=lang selected=lang === languageTag ? 'selected' : false)= languageDisplay
option(disabled) More to come...
.spacer .spacer
h2.text-center Danger Zone h2.text-center Danger Zone
.row .row