Add language routing
This commit is contained in:
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -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 } = {}) => {
|
||||||
|
@ -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,
|
||||||
|
42
common/app/utils/Language-Link.jsx
Normal file
42
common/app/utils/Language-Link.jsx
Normal 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);
|
@ -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": {}
|
||||||
|
4
common/utils/supported-languages.js
Normal file
4
common/utils/supported-languages.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default {
|
||||||
|
en: 'English',
|
||||||
|
es: 'Spanish'
|
||||||
|
};
|
@ -83,5 +83,5 @@ export default function about(app) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
router.get('/about', showAbout);
|
router.get('/about', showAbout);
|
||||||
app.use(router);
|
app.use('/:lang', router);
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -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: {
|
||||||
|
14
server/boot/a-react.js → server/boot/react.js
vendored
14
server/boot/a-react.js → server/boot/react.js
vendored
@ -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 }) => {
|
@ -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 => {
|
||||||
|
27
server/boot/z-lang-redirect.js
Normal file
27
server/boot/z-lang-redirect.js
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
@ -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": {},
|
||||||
|
66
server/middlewares/add-lang.js
Normal file
66
server/middlewares/add-lang.js
Normal 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();
|
||||||
|
};
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
4
server/utils/lang-passthrough-urls.js
Normal file
4
server/utils/lang-passthrough-urls.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
export default [
|
||||||
|
'auth',
|
||||||
|
'services'
|
||||||
|
].reduce((throughs, route) => (throughs[route] = true, throughs), {});
|
@ -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
|
||||||
|
Reference in New Issue
Block a user