diff --git a/common/app/routes/challenges/components/map/Block.jsx b/common/app/routes/challenges/components/map/Block.jsx
index 8b807a36ae..6f41a714dd 100644
--- a/common/app/routes/challenges/components/map/Block.jsx
+++ b/common/app/routes/challenges/components/map/Block.jsx
@@ -1,5 +1,5 @@
import React, { PropTypes } from 'react';
-import { Link } from 'react-router';
+import LangLink from '../../../../utils/Language-Link.jsx';
import { connect } from 'react-redux';
import FA from 'react-fontawesome';
import PureComponent from 'react-pure-render/component';
@@ -54,7 +54,7 @@ export class Block extends PureComponent {
className={ challengeClassName }
key={ title }
>
-
+
updateCurrentChallenge(challenge) }
>
@@ -66,7 +66,7 @@ export class Block extends PureComponent {
''
}
-
+
);
});
diff --git a/common/app/routes/challenges/redux/fetch-challenges-saga.js b/common/app/routes/challenges/redux/fetch-challenges-saga.js
index b785ca2a37..8fccade58c 100644
--- a/common/app/routes/challenges/redux/fetch-challenges-saga.js
+++ b/common/app/routes/challenges/redux/fetch-challenges-saga.js
@@ -24,6 +24,7 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
))
.flatMap(({ type, payload: { dashedName, block } = {} }) => {
const state = getState();
+ const lang = state.app.languageTag;
if (type === replaceChallenge) {
const { challenge: newChallenge } = challengeSelector({
...state,
@@ -38,8 +39,10 @@ export default function fetchChallengesSaga(action$, getState, { services }) {
return Observable.just(null);
}
const options = { service: 'map' };
+ options.params = { lang };
if (type === fetchChallenge) {
- options.params = { dashedName, block };
+ options.params.dashedName = dashedName;
+ options.params.block = block;
}
return services.readService$(options)
.flatMap(({ entities, result, redirect } = {}) => {
diff --git a/common/app/routes/index.js b/common/app/routes/index.js
index b2ea7c0155..67e6e2bac0 100644
--- a/common/app/routes/index.js
+++ b/common/app/routes/index.js
@@ -2,7 +2,7 @@ import { modernChallenges, map, challenges } from './challenges';
import NotFound from '../components/NotFound/index.jsx';
export default {
- path: '/',
+ path: '/:lang',
childRoutes: [
challenges,
modernChallenges,
diff --git a/common/app/utils/Language-Link.jsx b/common/app/utils/Language-Link.jsx
new file mode 100644
index 0000000000..0d6cf0d445
--- /dev/null
+++ b/common/app/utils/Language-Link.jsx
@@ -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 (
+
+ );
+ }
+}
+
+export default connect(mapStateToProps)(LangLink);
diff --git a/common/models/user.json b/common/models/user.json
index 9342b5cdb5..84e4e9ad51 100644
--- a/common/models/user.json
+++ b/common/models/user.json
@@ -195,6 +195,11 @@
"theme": {
"type": "string",
"default": "default"
+ },
+ "languageTag": {
+ "type": "string",
+ "description": "A IETF language tag",
+ "default": "en"
}
},
"validations": [],
@@ -256,6 +261,13 @@
"principalId": "$owner",
"permission": "ALLOW",
"property": "updateTheme"
+ },
+ {
+ "accessType": "EXECUTE",
+ "principalType": "ROLE",
+ "principalId": "$owner",
+ "permission": "ALLOW",
+ "property": "updateLanguage"
}
],
"methods": {}
diff --git a/common/utils/supported-languages.js b/common/utils/supported-languages.js
new file mode 100644
index 0000000000..536ad617f4
--- /dev/null
+++ b/common/utils/supported-languages.js
@@ -0,0 +1,4 @@
+export default {
+ en: 'English',
+ es: 'Spanish'
+};
diff --git a/server/boot/about.js b/server/boot/about.js
index c58153577d..2665dffaef 100644
--- a/server/boot/about.js
+++ b/server/boot/about.js
@@ -83,5 +83,5 @@ export default function about(app) {
}
router.get('/about', showAbout);
- app.use(router);
+ app.use('/:lang', router);
}
diff --git a/server/boot/commit.js b/server/boot/commit.js
index 8f900fb59a..85e414533e 100644
--- a/server/boot/commit.js
+++ b/server/boot/commit.js
@@ -50,6 +50,7 @@ function findNonprofit(name) {
export default function commit(app) {
const router = app.loopback.Router();
+ const api = app.loopback.Router();
const { Pledge } = app.models;
router.get(
@@ -68,19 +69,20 @@ export default function commit(app) {
renderDirectory
);
- router.post(
+ api.post(
'/commit/stop-commitment',
sendNonUserToCommit,
stopCommit
);
- router.post(
+ api.post(
'/commit/complete-goal',
sendNonUserToCommit,
completeCommitment
);
- app.use(router);
+ app.use(api);
+ app.use(':/lang', router);
function commitToNonprofit(req, res, next) {
const { user } = req;
diff --git a/server/boot/home.js b/server/boot/home.js
index a4e01ece1e..f788e8e17c 100644
--- a/server/boot/home.js
+++ b/server/boot/home.js
@@ -1,4 +1,5 @@
import { defaultProfileImage } from '../../common/utils/constantStrings.json';
+import supportedLanguages from '../../common/utils/supported-languages';
const message =
'Learn to Code and Help Nonprofits';
@@ -6,24 +7,37 @@ const message =
module.exports = function(app) {
var router = app.loopback.Router();
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);
function addDefaultImage(req, res, next) {
if (!req.user || req.user.picture) {
return next();
}
- req.user.picture = defaultProfileImage;
- return req.user.save(function(err) {
- if (err) { return next(err); }
- return next();
- });
+ return req.user.update$({ picture: defaultProfileImage })
+ .subscribe(
+ () => next(),
+ next
+ );
}
- function index(req, res) {
+ function index(req, res, next) {
+ if (!supportedLanguages[req._urlLang]) {
+ return next();
+ }
+
if (req.user) {
return res.redirect('/challenges/current-challenge');
}
+
return res.render('home', { title: message });
}
};
diff --git a/server/boot/randomAPIs.js b/server/boot/randomAPIs.js
index 57ea58a5f4..1423ca9d5c 100644
--- a/server/boot/randomAPIs.js
+++ b/server/boot/randomAPIs.js
@@ -6,24 +6,32 @@ import secrets from '../../config/secrets';
module.exports = function(app) {
const router = app.loopback.Router();
const User = app.models.User;
- router.get('/api/github', githubCalls);
- router.get('/chat', chat);
- router.get('/coding-bootcamp-cost-calculator', bootcampCalculator);
- router.get('/twitch', twitch);
- router.get('/pmi-acp-agile-project-managers', agileProjectManagers);
- router.get('/pmi-acp-agile-project-managers-form', agileProjectManagersForm);
+ const noLangRouter = app.loopback.Router();
+ noLangRouter.get('/api/github', githubCalls);
+ noLangRouter.get('/chat', chat);
+ noLangRouter.get('/twitch', twitch);
+ noLangRouter.get('/unsubscribe/:email', unsubscribeMonthly);
+ 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-form', nonprofitsForm);
- router.get('/unsubscribe/:email', unsubscribeMonthly);
- router.get('/unsubscribe-notifications/:email', unsubscribeNotifications);
- router.get('/unsubscribe-quincy/:email', unsubscribeQuincy);
- router.get('/unsubscribed', unsubscribed);
- router.get('/get-started', getStarted);
- router.get('/submit-cat-photo', submitCatPhoto);
+ router.get('/pmi-acp-agile-project-managers', agileProjectManagers);
+ router.get('/pmi-acp-agile-project-managers-form', agileProjectManagersForm);
+ router.get('/coding-bootcamp-cost-calculator', bootcampCalculator);
router.get('/stories', showTestimonials);
router.get('/shop', showShop);
- router.get('/shop/cancel-stickers', cancelStickers);
- router.get('/shop/confirm-stickers', confirmStickers);
router.get('/all-stories', showAllTestimonials);
router.get('/terms', terms);
router.get('/privacy', privacy);
@@ -34,12 +42,9 @@ module.exports = function(app) {
);
router.get('/code-of-conduct', codeOfConduct);
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) {
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) {
var githubHeaders = {
headers: {
diff --git a/server/boot/a-react.js b/server/boot/react.js
similarity index 88%
rename from server/boot/a-react.js
rename to server/boot/react.js
index a627c10630..41de457b72 100644
--- a/server/boot/a-react.js
+++ b/server/boot/react.js
@@ -12,8 +12,6 @@ const log = debug('fcc:react-server');
// add routes here as they slowly get reactified
// remove their individual controllers
const routes = [
- '/videos',
- '/videos/*',
'/challenges',
'/challenges/*',
'/map'
@@ -24,6 +22,12 @@ const devRoutes = [];
export default function reactSubRouter(app) {
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
routes.forEach((route) => {
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) {
+ const { lang } = req;
const serviceOptions = { req };
createApp({
serviceOptions,
- location: req.path
+ location: req.originalUrl,
+ initialState: { app: { languageTag: lang } }
})
// if react-router does not find a route send down the chain
.filter(({ redirect, props }) => {
diff --git a/server/boot/user.js b/server/boot/user.js
index ad08bee719..18c13119a1 100644
--- a/server/boot/user.js
+++ b/server/boot/user.js
@@ -3,6 +3,7 @@ import moment from 'moment-timezone';
import { Observable } from 'rx';
import debugFactory from 'debug';
+import supportedLanguages from '../../common/utils/supported-languages';
import {
frontEndChallengeId,
dataVisChallengeId,
@@ -123,8 +124,9 @@ function buildDisplayChallenges(challengeMap = {}, timezone) {
}
module.exports = function(app) {
- var router = app.loopback.Router();
- var User = app.models.User;
+ const router = app.loopback.Router();
+ const api = app.loopback.Router();
+ const User = app.models.User;
function findUserByUsername$(username, fields) {
return observeQuery(
User,
@@ -145,39 +147,39 @@ module.exports = function(app) {
router.get('/signin', getSignin);
router.get('/signout', signout);
router.get('/forgot', getForgot);
- router.post('/forgot', postForgot);
+ api.post('/forgot', postForgot);
router.get('/reset-password', getReset);
- router.post('/reset-password', postReset);
+ api.post('/reset-password', postReset);
router.get('/email-signup', getEmailSignup);
router.get('/email-signin', getEmailSignin);
router.get('/deprecated-signin', getDepSignin);
router.get('/update-email', getUpdateEmail);
- router.get(
+ api.get(
'/toggle-lockdown-mode',
sendNonUserToMap,
toggleLockdownMode
);
- router.get(
+ api.get(
'/toggle-announcement-email-mode',
sendNonUserToMap,
toggleReceivesAnnouncementEmails
);
- router.get(
+ api.get(
'/toggle-notification-email-mode',
sendNonUserToMap,
toggleReceivesNotificationEmails
);
- router.get(
+ api.get(
'/toggle-quincy-email-mode',
sendNonUserToMap,
toggleReceivesQuincyEmails
);
- router.post(
+ api.post(
'/account/delete',
ifNoUser401,
postDeleteAccount
);
- router.get(
+ api.get(
'/account',
sendNonUserToMap,
getAccount
@@ -188,32 +190,31 @@ module.exports = function(app) {
flashIfNotVerified,
getSettings
);
- // router.get('/vote1', vote1);
- // router.get('/vote2', vote2);
// Ensure these are the last routes!
- router.get(
+ api.get(
'/:username/front-end-certification',
showCert.bind(null, certTypes.frontEnd)
);
- router.get(
+ api.get(
'/:username/data-visualization-certification',
showCert.bind(null, certTypes.dataVis)
);
- router.get(
+ api.get(
'/:username/back-end-certification',
showCert.bind(null, certTypes.backEnd)
);
- router.get(
+ api.get(
'/:username/full-stack-certification',
(req, res) => res.redirect(req.url.replace('full-stack', 'back-end'))
);
router.get('/:username', returnUser);
+ app.use('/:lang', router);
app.use(router);
function getSignin(req, res) {
@@ -280,7 +281,7 @@ module.exports = function(app) {
function returnUser(req, res, next) {
const username = req.params.username.toLowerCase();
- const { user, path } = req;
+ const { user } = req;
// timezone of signed-in account
// to show all date related components
@@ -298,10 +299,7 @@ module.exports = function(app) {
return User.findOne$(query)
.filter(userPortfolio => {
if (!userPortfolio) {
- req.flash('errors', {
- msg: `We couldn't find a page for ${ path }`
- });
- res.redirect('/');
+ next();
}
return !!userPortfolio;
})
@@ -354,7 +352,8 @@ module.exports = function(app) {
calender,
github: userPortfolio.githubURL,
moment,
- encodeFcc
+ encodeFcc,
+ supportedLanguages
}));
})
.doOnNext(data => {
diff --git a/server/boot/z-lang-redirect.js b/server/boot/z-lang-redirect.js
new file mode 100644
index 0000000000..b4ba8ded74
--- /dev/null
+++ b/server/boot/z-lang-redirect.js
@@ -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);
+ });
+}
diff --git a/server/middleware.json b/server/middleware.json
index 5732969e4e..08b468111d 100644
--- a/server/middleware.json
+++ b/server/middleware.json
@@ -50,6 +50,7 @@
"./middlewares/global-locals": {},
"./middlewares/revision-helpers": {},
"./middlewares/migrate-completed-challenges": {},
+ "./middlewares/add-lang": {},
"./middlewares/flash-cheaters": {}
},
"files": {},
diff --git a/server/middlewares/add-lang.js b/server/middlewares/add-lang.js
new file mode 100644
index 0000000000..e59f58078d
--- /dev/null
+++ b/server/middlewares/add-lang.js
@@ -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();
+ };
+}
diff --git a/server/services/map.js b/server/services/map.js
index 5caba3cd89..5f56975983 100644
--- a/server/services/map.js
+++ b/server/services/map.js
@@ -1,12 +1,13 @@
import { Observable } from 'rx';
import { Schema, valuesOf, arrayOf, normalize } from 'normalizr';
-import { nameify, dasherize, unDasherize } from '../utils';
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 isBeta = !!process.env.BETA;
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 block = new Schema('block', { idAttribute: 'dashedName' });
const superBlock = new Schema('superBlock', { idAttribute: 'dashedName' });
@@ -99,6 +100,34 @@ function cachedMap(Block) {
.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 }) {
return isDev ||
!isComingSoon ||
@@ -123,7 +152,8 @@ function getFirstChallenge(challengeMap$) {
function getChallengeAndBlock(
challengeDashedName,
blockDashedName,
- challengeMap$
+ challengeMap$,
+ lang
) {
return challengeMap$
.flatMap(({ entities }) => {
@@ -134,7 +164,11 @@ function getChallengeAndBlock(
!challenge ||
!shouldNotFilterComingSoon(challenge)
) {
- return getChallengeByDashedName(challengeDashedName, challengeMap$);
+ return getChallengeByDashedName(
+ challengeDashedName,
+ challengeMap$,
+ lang
+ );
}
return Observable.just({
redirect: block.dashedName !== blockDashedName ?
@@ -142,7 +176,7 @@ function getChallengeAndBlock(
false,
entities: {
challenge: {
- [challenge.dashedName]: challenge
+ [challenge.dashedName]: mapChallengeToLang(challenge, lang)
}
},
result: {
@@ -153,7 +187,7 @@ function getChallengeAndBlock(
});
}
-function getChallengeByDashedName(dashedName, challengeMap$) {
+function getChallengeByDashedName(dashedName, challengeMap$, lang) {
const challengeName = unDasherize(dashedName)
.replace(challengesRegex, '');
const testChallengeName = new RegExp(challengeName, 'i');
@@ -179,7 +213,11 @@ function getChallengeByDashedName(dashedName, challengeMap$) {
.map(challenge => ({
redirect:
`/challenges/${challenge.block}/${challenge.dashedName}`,
- entities: { challenge: { [challenge.dashedName]: challenge } },
+ entities: {
+ challenge: {
+ [challenge.dashedName]: mapChallengeToLang(challenge, lang)
+ }
+ },
result: {
challenge: challenge.dashedName,
block: challenge.block
@@ -192,16 +230,19 @@ export default function mapService(app) {
const challengeMap$ = cachedMap(Block);
return {
name: 'map',
- read: (req, resource, { block, dashedName } = {}, config, cb) => {
+ read: (req, resource, { lang, block, dashedName } = {}, config, cb) => {
+ log(`${lang} language requested`);
if (block && dashedName) {
- return getChallengeAndBlock(dashedName, block, challengeMap$)
+ return getChallengeAndBlock(dashedName, block, challengeMap$, lang)
.subscribe(challenge => cb(null, challenge), cb);
}
if (dashedName) {
- return getChallengeByDashedName(dashedName, challengeMap$)
+ return getChallengeByDashedName(dashedName, challengeMap$, lang)
.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);
}
};
}
diff --git a/server/utils/lang-passthrough-urls.js b/server/utils/lang-passthrough-urls.js
new file mode 100644
index 0000000000..4799f3c457
--- /dev/null
+++ b/server/utils/lang-passthrough-urls.js
@@ -0,0 +1,4 @@
+export default [
+ 'auth',
+ 'services'
+].reduce((throughs, route) => (throughs[route] = true, throughs), {});
diff --git a/server/views/account/settings.jade b/server/views/account/settings.jade
index 4733bab150..fee9250cae 100644
--- a/server/views/account/settings.jade
+++ b/server/views/account/settings.jade
@@ -103,7 +103,13 @@ block content
a.btn.btn-lg.btn-block.btn-primary.btn-link-social(href='/update-email')
i.fa.fa-envelope
| 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
h2.text-center Danger Zone
.row